1 package org.collectionspace.services.listener;
3 import java.util.GregorianCalendar;
4 import java.util.HashSet;
6 import org.apache.commons.logging.Log;
7 import org.apache.commons.logging.LogFactory;
8 import org.collectionspace.services.client.workflow.WorkflowClient;
9 import org.collectionspace.services.common.api.Tools;
10 import org.collectionspace.services.movement.nuxeo.MovementConstants;
11 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
12 import org.nuxeo.ecm.core.api.ClientException;
13 import org.nuxeo.ecm.core.api.CoreSession;
14 import org.nuxeo.ecm.core.api.DocumentModel;
15 import org.nuxeo.ecm.core.api.DocumentModelList;
16 import org.nuxeo.ecm.core.event.Event;
17 import org.nuxeo.ecm.core.event.EventContext;
18 import org.nuxeo.ecm.core.event.EventListener;
19 import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
21 public abstract class AbstractUpdateObjectLocationValues implements EventListener {
23 // FIXME: We might experiment here with using log4j instead of Apache Commons Logging;
24 // am using the latter to follow Ray's pattern for now
25 private final static Log logger = LogFactory.getLog(AbstractUpdateObjectLocationValues.class);
26 // FIXME: Make the following message, or its equivalent, a constant usable by all event listeners
27 private final static String NO_FURTHER_PROCESSING_MESSAGE =
28 "This event listener will not continue processing this event ...";
29 private final static GregorianCalendar EARLIEST_COMPARISON_DATE = new GregorianCalendar(1600, 1, 1);
30 private final static String RELATIONS_COMMON_SCHEMA = "relations_common"; // FIXME: Get from external constant
31 private final static String RELATION_DOCTYPE = "Relation"; // FIXME: Get from external constant
32 private final static String SUBJECT_CSID_PROPERTY = "subjectCsid"; // FIXME: Get from external constant
33 private final static String OBJECT_CSID_PROPERTY = "objectCsid"; // FIXME: Get from external constant
34 private final static String SUBJECT_DOCTYPE_PROPERTY = "subjectDocumentType"; // FIXME: Get from external constant
35 private final static String OBJECT_DOCTYPE_PROPERTY = "objectDocumentType"; // FIXME: Get from external constant
36 protected final static String COLLECTIONOBJECTS_COMMON_SCHEMA = "collectionobjects_common"; // FIXME: Get from external constant
37 private final static String COLLECTIONOBJECT_DOCTYPE = "CollectionObject"; // FIXME: Get from external constant
38 protected final static String COMPUTED_CURRENT_LOCATION_PROPERTY = "computedCurrentLocation"; // FIXME: Create and then get from external constant
39 private final static String MOVEMENTS_COMMON_SCHEMA = "movements_common"; // FIXME: Get from external constant
40 private final static String MOVEMENT_DOCTYPE = MovementConstants.NUXEO_DOCTYPE;
41 private final static String LOCATION_DATE_PROPERTY = "locationDate"; // FIXME: Get from external constant
42 private final static String CURRENT_LOCATION_PROPERTY = "currentLocation"; // FIXME: Get from external constant
43 private final static String ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT =
44 "AND (ecm:currentLifeCycleState <> 'deleted') "
45 + "AND ecm:isProxy = 0 "
46 + "AND ecm:isCheckedInVersion = 0";
49 public void handleEvent(Event event) throws ClientException {
51 logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
53 EventContext eventContext = event.getContext();
54 if (eventContext == null) {
58 if (!(eventContext instanceof DocumentEventContext)) {
61 DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
62 DocumentModel docModel = docEventContext.getSourceDocument();
64 // If this document event involves a Relation record, does this pertain to
65 // a relationship between a Movement record and a CollectionObject record?
67 // If not, we're not interested in processing this document event
68 // in this event handler, as it will have no bearing on updating a
69 // computed current location for a CollectionObject.
72 // (The rest of the code flow below is then identical to that which
73 // is followed when this document event involves a Movement record.)
74 String movementCsid = "";
75 if (documentMatchesType(docModel, RELATION_DOCTYPE)) {
76 if (logger.isTraceEnabled()) {
77 logger.trace("An event involving a Relation document was received by UpdateObjectLocationOnMove ...");
79 // Get a Movement CSID from the Relation record. (If we can't
80 // get it, then we don't have a pertinent relation record.)
81 movementCsid = getCsidForDesiredDocType(docModel, MOVEMENT_DOCTYPE, COLLECTIONOBJECT_DOCTYPE);
82 if (Tools.isBlank(movementCsid)) {
83 logger.warn("Could not obtain CSID for Movement record from document event.");
84 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
87 } else if (documentMatchesType(docModel, MOVEMENT_DOCTYPE)) {
88 if (logger.isTraceEnabled()) {
89 logger.trace("An event involving a Movement document was received by UpdateObjectLocationOnMove ...");
91 // Otherwise, get a Movement CSID directly from the Movement record.
92 movementCsid = NuxeoUtils.getCsid(docModel);
93 if (Tools.isBlank(movementCsid)) {
94 logger.warn("Could not obtain CSID for Movement record from document event.");
95 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
99 if (logger.isTraceEnabled()) {
100 logger.trace("This event did not involve a document relevant to UpdateObjectLocationOnMove ...");
105 // Note: currently, all Document lifecycle transitions on
106 // the relevant doctype(s) are handled by this event handler,
107 // not just transitions between 'soft deleted' and active states.
109 // We are assuming that we'll want to re-compute current locations
110 // for related CollectionObjects on all such transitions, as the
111 // semantics of such transitions are opaque to this event handler,
112 // because arbitrary workflows can be bound to those doctype(s).
114 // If we need to filter out some of those lifecycle transitions,
115 // such as excluding transitions to the 'locked' workflow state; or,
116 // alternately, if we want to restrict this event handler's
117 // scope to handle only transitions into the 'soft deleted' state,
118 // we can add additional checks for doing so at this point in the code.
120 if (logger.isTraceEnabled()) {
121 logger.trace("Movement CSID=" + movementCsid);
124 // Find CollectionObject records that are related to this Movement record:
126 // Via an NXQL query, get a list of active relation records where:
127 // * This movement record's CSID is the subject CSID of the relation,
128 // and its object document type is a CollectionObject doctype;
130 // * This movement record's CSID is the object CSID of the relation,
131 // and its subject document type is a CollectionObject doctype.
132 CoreSession coreSession = docEventContext.getCoreSession();
133 // Some values below are hard-coded for readability, rather than
134 // being obtained from constants.
135 String query = String.format(
136 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
138 + " (%2$s:subjectCsid = '%3$s' "
139 + " AND %2$s:objectDocumentType = '%4$s') "
141 + " (%2$s:objectCsid = '%3$s' "
142 + " AND %2$s:subjectDocumentType = '%4$s') "
144 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
145 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, movementCsid, COLLECTIONOBJECT_DOCTYPE);
146 DocumentModelList relationDocModels = coreSession.query(query);
147 if (relationDocModels == null || relationDocModels.isEmpty()) {
148 // Encountering a Movement record that is not related to any
149 // CollectionObject is potentially a normal occurrence, so no
150 // error messages are logged here when we stop handling this event.
154 // Iterate through the list of Relation records found and build
155 // a list of CollectionObject CSIDs, by extracting the relevant CSIDs
156 // from those Relation records.
158 // FIXME: The following code might be refactored into a generic 'get
159 // values of a single property from a list of document models' method,
160 // if this doesn't already exist.
162 Set<String> collectionObjectCsids = new HashSet<String>(); // Prevents/removes duplicates on add
163 for (DocumentModel relationDocModel : relationDocModels) {
164 csid = getCsidForDesiredDocType(relationDocModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
165 if (Tools.notBlank(csid)) {
166 collectionObjectCsids.add(csid);
169 if (collectionObjectCsids == null || collectionObjectCsids.isEmpty()) {
170 logger.warn("Could not obtain any CSIDs of related CollectionObject records.");
171 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
174 if (logger.isTraceEnabled()) {
175 logger.trace("Found " + collectionObjectCsids.size() + " CSIDs of related CollectionObject records.");
179 // Iterate through the list of CollectionObject CSIDs found
180 // and update their location value(s).
181 for (String collectionObjectCsid : collectionObjectCsids) {
182 if (logger.isTraceEnabled()) {
183 logger.trace("CollectionObject CSID=" + collectionObjectCsid);
185 updateAllLocationValues(coreSession, collectionObjectCsid);
189 // FIXME: Generic methods like many of those below might be split off,
190 // into an event utilities class, base classes, or otherwise. - ADR 2012-12-05
192 // FIXME: Identify whether the equivalent of the documentMatchesType utility
193 // method is already implemented and substitute a call to the latter if so.
194 // This may well already exist.
196 * Identifies whether a document matches a supplied document type.
198 * @param docModel a document model.
199 * @param docType a document type string.
200 * @return true if the document matches the supplied document type; false if
203 protected static boolean documentMatchesType(DocumentModel docModel, String docType) {
204 if (docModel == null || Tools.isBlank(docType)) {
207 if (docModel.getType().startsWith(docType)) {
215 * Identifies whether a document is an active document; that is, if it is
216 * not a versioned record; not a proxy (symbolic link to an actual record);
217 * and not in the 'deleted' workflow state.
219 * (A note relating the latter: Nuxeo appears to send 'documentModified'
220 * events even on workflow transitions, such when records are 'soft deleted'
221 * by being transitioned to the 'deleted' workflow state.)
224 * @return true if the document is an active document; false if it is not.
226 protected static boolean isActiveDocument(DocumentModel docModel) {
227 if (docModel == null) {
230 boolean isActiveDocument = false;
232 if (!docModel.isVersion()
233 && !docModel.isProxy()
234 && !docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
235 isActiveDocument = true;
237 } catch (ClientException ce) {
238 logger.warn("Error while identifying whether document is an active document: ", ce);
240 return isActiveDocument;
244 * Returns a document model for a record identified by a CSID.
246 * @param session a repository session.
247 * @param collectionObjectCsid a CollectionObject identifier (CSID)
248 * @return a document model for the record identified by the supplied CSID.
250 protected static DocumentModel getDocModelFromCsid(CoreSession session, String collectionObjectCsid) {
251 DocumentModelList collectionObjectDocModels = null;
253 final String query = "SELECT * FROM "
254 + NuxeoUtils.BASE_DOCUMENT_TYPE
256 + NuxeoUtils.getByNameWhereClause(collectionObjectCsid);
257 collectionObjectDocModels = session.query(query);
258 } catch (Exception e) {
259 logger.warn("Exception in query to get document model for CollectionObject: ", e);
261 if (collectionObjectDocModels == null || collectionObjectDocModels.isEmpty()) {
262 logger.warn("Could not get document models for CollectionObject(s).");
264 } else if (collectionObjectDocModels.size() != 1) {
265 logger.debug("Found more than 1 document with CSID=" + collectionObjectCsid);
268 return collectionObjectDocModels.get(0);
271 // FIXME: A quick first pass, using an only partly query-based technique for
272 // getting the current location, augmented by procedural code.
274 // Should be replaced by a more performant method, based entirely, or nearly so,
277 // E.g. the following is a sample CMIS query for retrieving Movement records
278 // related to a CollectionObject, which might serve as the basis for that query.
280 "SELECT DOC.nuxeo:pathSegment, DOC.dc:title, REL.dc:title,"
281 + "REL.relations_common:objectCsid, REL.relations_common:subjectCsid FROM Movement DOC "
282 + "JOIN Relation REL ON REL.relations_common:objectCsid = DOC.nuxeo:pathSegment "
283 + "WHERE REL.relations_common:subjectCsid = '5b4c617e-53a0-484b-804e' "
284 + "AND DOC.nuxeo:isVersion = false "
285 + "ORDER BY DOC.collectionspace_core:updatedAt DESC";
288 * Returns the computed current location for a CollectionObject.
290 * @param session a repository session.
291 * @param collectionObjectCsid a CollectionObject identifier (CSID)
292 * @throws ClientException
293 * @return the computed current location for the CollectionObject identified
294 * by the supplied CSID.
296 protected static String computeCurrentLocation(CoreSession session, String collectionObjectCsid)
297 throws ClientException {
298 String computedCurrentLocation = "";
299 // Get Relation records for Movements related to this CollectionObject.
301 // Some values below are hard-coded for readability, rather than
302 // being obtained from constants.
303 String query = String.format(
304 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
306 + " (%2$s:subjectCsid = '%3$s' "
307 + " AND %2$s:objectDocumentType = '%4$s') "
309 + " (%2$s:objectCsid = '%3$s' "
310 + " AND %2$s:subjectDocumentType = '%4$s') "
312 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
313 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, collectionObjectCsid, MOVEMENT_DOCTYPE);
314 if (logger.isTraceEnabled()) {
315 logger.trace("query=" + query);
317 DocumentModelList relationDocModels = session.query(query);
318 if (relationDocModels == null || relationDocModels.isEmpty()) {
319 logger.warn("Unexpectedly found no relations to Movement records to/from to this CollectionObject record.");
320 return computedCurrentLocation;
322 if (logger.isTraceEnabled()) {
323 logger.trace("Found " + relationDocModels.size() + " relations to Movement records to/from this CollectionObject record.");
326 // Iterate through related movement records, to get the CollectionObject's
327 // computed current location from the related Movement record with the
328 // most recent location date.
329 GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
330 DocumentModel movementDocModel = null;
331 Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
332 String relMovementCsid = "";
333 String location = "";
334 for (DocumentModel relationDocModel : relationDocModels) {
335 // Due to the 'OR' operator in the query above, related Movement
336 // record CSIDs may reside in either the subject or object CSID fields
337 // of the relation record. Whichever CSID value doesn't match the
338 // CollectionObject's CSID is inferred to be the related Movement record's CSID.
339 relMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
340 if (relMovementCsid.equals(collectionObjectCsid)) {
341 relMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
343 if (Tools.isBlank(relMovementCsid)) {
346 // Because of the OR clause used in the query above, there may be
347 // two or more Relation records returned in the query results that
348 // reference the same Movement record, as either the subject
349 // or object of a relation to the same CollectionObject record;
350 // we need to filter out those duplicates.
351 if (alreadyProcessedMovementCsids.contains(relMovementCsid)) {
354 alreadyProcessedMovementCsids.add(relMovementCsid);
356 if (logger.isTraceEnabled()) {
357 logger.trace("Movement CSID=" + relMovementCsid);
359 movementDocModel = getDocModelFromCsid(session, relMovementCsid);
360 if (movementDocModel == null) {
364 // Verify that the Movement record is active. This will also exclude
365 // versioned Movement records from the computation of the current
366 // location, for tenants that are versioning such records.
367 if (!isActiveDocument(movementDocModel)) {
368 if (logger.isTraceEnabled()) {
369 logger.trace("Skipping this inactive Movement record ...");
373 GregorianCalendar locationDate =
374 (GregorianCalendar) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
375 if (locationDate == null) {
378 if (locationDate.after(mostRecentLocationDate)) {
379 mostRecentLocationDate = locationDate;
380 location = (String) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, CURRENT_LOCATION_PROPERTY);
381 if (Tools.notBlank(location)) {
382 computedCurrentLocation = location;
386 return computedCurrentLocation;
390 * Returns the CSID for a desired document type from a Relation record,
391 * where the relationship involves two specified, different document types.
393 * @param relationDocModel a document model for a Relation record.
394 * @param desiredDocType a desired document type.
395 * @param relatedDocType a related document type.
396 * @throws ClientException
397 * @return the CSID from the desired document type in the relation. Returns
398 * an empty string if the Relation record does not involve both the desired
399 * and related document types, or if the desired document type is at both
400 * ends of the relation.
402 protected static String getCsidForDesiredDocType(DocumentModel relationDocModel,
403 String desiredDocType, String relatedDocType) throws ClientException {
405 String subjectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
406 String objectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
407 if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(desiredDocType)) {
410 if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(relatedDocType)) {
411 csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
412 } else if (subjectDocType.startsWith(relatedDocType) && objectDocType.startsWith(desiredDocType)) {
413 csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
418 // Can be extended by sub-classes to update different/multiple values;
419 // e.g. values for moveable locations ("crates").
420 protected abstract void updateAllLocationValues(CoreSession coreSession, String collectionObjectCsid)
421 throws ClientException;