]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
6b8ca9a789a307bcbbd9f3c87838a2a879b48a13
[tmp/jakarta-migration.git] /
1 package org.collectionspace.services.listener;
2
3 import java.util.ArrayList;
4 import java.util.GregorianCalendar;
5 import java.util.HashMap;
6 import java.util.HashSet;
7 import java.util.List;
8 import java.util.Map;
9 import java.util.Set;
10 import org.apache.commons.logging.Log;
11 import org.apache.commons.logging.LogFactory;
12 import org.collectionspace.services.client.workflow.WorkflowClient;
13 import org.collectionspace.services.common.api.RefNameUtils;
14 import org.collectionspace.services.common.api.Tools;
15 import org.collectionspace.services.movement.nuxeo.MovementConstants;
16 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
17 import org.nuxeo.ecm.core.api.ClientException;
18 import org.nuxeo.ecm.core.api.CoreSession;
19 import org.nuxeo.ecm.core.api.DocumentModel;
20 import org.nuxeo.ecm.core.api.DocumentModelList;
21 import org.nuxeo.ecm.core.event.Event;
22 import org.nuxeo.ecm.core.event.EventContext;
23 import org.nuxeo.ecm.core.event.EventListener;
24 import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
25
26 public class UpdateObjectLocationOnMove implements EventListener {
27
28     // FIXME: We might experiment here with using log4j instead of Apache Commons Logging;
29     // am using the latter to follow Ray's pattern for now
30     private final Log logger = LogFactory.getLog(UpdateObjectLocationOnMove.class);
31     // FIXME: Make the following message, or its equivalent, a constant usable by all event listeners
32     private final String NO_FURTHER_PROCESSING_MESSAGE =
33             "This event listener will not continue processing this event ...";
34     private final List<String> relevantDocTypesList = new ArrayList<String>();
35     GregorianCalendar EARLIEST_COMPARISON_DATE = new GregorianCalendar(1600, 1, 1);
36     private final static String RELATIONS_COMMON_SCHEMA = "relations_common"; // FIXME: Get from external constant
37     private final static String RELATION_DOCTYPE = "Relation"; // FIXME: Get from external constant
38     private final static String SUBJECT_CSID_PROPERTY = "subjectCsid"; // FIXME: Get from external constant
39     private final static String OBJECT_CSID_PROPERTY = "objectCsid"; // FIXME: Get from external constant
40     private final static String SUBJECT_DOCTYPE_PROPERTY = "subjectDocumentType"; // FIXME: Get from external constant
41     private final static String OBJECT_DOCTYPE_PROPERTY = "objectDocumentType"; // FIXME: Get from external constant
42     private final static String COLLECTIONOBJECTS_COMMON_SCHEMA = "collectionobjects_common"; // FIXME: Get from external constant
43     private final static String COLLECTIONOBJECT_DOCTYPE = "CollectionObject"; // FIXME: Get from external constant
44     private final static String COMPUTED_CURRENT_LOCATION_PROPERTY = "computedCurrentLocation"; // FIXME: Create and then get from external constant
45     private final static String MOVEMENTS_COMMON_SCHEMA = "movements_common"; // FIXME: Get from external constant
46     private final static String MOVEMENT_DOCTYPE = MovementConstants.NUXEO_DOCTYPE;
47     private final static String LOCATION_DATE_PROPERTY = "locationDate"; // FIXME: Get from external constant
48     private final static String CURRENT_LOCATION_PROPERTY = "currentLocation"; // FIXME: Get from external constant
49     private final static String ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT =
50             "AND (ecm:currentLifeCycleState <> 'deleted') "
51             + "AND ecm:isProxy = 0 "
52             + "AND ecm:isCheckedInVersion = 0";
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         if (documentMatchesType(docModel, RELATION_DOCTYPE)) {
82             if (logger.isTraceEnabled()) {
83                 logger.trace("An event involving a Relation document was received by UpdateObjectLocationOnMove ...");
84             }
85             // Get a Movement CSID from the Relation record. (If we can't
86             // get it, then we don't have a pertinent relation record.)
87             movementCsid = getCsidForDesiredDocType(docModel, MOVEMENT_DOCTYPE, COLLECTIONOBJECT_DOCTYPE);
88             if (Tools.isBlank(movementCsid)) {
89                 logger.warn("Could not obtain CSID for Movement record from document event.");
90                 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
91                 return;
92             }
93         } else if (documentMatchesType(docModel, MOVEMENT_DOCTYPE)) {
94             if (logger.isTraceEnabled()) {
95                 logger.trace("An event involving a Movement document was received by UpdateObjectLocationOnMove ...");
96             }
97             // Otherwise, get a Movement CSID directly from the Movement record.
98             movementCsid = NuxeoUtils.getCsid(docModel);
99             if (Tools.isBlank(movementCsid)) {
100                 logger.warn("Could not obtain CSID for Movement record from document event.");
101                 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
102                 return;
103             }
104         } else {
105             if (logger.isTraceEnabled()) {
106                 logger.trace("This event did not involve a document relevant to UpdateObjectLocationOnMove ...");
107             }
108             return;
109         }
110
111         // Note: currently, all Document lifecycle transitions on
112         // the relevant doctype(s) are handled by this event handler,
113         // not just transitions between 'soft deleted' and active states.
114         //
115         // We are assuming that we'll want to re-compute current locations
116         // for related CollectionObjects on all such transitions, as the
117         // semantics of such transitions are opaque to this event handler,
118         // because arbitrary workflows can be bound to those doctype(s).
119         //
120         // If we need to filter out some of those lifecycle transitions,
121         // such as excluding transitions to the 'locked' workflow state; or,
122         // alternately, if we want to restrict this event handler's
123         // scope to handle only transitions into the 'soft deleted' state,
124         // we can add additional checks for doing so at this point in the code.
125
126         if (logger.isTraceEnabled()) {
127             logger.trace("Movement CSID=" + movementCsid);
128         }
129
130         // Find CollectionObject records that are related to this Movement record:
131         //
132         // Via an NXQL query, get a list of active relation records where:
133         // * This movement record's CSID is the subject CSID of the relation,
134         //   and its object document type is a CollectionObject doctype;
135         // or
136         // * This movement record's CSID is the object CSID of the relation,
137         //   and its subject document type is a CollectionObject doctype.
138         CoreSession coreSession = docEventContext.getCoreSession();
139         // Some values below are hard-coded for readability, rather than
140         // being obtained from constants.
141         String query = String.format(
142                 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
143                 + "("
144                 + "  (%2$s:subjectCsid = '%3$s' "
145                 + "  AND %2$s:objectDocumentType = '%4$s') "
146                 + " OR "
147                 + "  (%2$s:objectCsid = '%3$s' "
148                 + "  AND %2$s:subjectDocumentType = '%4$s') "
149                 + ")"
150                 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
151                 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, movementCsid, COLLECTIONOBJECT_DOCTYPE);
152         DocumentModelList relationDocModels = coreSession.query(query);
153         if (relationDocModels == null || relationDocModels.isEmpty()) {
154             // Encountering a Movement record that is not related to any
155             // CollectionObject is potentially a normal occurrence, so no
156             // error messages are logged here when we stop handling this event.
157             return;
158         }
159
160         // Iterate through the list of Relation records found and build
161         // a list of CollectionObject CSIDs, by extracting the relevant CSIDs
162         // from those Relation records.
163
164         // FIXME: The following code might be refactored into a generic 'get
165         // values of a single property from a list of document models' method,
166         // if this doesn't already exist.
167         String csid = "";
168         Set<String> collectionObjectCsids = new HashSet<String>(); // Prevents/removes duplicates on add
169         for (DocumentModel relationDocModel : relationDocModels) {
170             csid = getCsidForDesiredDocType(relationDocModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
171             if (Tools.notBlank(csid)) {
172                 collectionObjectCsids.add(csid);
173             }
174         }
175         if (collectionObjectCsids == null || collectionObjectCsids.isEmpty()) {
176             logger.warn("Could not obtain any CSIDs of related CollectionObject records.");
177             logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
178             return;
179         } else {
180             if (logger.isTraceEnabled()) {
181                 logger.trace("Found " + collectionObjectCsids.size() + " CSIDs of related CollectionObject records.");
182             }
183         }
184
185         // Iterate through the list of CollectionObject CSIDs found.
186         DocumentModel collectionObjectDocModel = null;
187         String computedCurrentLocationRefName = "";
188         Map<DocumentModel, String> docModelsToUpdate = new HashMap<DocumentModel, String>();
189         for (String collectionObjectCsid : collectionObjectCsids) {
190
191             if (logger.isTraceEnabled()) {
192                 logger.trace("CollectionObject CSID=" + collectionObjectCsids);
193             }
194             collectionObjectDocModel = getDocModelFromCsid(coreSession, collectionObjectCsid);
195             if (collectionObjectDocModel == null) {
196                 continue;
197             }
198             // Verify that the CollectionObject record is active.
199             if (!isActiveDocument(collectionObjectDocModel)) {
200                 continue;
201             }
202             // Obtain the computed current location of that CollectionObject.
203             computedCurrentLocationRefName = computeCurrentLocation(coreSession, collectionObjectCsid);
204             if (logger.isTraceEnabled()) {
205                 logger.trace("computedCurrentLocation refName=" + computedCurrentLocationRefName);
206             }
207
208             // Check that the value returned, which is expected to be a
209             // reference (refName) to a storage location authority term,
210             // is, at a minimum:
211             // * Non-null and non-blank. (We need to verify this assumption; can a
212             //   CollectionObject's computed current location meaningfully be 'un-set'?)
213             // * Capable of being successfully parsed by an authority item parser;
214             //   that is, returning a non-null parse result.
215             if ((Tools.isBlank(computedCurrentLocationRefName)
216                     || (RefNameUtils.parseAuthorityTermInfo(computedCurrentLocationRefName) == null))) {
217                 logger.warn("Could not parse computed current location refName '" + computedCurrentLocationRefName + "'");
218                 continue;
219             } else {
220                 if (logger.isTraceEnabled()) {
221                     logger.trace("refName passes basic validation tests.");
222                 }
223
224                 // If the value returned from the function passes validation,
225                 // compare it to the value in the computedCurrentLocation
226                 // field of that CollectionObject.
227                 String existingComputedCurrentLocationRefName =
228                         (String) collectionObjectDocModel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY);
229                 if (logger.isTraceEnabled()) {
230                     logger.trace("Existing computedCurrentLocation refName=" + existingComputedCurrentLocationRefName);
231                 }
232                 // If the CollectionObject lacks a computed current location value,
233                 // or if the new computed value differs from its existing value ...
234                 if (Tools.isBlank(existingComputedCurrentLocationRefName)
235                         || (!computedCurrentLocationRefName.equals(existingComputedCurrentLocationRefName))) {
236                     if (logger.isTraceEnabled()) {
237                         logger.trace("computedCurrentLocation refName requires updating.");
238                     }
239                     // ... set aside this CollectionObject's docModel and its new
240                     // computed current location value for subsequent updating
241                     docModelsToUpdate.put(collectionObjectDocModel, computedCurrentLocationRefName);
242                 } else {
243                     if (logger.isTraceEnabled()) {
244                         logger.trace("computedCurrentLocation refName does NOT require updating.");
245                     }
246                 }
247             }
248         }
249
250         // For each CollectionObject docModel that has been set aside for updating,
251         // update the value of its computedCurrentLocation field with its new,
252         // computed current location.
253         int collectionObjectsUpdated = 0;
254         for (Map.Entry<DocumentModel, String> entry : docModelsToUpdate.entrySet()) {
255             DocumentModel dmodel = entry.getKey();
256             String newCurrentLocationValue = entry.getValue();
257             dmodel.setProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY, newCurrentLocationValue);
258             coreSession.saveDocument(dmodel);
259             collectionObjectsUpdated++;
260             if (logger.isTraceEnabled()) {
261                 String afterUpdateComputedCurrentLocationRefName =
262                         (String) dmodel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY);
263                 logger.trace("Following update, new computedCurrentLocation refName value=" + afterUpdateComputedCurrentLocationRefName);
264
265             }
266         }
267         logger.info("Updated " + collectionObjectsUpdated + " CollectionObject record(s) with new computed current location(s).");
268     }
269
270     // FIXME: Generic methods like many of those below might be split off,
271     // into an event utilities class, base classes, or otherwise. - ADR 2012-12-05
272     //
273     // FIXME: Identify whether the equivalent of the documentMatchesType utility
274     // method is already implemented and substitute a call to the latter if so.
275     // This may well already exist.
276     /**
277      * Identifies whether a document matches a supplied document type.
278      *
279      * @param docModel a document model.
280      * @param docType a document type string.
281      * @return true if the document matches the supplied document type; false if
282      * it does not.
283      */
284     private boolean documentMatchesType(DocumentModel docModel, String docType) {
285         if (docModel == null || Tools.isBlank(docType)) {
286             return false;
287         }
288         if (docModel.getType().startsWith(docType)) {
289             return true;
290         } else {
291             return false;
292         }
293     }
294
295     /**
296      * Identifies whether a document is an active document; that is, if it is
297      * not a versioned record; not a proxy (symbolic link to an actual record);
298      * and not in the 'deleted' workflow state.
299      *
300      * (A note relating the latter: Nuxeo appears to send 'documentModified'
301      * events even on workflow transitions, such when records are 'soft deleted'
302      * by being transitioned to the 'deleted' workflow state.)
303      *
304      * @param docModel
305      * @return true if the document is an active document; false if it is not.
306      */
307     private boolean isActiveDocument(DocumentModel docModel) {
308         if (docModel == null) {
309             return false;
310         }
311         boolean isActiveDocument = false;
312         try {
313             if (!docModel.isVersion()
314                     && !docModel.isProxy()
315                     && !docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
316                 isActiveDocument = true;
317             }
318         } catch (ClientException ce) {
319             logger.warn("Error while identifying whether document is an active document: ", ce);
320         }
321         return isActiveDocument;
322     }
323
324     /**
325      * Returns a document model for a record identified by a CSID.
326      *
327      * @param session a repository session.
328      * @param collectionObjectCsid a CollectionObject identifier (CSID)
329      * @return a document model for the record identified by the supplied CSID.
330      */
331     private DocumentModel getDocModelFromCsid(CoreSession session, String collectionObjectCsid) {
332         DocumentModelList collectionObjectDocModels = null;
333         try {
334             final String query = "SELECT * FROM "
335                     + NuxeoUtils.BASE_DOCUMENT_TYPE
336                     + " WHERE "
337                     + NuxeoUtils.getByNameWhereClause(collectionObjectCsid);
338             collectionObjectDocModels = session.query(query);
339         } catch (Exception e) {
340             logger.warn("Exception in query to get document model for CollectionObject: ", e);
341         }
342         if (collectionObjectDocModels == null || collectionObjectDocModels.isEmpty()) {
343             logger.warn("Could not get document models for CollectionObject(s).");
344             return null;
345         } else if (collectionObjectDocModels.size() != 1) {
346             logger.debug("Found more than 1 document with CSID=" + collectionObjectCsid);
347             return null;
348         }
349         return collectionObjectDocModels.get(0);
350     }
351
352     // FIXME: A quick first pass, using an only partly query-based technique for
353     // getting the current location, augmented by procedural code.
354     //
355     // Should be replaced by a more performant method, based entirely, or nearly so,
356     // on a query.
357     //
358     // E.g. the following is a sample CMIS query for retrieving Movement records
359     // related to a CollectionObject, which might serve as the basis for that query.
360     /*
361      "SELECT DOC.nuxeo:pathSegment, DOC.dc:title, REL.dc:title,"
362      + "REL.relations_common:objectCsid, REL.relations_common:subjectCsid FROM Movement DOC "
363      + "JOIN Relation REL ON REL.relations_common:objectCsid = DOC.nuxeo:pathSegment "
364      + "WHERE REL.relations_common:subjectCsid = '5b4c617e-53a0-484b-804e' "
365      + "AND DOC.nuxeo:isVersion = false "
366      + "ORDER BY DOC.collectionspace_core:updatedAt DESC";
367      */
368     /**
369      * Returns the computed current location for a CollectionObject.
370      *
371      * @param session a repository session.
372      * @param collectionObjectCsid a CollectionObject identifier (CSID)
373      * @throws ClientException
374      * @return the computed current location for the CollectionObject identified
375      * by the supplied CSID.
376      */
377     private String computeCurrentLocation(CoreSession session, String collectionObjectCsid)
378             throws ClientException {
379         String computedCurrentLocation = "";
380         // Get Relation records for Movements related to this CollectionObject.
381         //
382         // Some values below are hard-coded for readability, rather than
383         // being obtained from constants.
384         String query = String.format(
385                 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
386                 + "("
387                 + "  (%2$s:subjectCsid = '%3$s' "
388                 + "  AND %2$s:objectDocumentType = '%4$s') "
389                 + " OR "
390                 + "  (%2$s:objectCsid = '%3$s' "
391                 + "  AND %2$s:subjectDocumentType = '%4$s') "
392                 + ")"
393                 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
394                 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, collectionObjectCsid, MOVEMENT_DOCTYPE);
395         if (logger.isTraceEnabled()) {
396             logger.trace("query=" + query);
397         }
398         DocumentModelList relatedDocModels = session.query(query);
399         if (relatedDocModels == null || relatedDocModels.isEmpty()) {
400             logger.warn("Unexpectedly found no Movement records related to this CollectionObject record.");
401             return computedCurrentLocation;
402         } else {
403             if (logger.isTraceEnabled()) {
404                 logger.trace("Found " + relatedDocModels.size() + " Movement record(s) related to this CollectionObject record.");
405             }
406         }
407         // Iterate through related movement records, to get the CollectionObject's
408         // computed current location from the related Movement record with the
409         // most recent location date.
410         GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
411         DocumentModel movementDocModel = null;
412         String csid = "";
413         String location = "";
414         for (DocumentModel relatedDocModel : relatedDocModels) {
415             // Due to the 'OR' operator in the query above, related Movement
416             // record CSIDs may reside in either the subject or object CSID fields
417             // of the relation record. Whichever CSID value doesn't match the
418             // CollectionObject's CSID is inferred to be the related Movement record's CSID.
419             csid = (String) relatedDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
420             if (csid.equals(collectionObjectCsid)) {
421                 csid = (String) relatedDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
422             }
423             movementDocModel = getDocModelFromCsid(session, csid);
424             if (movementDocModel == null) {
425                 continue;
426             }
427             if (logger.isTraceEnabled()) {
428                 logger.trace("Movement CSID=" + NuxeoUtils.getCsid(movementDocModel));
429             }
430             // Verify that the Movement record is active. This will also exclude
431             // versioned Movement records from the computation of the current
432             // location, for tenants that are versioning such records.
433             if (!isActiveDocument(movementDocModel)) {
434                 if (logger.isTraceEnabled()) {
435                     logger.trace("Skipping this inactive Movement record ...");
436                 }
437                 continue;
438             }
439             GregorianCalendar locationDate =
440                     (GregorianCalendar) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
441             if (locationDate == null) {
442                 continue;
443             }
444             if (locationDate.after(mostRecentLocationDate)) {
445                 mostRecentLocationDate = locationDate;
446                 location = (String) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, CURRENT_LOCATION_PROPERTY);
447             }
448             if (Tools.notBlank(location)) {
449                 computedCurrentLocation = location;
450             }
451         }
452         return computedCurrentLocation;
453     }
454
455     /**
456      * Returns the CSID for a desired document type from a Relation record,
457      * where the relationship involves two specified, different document types.
458      *
459      * @param relationDocModel a document model for a Relation record.
460      * @param desiredDocType a desired document type.
461      * @param relatedDocType a related document type.
462      * @throws ClientException
463      * @return the CSID from the desired document type in the relation. Returns
464      * an empty string if the Relation record does not involve both the desired
465      * and related document types, or if the desired document type is at both
466      * ends of the relation.
467      */
468     private String getCsidForDesiredDocType(DocumentModel relationDocModel,
469             String desiredDocType, String relatedDocType) throws ClientException {
470         String csid = "";
471         String subjectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
472         String objectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
473         if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(desiredDocType)) {
474             return csid;
475         }
476         if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(relatedDocType)) {
477             csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
478         } else if (subjectDocType.startsWith(relatedDocType) && objectDocType.startsWith(desiredDocType)) {
479             csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
480         }
481         return csid;
482     }
483 }