1 package org.collectionspace.services.listener;
3 import java.util.ArrayList;
4 import java.util.GregorianCalendar;
5 import java.util.HashMap;
8 import org.apache.commons.logging.Log;
9 import org.apache.commons.logging.LogFactory;
10 import org.collectionspace.services.client.workflow.WorkflowClient;
11 import org.collectionspace.services.common.api.RefNameUtils;
12 import org.collectionspace.services.common.api.Tools;
13 import org.collectionspace.services.movement.nuxeo.MovementConstants;
14 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
15 import org.nuxeo.ecm.core.api.ClientException;
16 import org.nuxeo.ecm.core.api.CoreSession;
17 import org.nuxeo.ecm.core.api.DocumentModel;
18 import org.nuxeo.ecm.core.api.DocumentModelList;
19 import org.nuxeo.ecm.core.event.Event;
20 import org.nuxeo.ecm.core.event.EventContext;
21 import org.nuxeo.ecm.core.event.EventListener;
22 import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
24 public class UpdateObjectLocationOnMove implements EventListener {
26 // FIXME: We might experiment here with using log4j instead of Apache Commons Logging;
27 // am using the latter to follow Ray's pattern for now
28 private final Log logger = LogFactory.getLog(UpdateObjectLocationOnMove.class);
29 // FIXME: Make the following message, or its equivalent, a constant usable by all event listeners
30 private final String NO_FURTHER_PROCESSING_MESSAGE =
31 "This event listener will not continue processing this event ...";
32 private final List<String> relevantDocTypesList = new ArrayList<String>();
33 GregorianCalendar EARLIEST_COMPARISON_DATE = new GregorianCalendar(1600, 1, 1);
34 private final static String RELATIONS_COMMON_SCHEMA = "relations_common"; // FIXME: Get from external constant
35 private final static String RELATION_DOCTYPE = "Relation"; // FIXME: Get from external constant
36 private final static String SUBJECT_CSID_PROPERTY = "subjectCsid"; // FIXME: Get from external constant
37 private final static String OBJECT_CSID_PROPERTY = "objectCsid"; // FIXME: Get from external constant
38 private final static String COLLECTIONOBJECTS_COMMON_SCHEMA = "collectionobjects_common"; // FIXME: Get from external constant
39 private final static String COLLECTIONOBJECT_DOCTYPE = "CollectionObject"; // FIXME: Get from external constant
40 private final static String COMPUTED_CURRENT_LOCATION_PROPERTY = "computedCurrentLocation"; // FIXME: Create and then get from external constant
41 private final static String MOVEMENTS_COMMON_SCHEMA = "movements_common"; // FIXME: Get from external constant
42 private final static String MOVEMENT_DOCTYPE = "Movement"; // FIXME: Get from external constant
43 private final static String LOCATION_DATE_PROPERTY = "locationDate"; // FIXME: Get from external constant
44 private final static String CURRENT_LOCATION_PROPERTY = "currentLocation"; // FIXME: Get from external constant
45 private final static String ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT =
46 "AND (ecm:currentLifeCycleState <> 'deleted') "
47 + "AND ecm:isProxy = 0 "
48 + "AND ecm:isCheckedInVersion = 0";
50 // ####################################################################
51 // FIXME: Per Rick, what happens if a relation record is updated,
52 // that either adds or removes a relation between a Movement
53 // record and a CollectionObject record? Do we need to listen
54 // for that event as well and update the CollectionObject record's
55 // computedCurrentLocation accordingly?
57 // The following code is currently only handling create and
58 // update events affecting Movement records.
59 // ####################################################################
60 // FIXME: We'll likely also need to handle workflow state transition and
61 // deletion events, where the soft or hard deletion of a Movement or
62 // Relation record effectively changes the current location for a CollectionObject.
64 public void handleEvent(Event event) throws ClientException {
66 logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
68 EventContext eventContext = event.getContext();
69 if (eventContext == null) {
73 if (!(eventContext instanceof DocumentEventContext)) {
76 DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
77 DocumentModel docModel = docEventContext.getSourceDocument();
79 // If this event does not involve one of our relevant doctypes,
80 // return without further handling the event.
82 // FIXME: We will likely need to add the Relation doctype here,
83 // along with additional code to handle such changes. (See comment above.)
84 boolean involvesRelevantDocType = false;
85 relevantDocTypesList.add(MovementConstants.NUXEO_DOCTYPE);
86 for (String docType : relevantDocTypesList) {
87 if (documentMatchesType(docModel, docType)) {
88 involvesRelevantDocType = true;
92 if (!involvesRelevantDocType) {
95 if (!isActiveDocument(docModel)) {
99 if (logger.isTraceEnabled()) {
100 logger.debug("An event involving an active document of the relevant type(s) was received by UpdateObjectLocationOnMove ...");
103 // Find CollectionObject records that are related to this Movement record:
105 // Via an NXQL query, get a list of (non-deleted) relation records where:
106 // * This movement record's CSID is the subject CSID of the relation.
107 // * The object document type is a CollectionObject doctype.
109 // Note: this assumes that every such relation is captured by
110 // relations with Movement-as-subject and CollectionObject-as-object,
111 // logic that matches that of the SQL function to obtain the computed
112 // current location of the CollectionObject.
114 // That may NOT always be the case; it's possible some such relations may
115 // exist only with CollectionObject-as-subject and Movement-as-object.
116 String movementCsid = NuxeoUtils.getCsid(docModel);
117 // CoreSession coreSession = docEventContext.getCoreSession();
118 CoreSession coreSession = docModel.getCoreSession();
119 String query = String.format(
120 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
121 + "(relations_common:subjectCsid = '%2$s' "
122 + "AND relations_common:objectDocumentType = '%3$s') "
123 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
124 RELATION_DOCTYPE, movementCsid, COLLECTIONOBJECT_DOCTYPE);
125 DocumentModelList relatedDocModels = coreSession.query(query);
126 if (relatedDocModels == null || relatedDocModels.isEmpty()) {
130 // Iterate through the list of Relation records found and build
131 // a list of CollectionObject CSIDs, by extracting the object CSIDs
132 // from those Relation records.
134 // FIXME: The following code might be refactored into a generic 'get property
135 // values from a list of document models' method, if this doesn't already exist.
137 List<String> collectionObjectCsids = new ArrayList<String>();
138 for (DocumentModel relatedDocModel : relatedDocModels) {
139 csid = (String) relatedDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
140 if (Tools.notBlank(csid)) {
141 collectionObjectCsids.add(csid);
144 if (collectionObjectCsids == null || collectionObjectCsids.isEmpty()) {
145 logger.warn("Could not obtain any CSIDs of related CollectionObject records.");
146 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
149 if (logger.isTraceEnabled()) {
150 logger.trace("Found " + collectionObjectCsids.size() + " CSIDs of related CollectionObject records.");
154 // Iterate through the list of CollectionObject CSIDs found.
155 DocumentModel collectionObjectDocModel = null;
156 String computedCurrentLocationRefName = "";
157 Map<DocumentModel, String> docModelsToUpdate = new HashMap<DocumentModel, String>();
158 for (String collectionObjectCsid : collectionObjectCsids) {
160 // Verify that the CollectionObject record is active.
161 collectionObjectDocModel = getDocModelFromCsid(coreSession, collectionObjectCsid);
162 if (!isActiveDocument(collectionObjectDocModel)) {
166 // Obtain the computed current location of that CollectionObject.
167 computedCurrentLocationRefName = computeCurrentLocation(coreSession, collectionObjectCsid);
168 if (logger.isTraceEnabled()) {
169 logger.trace("computedCurrentLocation refName=" + computedCurrentLocationRefName);
172 // Check that the value returned, which is expected to be a
173 // reference (refName) to a storage location authority term,
175 // * Non-null and non-blank. (We need to verify this assumption; can a
176 // CollectionObject's computed current location meaningfully be 'un-set'?)
177 // * Capable of being successfully parsed by an authority item parser;
178 // that is, returning a non-null parse result.
179 if ((Tools.notBlank(computedCurrentLocationRefName)
180 && (RefNameUtils.parseAuthorityTermInfo(computedCurrentLocationRefName) != null))) {
181 if (logger.isTraceEnabled()) {
182 logger.trace("refName passes basic validation tests.");
185 // If the value returned from the function passes validation,
186 // compare that value to the value in the computedCurrentLocation
187 // field of that CollectionObject
189 // If the CollectionObject does not already have a
190 // computedCurrentLocation value, or if the two values differ ...
191 String existingComputedCurrentLocationRefName =
192 (String) collectionObjectDocModel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY);
193 if (Tools.isBlank(existingComputedCurrentLocationRefName)
194 || !computedCurrentLocationRefName.equals(existingComputedCurrentLocationRefName)) {
195 if (logger.isTraceEnabled()) {
196 logger.trace("Existing computedCurrentLocation refName=" + existingComputedCurrentLocationRefName);
197 logger.trace("computedCurrentLocation refName requires updating.");
199 // ... set aside this CollectionObject's docModel and its new
200 // computed current location value for subsequent updating
201 docModelsToUpdate.put(collectionObjectDocModel, computedCurrentLocationRefName);
204 if (logger.isTraceEnabled()) {
205 logger.trace("computedCurrentLocation refName does NOT require updating.");
211 // For each CollectionObject docModel that has been set aside for updating,
212 // update its computedCurrentLocation field with its computed current
213 // location value returned from the SQL function.
214 for (Map.Entry<DocumentModel, String> entry : docModelsToUpdate.entrySet()) {
215 DocumentModel dmodel = entry.getKey();
216 String newCurrentLocationValue = entry.getValue();
217 dmodel.setProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY, newCurrentLocationValue);
218 coreSession.saveDocument(dmodel);
219 if (logger.isTraceEnabled()) {
220 String afterUpdateComputedCurrentLocationRefName =
221 (String) dmodel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY);
222 logger.trace("Following update, new computedCurrentLocation refName value=" + afterUpdateComputedCurrentLocationRefName);
228 // FIXME: Generic methods like many of those below might be split off,
229 // into an event utilities class, base classes, or otherwise. - ADR 2012-12-05
231 // FIXME: Identify whether the equivalent of the documentMatchesType utility
232 // method is already implemented and substitute a call to the latter if so.
233 // This may well already exist.
235 * Identifies whether a document matches a supplied document type.
237 * @param docModel a document model.
238 * @param docType a document type string.
239 * @return true if the document matches the supplied document type; false if
242 private boolean documentMatchesType(DocumentModel docModel, String docType) {
243 if (docModel == null || Tools.isBlank(docType)) {
246 if (docModel.getType().startsWith(docType)) {
254 * Identifies whether a document is an active document; that is, if it is
255 * not a versioned record; not a proxy (symbolic link to an actual record);
256 * and not in the 'deleted' workflow state.
258 * (A note relating the latter: Nuxeo appears to send 'documentModified'
259 * events even on workflow transitions, such when records are 'soft deleted'
260 * by being transitioned to the 'deleted' workflow state.)
263 * @return true if the document is an active document; false if it is not.
265 private boolean isActiveDocument(DocumentModel docModel) {
266 if (docModel == null) {
269 boolean isActiveDocument = false;
271 if (!docModel.isVersion()
272 && !docModel.isProxy()
273 && !docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
274 isActiveDocument = true;
276 } catch (ClientException ce) {
277 logger.warn("Error while identifying whether document is an active document: ", ce);
279 return isActiveDocument;
283 * Returns a document model for a record identified by a CSID.
285 * @param session a repository session.
286 * @param collectionObjectCsid a CollectionObject identifier (CSID)
287 * @return a document model for the record identified by the supplied CSID.
289 private DocumentModel getDocModelFromCsid(CoreSession session, String collectionObjectCsid) {
290 DocumentModelList collectionObjectDocModels = null;
292 final String query = "SELECT * FROM "
293 + NuxeoUtils.BASE_DOCUMENT_TYPE
295 + NuxeoUtils.getByNameWhereClause(collectionObjectCsid);
296 collectionObjectDocModels = session.query(query);
297 } catch (Exception e) {
298 logger.warn("Exception in query to get document model for CollectionObject: ", e);
300 if (collectionObjectDocModels == null || collectionObjectDocModels.isEmpty()) {
301 logger.warn("Could not get document models for CollectionObject(s).");
302 } else if (collectionObjectDocModels.size() != 1) {
303 logger.debug("Found more than 1 document with CSID=" + collectionObjectCsid);
305 return collectionObjectDocModels.get(0);
308 // FIXME: A quick first pass, using an only partly query-based technique for
309 // getting the current location, augmented by procedural code.
311 // Should be replaced by a more performant method, based entirely, or nearly so,
314 // E.g. the following is a sample CMIS query for retrieving Movement records
315 // related to a CollectionObject, which might serve as the basis for that query.
317 "SELECT DOC.nuxeo:pathSegment, DOC.dc:title, REL.dc:title,"
318 + "REL.relations_common:objectCsid, REL.relations_common:subjectCsid FROM Movement DOC "
319 + "JOIN Relation REL ON REL.relations_common:objectCsid = DOC.nuxeo:pathSegment "
320 + "WHERE REL.relations_common:subjectCsid = '5b4c617e-53a0-484b-804e' "
321 + "AND DOC.nuxeo:isVersion = false "
322 + "ORDER BY DOC.collectionspace_core:updatedAt DESC";
325 * Returns the computed current location for a CollectionObject.
327 * @param session a repository session.
328 * @param collectionObjectCsid a CollectionObject identifier (CSID)
329 * @throws ClientException
330 * @return the computed current location for the CollectionObject identified
331 * by the supplied CSID.
333 private String computeCurrentLocation(CoreSession session, String collectionObjectCsid)
334 throws ClientException {
335 String computedCurrentLocation = "";
336 // Get Relation records for Movements related to this CollectionObject
337 String query = String.format(
338 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
339 + "(relations_common:subjectCsid = '%2$s' "
340 + "AND relations_common:objectDocumentType = '%3$s') "
341 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
342 RELATION_DOCTYPE, collectionObjectCsid, MOVEMENT_DOCTYPE);
343 DocumentModelList relatedDocModels = session.query(query);
344 if (relatedDocModels == null || relatedDocModels.isEmpty()) {
345 return computedCurrentLocation;
348 // Iterate through related movement records, to get the CollectionObject's
349 // computed current location from the related Movement record with the
350 // most recent location date.
351 GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
352 DocumentModel movementDocModel = null;
354 String location = "";
355 for (DocumentModel relatedDocModel : relatedDocModels) {
356 // The object CSID in the relation is the related Movement record's CSID
357 csid = (String) relatedDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
358 movementDocModel = getDocModelFromCsid(session, csid);
359 GregorianCalendar locationDate =
360 (GregorianCalendar) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
361 if (locationDate == null) {
364 if (locationDate.after(mostRecentLocationDate)) {
365 mostRecentLocationDate = locationDate;
366 location = (String) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, CURRENT_LOCATION_PROPERTY);
368 if (Tools.notBlank(location)) {
369 computedCurrentLocation = location;
372 return computedCurrentLocation;