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