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