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 SUBJECT_DOCTYPE_PROPERTY = "subjectDocumentType"; // FIXME: Get from external constant
39 private final static String OBJECT_DOCTYPE_PROPERTY = "objectDocumentType"; // FIXME: Get from external constant
40 private final static String COLLECTIONOBJECTS_COMMON_SCHEMA = "collectionobjects_common"; // FIXME: Get from external constant
41 private final static String COLLECTIONOBJECT_DOCTYPE = "CollectionObject"; // FIXME: Get from external constant
42 private final static String COMPUTED_CURRENT_LOCATION_PROPERTY = "computedCurrentLocation"; // FIXME: Create and then get from external constant
43 private final static String MOVEMENTS_COMMON_SCHEMA = "movements_common"; // FIXME: Get from external constant
44 private final static String MOVEMENT_DOCTYPE = MovementConstants.NUXEO_DOCTYPE;
45 private final static String LOCATION_DATE_PROPERTY = "locationDate"; // FIXME: Get from external constant
46 private final static String CURRENT_LOCATION_PROPERTY = "currentLocation"; // FIXME: Get from external constant
47 private final static String ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT =
48 "AND (ecm:currentLifeCycleState <> 'deleted') "
49 + "AND ecm:isProxy = 0 "
50 + "AND ecm:isCheckedInVersion = 0";
53 public void handleEvent(Event event) throws ClientException {
55 logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
57 EventContext eventContext = event.getContext();
58 if (eventContext == null) {
62 if (!(eventContext instanceof DocumentEventContext)) {
65 DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
66 DocumentModel docModel = docEventContext.getSourceDocument();
68 // If this event does not involve one of the relevant doctypes
69 // designated as being handled by this event handler, return
70 // without further handling the event.
71 boolean involvesRelevantDocType = false;
72 relevantDocTypesList.add(MovementConstants.NUXEO_DOCTYPE);
73 relevantDocTypesList.add(RELATION_DOCTYPE);
74 for (String docType : relevantDocTypesList) {
75 if (documentMatchesType(docModel, docType)) {
76 involvesRelevantDocType = true;
80 if (!involvesRelevantDocType) {
84 if (logger.isTraceEnabled()) {
85 logger.trace("An event involving a document of the relevant type(s) was received by UpdateObjectLocationOnMove ...");
88 // Note: currently, all Document lifecycle transitions on
89 // the relevant doctype(s) are handled by this event handler,
90 // not just transitions between 'soft deleted' and active states.
92 // We are assuming that we'll want to re-compute current locations
93 // for related CollectionObjects on any such transitions, as the
94 // semantics of such transitions are opaque to this event handler,
95 // since arbitrary workflows can be bound to those doctype(s).
97 // If we need to filter out some of those lifecycle transitions,
98 // such as excluding transitions to the 'locked' workflow state; or,
99 // alternately, if we want to restrict this event handler's
100 // scope to handle only transitions into the 'soft deleted' state,
101 // we can add additional checks for doing so at this point.
103 // If this document event involves a Relation record, does this pertain to
104 // a relationship between a Movement record and a CollectionObject record?
106 // If not, we're not interested in processing this document event
107 // in this event handler, as it will have no bearing on updating a
108 // computed current location for a CollectionObject.
110 // If so, get the Movement CSID from the relation record.
111 // (The rest of the code flow below is then identical to that which
112 // is followed when this document event involves a Movement record.)
113 String movementCsid = "";
114 boolean eventPertainsToRelevantRelationship = false;
115 if (documentMatchesType(docModel, RELATION_DOCTYPE)) {
116 String subjectDocType = (String) docModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
117 String objectDocType = (String) docModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
118 if (subjectDocType.startsWith(MOVEMENT_DOCTYPE) && objectDocType.startsWith(COLLECTIONOBJECT_DOCTYPE)) {
119 eventPertainsToRelevantRelationship = true;
120 movementCsid = (String) docModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
121 } else if (subjectDocType.startsWith(COLLECTIONOBJECT_DOCTYPE) && objectDocType.startsWith(MOVEMENT_DOCTYPE)) {
122 eventPertainsToRelevantRelationship = true;
123 movementCsid = (String) docModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
125 if (!eventPertainsToRelevantRelationship) {
130 // If we haven't obtained the Movement CSID from a Relation record,
131 // then we can infer this is a Movement record (the only other type
132 // of document handled by this event handler), and obtain the CSID
133 // directly from that record.
134 if (Tools.isBlank(movementCsid)) {
135 movementCsid = NuxeoUtils.getCsid(docModel);
137 if (Tools.isBlank(movementCsid)) {
138 logger.warn("Could not obtain CSID for Movement record from document event.");
139 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
143 // Find CollectionObject records that are related to this Movement record:
145 // Via an NXQL query, get a list of active relation records where:
146 // * This movement record's CSID is the subject CSID of the relation,
147 // and its object document type is a CollectionObject doctype;
149 // * This movement record's CSID is the object CSID of the relation,
150 // and its subject document type is a CollectionObject doctype.
151 CoreSession coreSession = docEventContext.getCoreSession();
152 // Some values below are hard-coded for readability, rather than
153 // being obtained from constants.
154 String query = String.format(
155 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
157 + " (%2$s:subjectCsid = '%3$s' "
158 + " AND %2$s:objectDocumentType = '%4$s') "
160 + " (%2$s:objectCsid = '%3$s' "
161 + " AND %2$s:subjectDocumentType = '%4$s') "
163 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
164 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, movementCsid, COLLECTIONOBJECT_DOCTYPE);
165 DocumentModelList relatedDocModels = coreSession.query(query);
166 if (relatedDocModels == null || relatedDocModels.isEmpty()) {
170 // Iterate through the list of Relation records found and build
171 // a list of CollectionObject CSIDs, by extracting the object CSIDs
172 // from those Relation records.
174 // FIXME: The following code might be refactored into a generic 'get
175 // values of a single property from a list of document models' method,
176 // if this doesn't already exist.
178 List<String> collectionObjectCsids = new ArrayList<String>();
179 for (DocumentModel relatedDocModel : relatedDocModels) {
180 csid = (String) relatedDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
181 if (Tools.notBlank(csid)) {
182 collectionObjectCsids.add(csid);
185 if (collectionObjectCsids == null || collectionObjectCsids.isEmpty()) {
186 logger.warn("Could not obtain any CSIDs of related CollectionObject records.");
187 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
190 if (logger.isTraceEnabled()) {
191 logger.trace("Found " + collectionObjectCsids.size() + " CSIDs of related CollectionObject records.");
195 // Iterate through the list of CollectionObject CSIDs found.
196 DocumentModel collectionObjectDocModel = null;
197 String computedCurrentLocationRefName = "";
198 Map<DocumentModel, String> docModelsToUpdate = new HashMap<DocumentModel, String>();
199 for (String collectionObjectCsid : collectionObjectCsids) {
201 collectionObjectDocModel = getDocModelFromCsid(coreSession, collectionObjectCsid);
202 // Verify that the CollectionObject record is active.
203 if (!isActiveDocument(collectionObjectDocModel)) {
206 // Obtain the computed current location of that CollectionObject.
207 computedCurrentLocationRefName = computeCurrentLocation(coreSession, collectionObjectCsid);
208 if (logger.isTraceEnabled()) {
209 logger.trace("computedCurrentLocation refName=" + computedCurrentLocationRefName);
212 // Check that the value returned, which is expected to be a
213 // reference (refName) to a storage location authority term,
215 // * Non-null and non-blank. (We need to verify this assumption; can a
216 // CollectionObject's computed current location meaningfully be 'un-set'?)
217 // * Capable of being successfully parsed by an authority item parser;
218 // that is, returning a non-null parse result.
219 if ((Tools.notBlank(computedCurrentLocationRefName)
220 && (RefNameUtils.parseAuthorityTermInfo(computedCurrentLocationRefName) != null))) {
221 if (logger.isTraceEnabled()) {
222 logger.trace("refName passes basic validation tests.");
225 // If the value returned from the function passes validation,
226 // compare it to the value in the computedCurrentLocation
227 // field of that CollectionObject.
229 // If the CollectionObject does not already have a
230 // computedCurrentLocation value, or if the two values differ ...
231 String existingComputedCurrentLocationRefName =
232 (String) collectionObjectDocModel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY);
233 if (Tools.isBlank(existingComputedCurrentLocationRefName)
234 || !computedCurrentLocationRefName.equals(existingComputedCurrentLocationRefName)) {
235 if (logger.isTraceEnabled()) {
236 logger.trace("Existing computedCurrentLocation refName=" + existingComputedCurrentLocationRefName);
237 logger.trace("computedCurrentLocation refName requires updating.");
239 // ... set aside this CollectionObject's docModel and its new
240 // computed current location value for subsequent updating
241 docModelsToUpdate.put(collectionObjectDocModel, computedCurrentLocationRefName);
244 if (logger.isTraceEnabled()) {
245 logger.trace("computedCurrentLocation refName does NOT require updating.");
251 // For each CollectionObject docModel that has been set aside for updating,
252 // update the value of its computedCurrentLocation field with its new,
253 // computed current location.
254 int collectionObjectsUpdated = 0;
255 for (Map.Entry<DocumentModel, String> entry : docModelsToUpdate.entrySet()) {
256 DocumentModel dmodel = entry.getKey();
257 String newCurrentLocationValue = entry.getValue();
258 dmodel.setProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY, newCurrentLocationValue);
259 coreSession.saveDocument(dmodel);
260 collectionObjectsUpdated++;
261 if (logger.isTraceEnabled()) {
262 String afterUpdateComputedCurrentLocationRefName =
263 (String) dmodel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY);
264 logger.trace("Following update, new computedCurrentLocation refName value=" + afterUpdateComputedCurrentLocationRefName);
268 logger.info("Updated " + collectionObjectsUpdated + " CollectionObject record(s) with new computed current location(s).");
271 // FIXME: Generic methods like many of those below might be split off,
272 // into an event utilities class, base classes, or otherwise. - ADR 2012-12-05
274 // FIXME: Identify whether the equivalent of the documentMatchesType utility
275 // method is already implemented and substitute a call to the latter if so.
276 // This may well already exist.
278 * Identifies whether a document matches a supplied document type.
280 * @param docModel a document model.
281 * @param docType a document type string.
282 * @return true if the document matches the supplied document type; false if
285 private boolean documentMatchesType(DocumentModel docModel, String docType) {
286 if (docModel == null || Tools.isBlank(docType)) {
289 if (docModel.getType().startsWith(docType)) {
297 * Identifies whether a document is an active document; that is, if it is
298 * not a versioned record; not a proxy (symbolic link to an actual record);
299 * and not in the 'deleted' workflow state.
301 * (A note relating the latter: Nuxeo appears to send 'documentModified'
302 * events even on workflow transitions, such when records are 'soft deleted'
303 * by being transitioned to the 'deleted' workflow state.)
306 * @return true if the document is an active document; false if it is not.
308 private boolean isActiveDocument(DocumentModel docModel) {
309 if (docModel == null) {
312 boolean isActiveDocument = false;
314 if (!docModel.isVersion()
315 && !docModel.isProxy()
316 && !docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
317 isActiveDocument = true;
319 } catch (ClientException ce) {
320 logger.warn("Error while identifying whether document is an active document: ", ce);
322 return isActiveDocument;
326 * Returns a document model for a record identified by a CSID.
328 * @param session a repository session.
329 * @param collectionObjectCsid a CollectionObject identifier (CSID)
330 * @return a document model for the record identified by the supplied CSID.
332 private DocumentModel getDocModelFromCsid(CoreSession session, String collectionObjectCsid) {
333 DocumentModelList collectionObjectDocModels = null;
335 final String query = "SELECT * FROM "
336 + NuxeoUtils.BASE_DOCUMENT_TYPE
338 + NuxeoUtils.getByNameWhereClause(collectionObjectCsid);
339 collectionObjectDocModels = session.query(query);
340 } catch (Exception e) {
341 logger.warn("Exception in query to get document model for CollectionObject: ", e);
343 if (collectionObjectDocModels == null || collectionObjectDocModels.isEmpty()) {
344 logger.warn("Could not get document models for CollectionObject(s).");
345 } else if (collectionObjectDocModels.size() != 1) {
346 logger.debug("Found more than 1 document with CSID=" + collectionObjectCsid);
348 return collectionObjectDocModels.get(0);
351 // FIXME: A quick first pass, using an only partly query-based technique for
352 // getting the current location, augmented by procedural code.
354 // Should be replaced by a more performant method, based entirely, or nearly so,
357 // E.g. the following is a sample CMIS query for retrieving Movement records
358 // related to a CollectionObject, which might serve as the basis for that query.
360 "SELECT DOC.nuxeo:pathSegment, DOC.dc:title, REL.dc:title,"
361 + "REL.relations_common:objectCsid, REL.relations_common:subjectCsid FROM Movement DOC "
362 + "JOIN Relation REL ON REL.relations_common:objectCsid = DOC.nuxeo:pathSegment "
363 + "WHERE REL.relations_common:subjectCsid = '5b4c617e-53a0-484b-804e' "
364 + "AND DOC.nuxeo:isVersion = false "
365 + "ORDER BY DOC.collectionspace_core:updatedAt DESC";
368 * Returns the computed current location for a CollectionObject.
370 * @param session a repository session.
371 * @param collectionObjectCsid a CollectionObject identifier (CSID)
372 * @throws ClientException
373 * @return the computed current location for the CollectionObject identified
374 * by the supplied CSID.
376 private String computeCurrentLocation(CoreSession session, String collectionObjectCsid)
377 throws ClientException {
378 String computedCurrentLocation = "";
379 // Get Relation records for Movements related to this CollectionObject.
381 // Some values below are hard-coded for readability, rather than
382 // being obtained from constants.
383 String query = String.format(
384 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
386 + " (%2$s:subjectCsid = '%3$s' "
387 + " AND %2$s:objectDocumentType = '%4$s') "
389 + " (%2$s:objectCsid = '%3$s' "
390 + " AND %2$s:subjectDocumentType = '%4$s') "
392 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
393 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, collectionObjectCsid, MOVEMENT_DOCTYPE);
394 DocumentModelList relatedDocModels = session.query(query);
395 if (relatedDocModels == null || relatedDocModels.isEmpty()) {
396 return computedCurrentLocation;
399 // Iterate through related movement records, to get the CollectionObject's
400 // computed current location from the related Movement record with the
401 // most recent location date.
402 GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
403 DocumentModel movementDocModel = null;
405 String location = "";
406 for (DocumentModel relatedDocModel : relatedDocModels) {
407 // Due to the 'OR' operator in the query above, related Movement
408 // record CSIDs may reside in either the subject or object CSID fields
409 // of the relation record. Whichever CSID value doesn't match the
410 // CollectionObject's CSID is inferred to be the related Movement record's CSID.
411 csid = (String) relatedDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
412 if (csid.equals(collectionObjectCsid)) {
413 csid = (String) relatedDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
415 movementDocModel = getDocModelFromCsid(session, csid);
416 // Verify that the Movement record is active. This will also exclude
417 // versioned Movement records from the computation of the current
418 // location, for tenants that are versioning such records.
419 if (!isActiveDocument(movementDocModel)) {
422 GregorianCalendar locationDate =
423 (GregorianCalendar) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
424 if (locationDate == null) {
427 if (locationDate.after(mostRecentLocationDate)) {
428 mostRecentLocationDate = locationDate;
429 location = (String) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, CURRENT_LOCATION_PROPERTY);
431 if (Tools.notBlank(location)) {
432 computedCurrentLocation = location;
435 return computedCurrentLocation;