]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
a046d53db85ef816abf766a7c428449c40161fda
[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     @Override
49     public void handleEvent(Event event) throws ClientException {
50
51         logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
52
53         EventContext eventContext = event.getContext();
54         if (eventContext == null) {
55             return;
56         }
57
58         if (!(eventContext instanceof DocumentEventContext)) {
59             return;
60         }
61         DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
62         DocumentModel docModel = docEventContext.getSourceDocument();
63
64         // If this document event involves a Relation record, does this pertain to
65         // a relationship between a Movement record and a CollectionObject record?
66         //
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.
70
71         //
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 ...");
78             }
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);
85                 return;
86             }
87         } else if (documentMatchesType(docModel, MOVEMENT_DOCTYPE)) {
88             if (logger.isTraceEnabled()) {
89                 logger.trace("An event involving a Movement document was received by UpdateObjectLocationOnMove ...");
90             }
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);
96                 return;
97             }
98         } else {
99             if (logger.isTraceEnabled()) {
100                 logger.trace("This event did not involve a document relevant to UpdateObjectLocationOnMove ...");
101             }
102             return;
103         }
104
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.
108         //
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).
113         //
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.
119
120         if (logger.isTraceEnabled()) {
121             logger.trace("Movement CSID=" + movementCsid);
122         }
123
124         // Find CollectionObject records that are related to this Movement record:
125         //
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;
129         // or
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 "
137                 + "("
138                 + "  (%2$s:subjectCsid = '%3$s' "
139                 + "  AND %2$s:objectDocumentType = '%4$s') "
140                 + " OR "
141                 + "  (%2$s:objectCsid = '%3$s' "
142                 + "  AND %2$s:subjectDocumentType = '%4$s') "
143                 + ")"
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.
151             return;
152         }
153
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.
157         String csid;
158         Set<String> collectionObjectCsids = new HashSet<String>();
159         for (DocumentModel relationDocModel : relationDocModels) {
160             csid = getCsidForDesiredDocType(relationDocModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
161             if (Tools.notBlank(csid)) {
162                 collectionObjectCsids.add(csid);
163             }
164         }
165         if (collectionObjectCsids == null || collectionObjectCsids.isEmpty()) {
166             logger.warn("Could not obtain any CSIDs of related CollectionObject records.");
167             logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
168             return;
169         } else {
170             if (logger.isTraceEnabled()) {
171                 logger.trace("Found " + collectionObjectCsids.size() + " CSIDs of related CollectionObject records.");
172             }
173         }
174
175         // Iterate through the list of CollectionObject CSIDs found.
176         // For each CollectionObject, obtain its most recent, related Movement,
177         // and update relevant field(s) with values from that Movement record.
178         DocumentModel collectionObjectDocModel;
179         DocumentModel mostRecentMovementDocModel;
180         for (String collectionObjectCsid : collectionObjectCsids) {
181             if (logger.isTraceEnabled()) {
182                 logger.trace("CollectionObject CSID=" + collectionObjectCsid);
183             }
184             // Verify that the CollectionObject is retrievable.
185             collectionObjectDocModel = getDocModelFromCsid(coreSession, collectionObjectCsid);
186             if (collectionObjectDocModel == null) {
187                 continue;
188             }
189             // Verify that the CollectionObject record is active.
190             if (!isActiveDocument(collectionObjectDocModel)) {
191                 continue;
192             }
193             // Get the CollectionObject's most recent, related Movement.
194             mostRecentMovementDocModel = getMostRecentMovement(coreSession, collectionObjectCsid);
195             if (mostRecentMovementDocModel == null) {
196                 continue;
197             }
198             // Update the CollectionObject with values from that Movement.
199             collectionObjectDocModel =
200                     updateCollectionObjectValuesFromMovement(collectionObjectDocModel, mostRecentMovementDocModel);
201             coreSession.saveDocument(collectionObjectDocModel);
202         }
203
204     }
205
206     // FIXME: Generic methods like many of those below might be split off from
207     // this specific event listener/handler, into an event handler utilities
208     // class, base classes, or otherwise.
209     //
210     // FIXME: Identify whether the equivalent of the documentMatchesType utility
211     // method is already implemented and substitute a call to the latter if so.
212     // This may well already exist.
213     /**
214      * Identifies whether a document matches a supplied document type.
215      *
216      * @param docModel a document model.
217      * @param docType a document type string.
218      * @return true if the document matches the supplied document type; false if
219      * it does not.
220      */
221     protected static boolean documentMatchesType(DocumentModel docModel, String docType) {
222         if (docModel == null || Tools.isBlank(docType)) {
223             return false;
224         }
225         if (docModel.getType().startsWith(docType)) {
226             return true;
227         } else {
228             return false;
229         }
230     }
231
232     /**
233      * Identifies whether a document is an active document; that is, if it is
234      * not a versioned record; not a proxy (symbolic link to an actual record);
235      * and not in the 'deleted' workflow state.
236      *
237      * (A note relating the latter: Nuxeo appears to send 'documentModified'
238      * events even on workflow transitions, such when records are 'soft deleted'
239      * by being transitioned to the 'deleted' workflow state.)
240      *
241      * @param docModel
242      * @return true if the document is an active document; false if it is not.
243      */
244     protected static boolean isActiveDocument(DocumentModel docModel) {
245         if (docModel == null) {
246             return false;
247         }
248         boolean isActiveDocument = false;
249         try {
250             if (!docModel.isVersion()
251                     && !docModel.isProxy()
252                     && !docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
253                 isActiveDocument = true;
254             }
255         } catch (ClientException ce) {
256             logger.warn("Error while identifying whether document is an active document: ", ce);
257         }
258         return isActiveDocument;
259     }
260
261     /**
262      * Returns a document model for a record identified by a CSID.
263      *
264      * @param session a repository session.
265      * @param collectionObjectCsid a CollectionObject identifier (CSID)
266      * @return a document model for the record identified by the supplied CSID.
267      */
268     protected static DocumentModel getDocModelFromCsid(CoreSession session, String collectionObjectCsid) {
269         DocumentModelList collectionObjectDocModels = null;
270         try {
271             final String query = "SELECT * FROM "
272                     + NuxeoUtils.BASE_DOCUMENT_TYPE
273                     + " WHERE "
274                     + NuxeoUtils.getByNameWhereClause(collectionObjectCsid);
275             collectionObjectDocModels = session.query(query);
276         } catch (Exception e) {
277             logger.warn("Exception in query to get document model for CollectionObject: ", e);
278         }
279         if (collectionObjectDocModels == null || collectionObjectDocModels.isEmpty()) {
280             logger.warn("Could not get document models for CollectionObject(s).");
281             return null;
282         } else if (collectionObjectDocModels.size() != 1) {
283             logger.debug("Found more than 1 document with CSID=" + collectionObjectCsid);
284             return null;
285         }
286         return collectionObjectDocModels.get(0);
287     }
288
289     // FIXME: A quick first pass, using an only partly query-based technique for
290     // getting the most recent Movement record related to a CollectionObject,
291     // augmented by procedural code.
292     //
293     // Should be replaced by a more performant method, based entirely, or nearly so,
294     // on a query.
295     //
296     // E.g. the following is a sample CMIS query for retrieving Movement records
297     // related to a CollectionObject, which might serve as the basis for that query.
298     /*
299      "SELECT DOC.nuxeo:pathSegment, DOC.dc:title, REL.dc:title,"
300      + "REL.relations_common:objectCsid, REL.relations_common:subjectCsid FROM Movement DOC "
301      + "JOIN Relation REL ON REL.relations_common:objectCsid = DOC.nuxeo:pathSegment "
302      + "WHERE REL.relations_common:subjectCsid = '5b4c617e-53a0-484b-804e' "
303      + "AND DOC.nuxeo:isVersion = false "
304      + "ORDER BY DOC.collectionspace_core:updatedAt DESC";
305      */
306     /**
307      * Returns the most recent Movement record related to a CollectionObject.
308      *
309      * This method currently returns the related Movement record with the latest
310      * (i.e. most recent in time) Location Date field value.
311      *
312      * @param session a repository session.
313      * @param collectionObjectCsid a CollectionObject identifier (CSID)
314      * @throws ClientException
315      * @return the most recent Movement record related to the CollectionObject
316      * identified by the supplied CSID.
317      */
318     protected static DocumentModel getMostRecentMovement(CoreSession session, String collectionObjectCsid)
319             throws ClientException {
320         DocumentModel mostRecentMovementDocModel = null;
321         // Get Relation records for Movements related to this CollectionObject.
322         //
323         // Some values below are hard-coded for readability, rather than
324         // being obtained from constants.
325         String query = String.format(
326                 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
327                 + "("
328                 + "  (%2$s:subjectCsid = '%3$s' "
329                 + "  AND %2$s:objectDocumentType = '%4$s') "
330                 + " OR "
331                 + "  (%2$s:objectCsid = '%3$s' "
332                 + "  AND %2$s:subjectDocumentType = '%4$s') "
333                 + ")"
334                 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
335                 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, collectionObjectCsid, MOVEMENT_DOCTYPE);
336         if (logger.isTraceEnabled()) {
337             logger.trace("query=" + query);
338         }
339         DocumentModelList relationDocModels = session.query(query);
340         if (relationDocModels == null || relationDocModels.isEmpty()) {
341             logger.warn("Unexpectedly found no relations to Movement records to/from to this CollectionObject record.");
342             return mostRecentMovementDocModel;
343         } else {
344             if (logger.isTraceEnabled()) {
345                 logger.trace("Found " + relationDocModels.size() + " relations to Movement records to/from this CollectionObject record.");
346             }
347         }
348         // Iterate through related movement records, to find the related
349         // Movement record with the most recent location date.
350         GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
351         DocumentModel movementDocModel = null;
352         Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
353         String relatedMovementCsid = "";
354         for (DocumentModel relationDocModel : relationDocModels) {
355             // Due to the 'OR' operator in the query above, related Movement
356             // record CSIDs may reside in either the subject or object CSID fields
357             // of the relation record. Whichever CSID value doesn't match the
358             // CollectionObject's CSID is inferred to be the related Movement record's CSID.
359             relatedMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
360             if (relatedMovementCsid.equals(collectionObjectCsid)) {
361                 relatedMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
362             }
363             if (Tools.isBlank(relatedMovementCsid)) {
364                 continue;
365             }
366             // Because of the OR clause used in the query above, there may be
367             // two or more Relation records returned in the query results that
368             // reference the same Movement record, as either the subject
369             // or object of a relation to the same CollectionObject record;
370             // we need to filter out those duplicates.
371             if (alreadyProcessedMovementCsids.contains(relatedMovementCsid)) {
372                 continue;
373             } else {
374                 alreadyProcessedMovementCsids.add(relatedMovementCsid);
375             }
376             if (logger.isTraceEnabled()) {
377                 logger.trace("Movement CSID=" + relatedMovementCsid);
378             }
379             movementDocModel = getDocModelFromCsid(session, relatedMovementCsid);
380             if (movementDocModel == null) {
381                 continue;
382             }
383
384             // Verify that the Movement record is active. This will also exclude
385             // versioned Movement records from the computation of the current
386             // location, for tenants that are versioning such records.
387             if (!isActiveDocument(movementDocModel)) {
388                 if (logger.isTraceEnabled()) {
389                     logger.trace("Skipping this inactive Movement record ...");
390                 }
391                 continue;
392             }
393             GregorianCalendar locationDate =
394                     (GregorianCalendar) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
395             if (locationDate == null) {
396                 continue;
397             }
398             if (locationDate.after(mostRecentLocationDate)) {
399                 mostRecentLocationDate = locationDate;
400                 mostRecentMovementDocModel = movementDocModel;
401             }
402         }
403         return mostRecentMovementDocModel;
404     }
405
406     /**
407      * Returns the CSID for a desired document type from a Relation record,
408      * where the relationship involves two specified, different document types.
409      *
410      * @param relationDocModel a document model for a Relation record.
411      * @param desiredDocType a desired document type.
412      * @param relatedDocType a related document type.
413      * @throws ClientException
414      * @return the CSID from the desired document type in the relation. Returns
415      * an empty string if the Relation record does not involve both the desired
416      * and related document types, or if the desired document type is at both
417      * ends of the relation.
418      */
419     protected static String getCsidForDesiredDocType(DocumentModel relationDocModel,
420             String desiredDocType, String relatedDocType) throws ClientException {
421         String csid = "";
422         String subjectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
423         String objectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
424         if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(desiredDocType)) {
425             return csid;
426         }
427         if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(relatedDocType)) {
428             csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
429         } else if (subjectDocType.startsWith(relatedDocType) && objectDocType.startsWith(desiredDocType)) {
430             csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
431         }
432         return csid;
433     }
434
435     // Can be extended by sub-classes to update different/multiple values;
436     // e.g. values for moveable locations ("crates").
437     /**
438      * Updates a CollectionObject record with selected values from a Movement
439      * record.
440      *
441      * @param collectionObjectDocModel a document model for a CollectionObject
442      * record.
443      * @param movementDocModel a document model for a Movement record.
444      * @throws ClientException
445      */
446     protected abstract DocumentModel updateCollectionObjectValuesFromMovement(DocumentModel collectionObjectDocModel,
447             DocumentModel movementDocModel)
448             throws ClientException;
449 }