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