]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
6ca3d667252f74c663de1ab01854ae5f77988a30
[tmp/jakarta-migration.git] /
1 package org.collectionspace.services.listener;
2
3 import java.util.GregorianCalendar;
4 import java.util.HashSet;
5 import java.util.Set;
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;
20
21 public abstract class AbstractUpdateObjectLocationValues implements EventListener {
22
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     protected 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     protected 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";
47
48     public enum EventNotificationDocumentType {
49         // Document type about which we've received a notification
50
51         MOVEMENT, RELATION;
52     }
53
54     @Override
55     public void handleEvent(Event event) throws ClientException {
56
57         logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
58
59         EventContext eventContext = event.getContext();
60         if (eventContext == null) {
61             return;
62         }
63
64         if (!(eventContext instanceof DocumentEventContext)) {
65             return;
66         }
67         DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
68         DocumentModel docModel = docEventContext.getSourceDocument();
69
70         // If this document event involves a Relation record, does this pertain to
71         // a relationship between a Movement record and a CollectionObject record?
72         //
73         // If not, we're not interested in processing this document event
74         // in this event handler, as it will have no bearing on updating a
75         // computed current location for a CollectionObject.
76
77         //
78         // (The rest of the code flow below is then identical to that which
79         // is followed when this document event involves a Movement record.
80         String movementCsid = "";
81         Enum notificationDocumentType;
82         if (documentMatchesType(docModel, RELATION_DOCTYPE)) {
83             if (logger.isTraceEnabled()) {
84                 logger.trace("An event involving a Relation document was received by UpdateObjectLocationOnMove ...");
85             }
86             // Get a Movement CSID from the Relation record. (If we can't
87             // get it, then we don't have a pertinent relation record.)
88             movementCsid = getCsidForDesiredDocTypeFromRelation(docModel, MOVEMENT_DOCTYPE, COLLECTIONOBJECT_DOCTYPE);
89             if (Tools.isBlank(movementCsid)) {
90                 logger.warn("Could not obtain CSID for Movement record from document event.");
91                 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
92                 return;
93             }
94             notificationDocumentType = EventNotificationDocumentType.RELATION;
95         } else if (documentMatchesType(docModel, MOVEMENT_DOCTYPE)) {
96             // Otherwise, get a Movement CSID directly from the Movement record.
97             if (logger.isTraceEnabled()) {
98                 logger.trace("An event involving a Movement document was received by UpdateObjectLocationOnMove ...");
99             }
100             // FIXME: exclude creation events for Movement records here, if we can
101             // identify that we'l still be properly handling creation events
102             // that include a relations list as part of the creation payload,
103             // perhaps because that may trigger a separate event notification.
104             movementCsid = NuxeoUtils.getCsid(docModel);
105             if (Tools.isBlank(movementCsid)) {
106                 logger.warn("Could not obtain CSID for Movement record from document event.");
107                 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
108                 return;
109             }
110             notificationDocumentType = EventNotificationDocumentType.MOVEMENT;
111         } else {
112             if (logger.isTraceEnabled()) {
113                 logger.trace("This event did not involve a document relevant to UpdateObjectLocationOnMove ...");
114             }
115             return;
116         }
117
118         // Note: currently, all Document lifecycle transitions on
119         // the relevant doctype(s) are handled by this event handler,
120         // not just transitions between 'soft deleted' and active states.
121         //
122         // We are assuming that we'll want to re-compute current locations
123         // for related CollectionObjects on all such transitions, as the
124         // semantics of such transitions are opaque to this event handler,
125         // because arbitrary workflows can be bound to those doctype(s).
126         //
127         // If we need to filter out some of those lifecycle transitions,
128         // such as excluding transitions to the 'locked' workflow state; or,
129         // alternately, if we want to restrict this event handler's
130         // scope to handle only transitions into the 'soft deleted' state,
131         // we can add additional checks for doing so at this point in the code.
132
133         // For debugging
134         if (logger.isTraceEnabled()) {
135             logger.trace("Movement CSID=" + movementCsid);
136             logger.trace("Notification document type=" + notificationDocumentType.name());
137         }
138
139         CoreSession coreSession = docEventContext.getCoreSession();
140         Set<String> collectionObjectCsids = new HashSet<>();
141
142         if (notificationDocumentType == EventNotificationDocumentType.RELATION) {
143             String relatedCollectionObjectCsid =
144                     getCsidForDesiredDocTypeFromRelation(docModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
145             collectionObjectCsids.add(relatedCollectionObjectCsid);
146         } else if (notificationDocumentType == EventNotificationDocumentType.MOVEMENT) {
147             collectionObjectCsids.addAll(getCollectionObjectCsidsRelatedToMovement(movementCsid, coreSession));
148         }
149
150         if (collectionObjectCsids.isEmpty()) {
151             logger.warn("Could not obtain any CSIDs of related CollectionObject records.");
152             logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
153             return;
154         } else {
155             if (logger.isTraceEnabled()) {
156                 logger.trace("Found " + collectionObjectCsids.size() + " CSIDs of related CollectionObject records.");
157             }
158         }
159         // Iterate through the list of CollectionObject CSIDs found.
160         // For each CollectionObject, obtain its most recent, related Movement,
161         // and update relevant field(s) with values from that Movement record.
162         DocumentModel collectionObjectDocModel;
163         DocumentModel mostRecentMovementDocModel;
164         for (String collectionObjectCsid : collectionObjectCsids) {
165             if (logger.isTraceEnabled()) {
166                 logger.trace("CollectionObject CSID=" + collectionObjectCsid);
167             }
168             // Verify that the CollectionObject is retrievable.
169             collectionObjectDocModel = getDocModelFromCsid(coreSession, collectionObjectCsid);
170             if (collectionObjectDocModel == null) {
171                 continue;
172             }
173             // Verify that the CollectionObject record is active.
174             if (!isActiveDocument(collectionObjectDocModel)) {
175                 continue;
176             }
177             // Get the CollectionObject's most recent, related Movement.
178             mostRecentMovementDocModel = getMostRecentMovement(coreSession, collectionObjectCsid);
179             if (mostRecentMovementDocModel == null) {
180                 continue;
181             }
182             // Update the CollectionObject with values from that Movement.
183             collectionObjectDocModel =
184                     updateCollectionObjectValuesFromMovement(collectionObjectDocModel, mostRecentMovementDocModel);
185             coreSession.saveDocument(collectionObjectDocModel);
186         }
187     }
188
189     /**
190      * Returns the CSIDs of CollectionObject records that are related to a
191      * Movement record.
192      *
193      * @param movementCsid the CSID of a Movement record.
194      * @param coreSession a repository session.
195      * @throws ClientException
196      * @return the CSIDs of the CollectionObject records, if any, which are
197      * related to the Movement record.
198      */
199     private Set<String> getCollectionObjectCsidsRelatedToMovement(String movementCsid,
200             CoreSession coreSession) throws ClientException {
201
202         Set<String> csids = new HashSet<>();
203
204         // Via an NXQL query, get a list of active relation records where:
205         // * This movement record's CSID is the subject CSID of the relation,
206         //   and its object document type is a CollectionObject doctype;
207         // or
208         // * This movement record's CSID is the object CSID of the relation,
209         //   and its subject document type is a CollectionObject doctype.
210         //
211         // Some values below are hard-coded for readability, rather than
212         // being obtained from constants.
213         String query = String.format(
214                 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
215                 + "("
216                 + "  (%2$s:subjectCsid = '%3$s' "
217                 + "  AND %2$s:objectDocumentType = '%4$s') "
218                 + " OR "
219                 + "  (%2$s:objectCsid = '%3$s' "
220                 + "  AND %2$s:subjectDocumentType = '%4$s') "
221                 + ")"
222                 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
223                 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, movementCsid, COLLECTIONOBJECT_DOCTYPE);
224         DocumentModelList relationDocModels = coreSession.query(query);
225         if (relationDocModels == null || relationDocModels.isEmpty()) {
226             // Encountering a Movement record that is not related to any
227             // CollectionObject is potentially a normal occurrence, so no
228             // error messages are logged here when we stop handling this event.
229             return csids;
230         }
231         // Iterate through the list of Relation records found and build
232         // a list of CollectionObject CSIDs, by extracting the relevant CSIDs
233         // from those Relation records.
234         String csid;
235         for (DocumentModel relationDocModel : relationDocModels) {
236             csid = getCsidForDesiredDocTypeFromRelation(relationDocModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
237             if (Tools.notBlank(csid)) {
238                 csids.add(csid);
239             }
240         }
241         return csids;
242     }
243
244 // FIXME: Generic methods like many of those below might be split off from
245 // this specific event listener/handler, into an event handler utilities
246 // class, base classes, or otherwise.
247 //
248 // FIXME: Identify whether the equivalent of the documentMatchesType utility
249 // method is already implemented and substitute a call to the latter if so.
250 // This may well already exist.
251     /**
252      * Identifies whether a document matches a supplied document type.
253      *
254      * @param docModel a document model.
255      * @param docType a document type string.
256      * @return true if the document matches the supplied document type; false if
257      * it does not.
258      */
259     protected static boolean documentMatchesType(DocumentModel docModel, String docType) {
260         if (docModel == null || Tools.isBlank(docType)) {
261             return false;
262         }
263         if (docModel.getType().startsWith(docType)) {
264             return true;
265         } else {
266             return false;
267         }
268     }
269
270     /**
271      * Identifies whether a document is an active document; that is, if it is
272      * not a versioned record; not a proxy (symbolic link to an actual record);
273      * and not in the 'deleted' workflow state.
274      *
275      * (A note relating the latter: Nuxeo appears to send 'documentModified'
276      * events even on workflow transitions, such when records are 'soft deleted'
277      * by being transitioned to the 'deleted' workflow state.)
278      *
279      * @param docModel
280      * @return true if the document is an active document; false if it is not.
281      */
282     protected static boolean isActiveDocument(DocumentModel docModel) {
283         if (docModel == null) {
284             return false;
285         }
286         boolean isActiveDocument = false;
287         try {
288             if (!docModel.isVersion()
289                     && !docModel.isProxy()
290                     && !docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
291                 isActiveDocument = true;
292             }
293         } catch (ClientException ce) {
294             logger.warn("Error while identifying whether document is an active document: ", ce);
295         }
296         return isActiveDocument;
297     }
298
299     /**
300      * Returns a document model for a record identified by a CSID.
301      *
302      * @param session a repository session.
303      * @param collectionObjectCsid a CollectionObject identifier (CSID)
304      * @return a document model for the record identified by the supplied CSID.
305      */
306     protected static DocumentModel getDocModelFromCsid(CoreSession session, String collectionObjectCsid) {
307         DocumentModelList collectionObjectDocModels = null;
308         try {
309             final String query = "SELECT * FROM "
310                     + NuxeoUtils.BASE_DOCUMENT_TYPE
311                     + " WHERE "
312                     + NuxeoUtils.getByNameWhereClause(collectionObjectCsid);
313             collectionObjectDocModels = session.query(query);
314         } catch (Exception e) {
315             logger.warn("Exception in query to get document model for CollectionObject: ", e);
316         }
317         if (collectionObjectDocModels == null || collectionObjectDocModels.isEmpty()) {
318             logger.warn("Could not get document models for CollectionObject(s).");
319             return null;
320         } else if (collectionObjectDocModels.size() != 1) {
321             logger.debug("Found more than 1 document with CSID=" + collectionObjectCsid);
322             return null;
323         }
324         return collectionObjectDocModels.get(0);
325     }
326
327     // FIXME: A quick first pass, using an only partly query-based technique for
328     // getting the most recent Movement record related to a CollectionObject,
329     // augmented by procedural code.
330     //
331     // Could be replaced by a potentially more performant method, based on a query.
332     //
333     // E.g. the following is a sample CMIS query for retrieving Movement records
334     // related to a CollectionObject, which might serve as the basis for that query.
335     /*
336      "SELECT DOC.nuxeo:pathSegment, DOC.dc:title, REL.dc:title,"
337      + "REL.relations_common:objectCsid, REL.relations_common:subjectCsid FROM Movement DOC "
338      + "JOIN Relation REL ON REL.relations_common:objectCsid = DOC.nuxeo:pathSegment "
339      + "WHERE REL.relations_common:subjectCsid = '5b4c617e-53a0-484b-804e' "
340      + "AND DOC.nuxeo:isVersion = false "
341      + "ORDER BY DOC.collectionspace_core:updatedAt DESC";
342      */
343     /**
344      * Returns the most recent Movement record related to a CollectionObject.
345      *
346      * This method currently returns the related Movement record with the latest
347      * (i.e. most recent in time) Location Date field value.
348      *
349      * @param session a repository session.
350      * @param collectionObjectCsid a CollectionObject identifier (CSID)
351      * @throws ClientException
352      * @return the most recent Movement record related to the CollectionObject
353      * identified by the supplied CSID.
354      */
355     protected static DocumentModel getMostRecentMovement(CoreSession session, String collectionObjectCsid)
356             throws ClientException {
357         DocumentModel mostRecentMovementDocModel = null;
358         // Get Relation records for Movements related to this CollectionObject.
359         //
360         // Some values below are hard-coded for readability, rather than
361         // being obtained from constants.
362         String query = String.format(
363                 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
364                 + "("
365                 + "  (%2$s:subjectCsid = '%3$s' "
366                 + "  AND %2$s:objectDocumentType = '%4$s') "
367                 + " OR "
368                 + "  (%2$s:objectCsid = '%3$s' "
369                 + "  AND %2$s:subjectDocumentType = '%4$s') "
370                 + ")"
371                 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
372                 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, collectionObjectCsid, MOVEMENT_DOCTYPE);
373         if (logger.isTraceEnabled()) {
374             logger.trace("query=" + query);
375         }
376         DocumentModelList relationDocModels = session.query(query);
377         if (relationDocModels == null || relationDocModels.isEmpty()) {
378             logger.warn("Unexpectedly found no relations to Movement records to/from to this CollectionObject record.");
379             return mostRecentMovementDocModel;
380         } else {
381             if (logger.isTraceEnabled()) {
382                 logger.trace("Found " + relationDocModels.size() + " relations to Movement records to/from this CollectionObject record.");
383             }
384         }
385         // Iterate through related movement records, to find the related
386         // Movement record with the most recent location date.
387         GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
388         DocumentModel movementDocModel;
389         Set<String> alreadyProcessedMovementCsids = new HashSet<>();
390         String relatedMovementCsid;
391         for (DocumentModel relationDocModel : relationDocModels) {
392             // Due to the 'OR' operator in the query above, related Movement
393             // record CSIDs may reside in either the subject or object CSID fields
394             // of the relation record. Whichever CSID value doesn't match the
395             // CollectionObject's CSID is inferred to be the related Movement record's CSID.
396             relatedMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
397             if (relatedMovementCsid.equals(collectionObjectCsid)) {
398                 relatedMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
399             }
400             if (Tools.isBlank(relatedMovementCsid)) {
401                 continue;
402             }
403             // Because of the OR clause used in the query above, there may be
404             // two or more Relation records returned in the query results that
405             // reference the same Movement record, as either the subject
406             // or object of a relation to the same CollectionObject record;
407             // we need to filter out those duplicates.
408             if (alreadyProcessedMovementCsids.contains(relatedMovementCsid)) {
409                 continue;
410             } else {
411                 alreadyProcessedMovementCsids.add(relatedMovementCsid);
412             }
413             if (logger.isTraceEnabled()) {
414                 logger.trace("Related movement CSID=" + relatedMovementCsid);
415             }
416             movementDocModel = getDocModelFromCsid(session, relatedMovementCsid);
417             if (movementDocModel == null) {
418                 continue;
419             }
420
421             // Verify that the Movement record is active. This will also exclude
422             // versioned Movement records from the computation of the current
423             // location, for tenants that are versioning such records.
424             if (!isActiveDocument(movementDocModel)) {
425                 if (logger.isTraceEnabled()) {
426                     logger.trace("Skipping this inactive Movement record ...");
427                 }
428                 continue;
429             }
430             GregorianCalendar locationDate =
431                     (GregorianCalendar) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
432             if (locationDate == null) {
433                 continue;
434             }
435             if (locationDate.after(mostRecentLocationDate)) {
436                 mostRecentLocationDate = locationDate;
437                 mostRecentMovementDocModel = movementDocModel;
438             }
439         }
440         return mostRecentMovementDocModel;
441     }
442
443     /**
444      * Returns the CSID for a desired document type from a Relation record,
445      * where the relationship involves two specified, different document types.
446      *
447      * @param relationDocModel a document model for a Relation record.
448      * @param desiredDocType a desired document type.
449      * @param relatedDocType a related document type.
450      * @throws ClientException
451      * @return the CSID from the desired document type in the relation. Returns
452      * an empty string if the Relation record does not involve both the desired
453      * and related document types, or if the desired document type is at both
454      * ends of the relation.
455      */
456     protected static String getCsidForDesiredDocTypeFromRelation(DocumentModel relationDocModel,
457             String desiredDocType, String relatedDocType) throws ClientException {
458         String csid = "";
459         String subjectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
460         String objectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
461         if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(desiredDocType)) {
462             return csid;
463         }
464         if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(relatedDocType)) {
465             csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
466         } else if (subjectDocType.startsWith(relatedDocType) && objectDocType.startsWith(desiredDocType)) {
467             csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
468         }
469         return csid;
470     }
471
472     // The following method can be extended by sub-classes to update
473     // different/multiple values; e.g. values for moveable locations ("crates").
474     /**
475      * Updates a CollectionObject record with selected values from a Movement
476      * record.
477      *
478      * @param collectionObjectDocModel a document model for a CollectionObject
479      * record.
480      * @param movementDocModel a document model for a Movement record.
481      * @return a potentially updated document model for the CollectionObject
482      * record.
483      * @throws ClientException
484      */
485     protected abstract DocumentModel updateCollectionObjectValuesFromMovement(DocumentModel collectionObjectDocModel,
486             DocumentModel movementDocModel)
487             throws ClientException;
488 }