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