]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
0f4826ae9ccf72965f7d47c4c3edbe5cac542a52
[tmp/jakarta-migration.git] /
1 package org.collectionspace.services.listener;
2
3 import java.util.Calendar;
4 import java.util.GregorianCalendar;
5 import java.util.HashSet;
6 import java.util.Locale;
7 import java.util.Set;
8
9 import org.apache.commons.logging.Log;
10 import org.apache.commons.logging.LogFactory;
11 import org.collectionspace.services.client.LocationAuthorityClient;
12 import org.collectionspace.services.client.workflow.WorkflowClient;
13 import org.collectionspace.services.collectionobject.nuxeo.CollectionObjectConstants;
14 import org.collectionspace.services.common.api.Tools;
15 import org.collectionspace.services.common.relation.nuxeo.RelationConstants;
16 import org.collectionspace.services.common.api.RefName;
17 import org.collectionspace.services.movement.nuxeo.MovementConstants;
18 import org.collectionspace.services.nuxeo.client.java.CoreSessionInterface;
19 import org.collectionspace.services.nuxeo.client.java.CoreSessionWrapper;
20 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
21 import org.nuxeo.common.collections.ScopeType;
22 import org.nuxeo.common.collections.ScopedMap;
23 import org.nuxeo.ecm.core.api.ClientException;
24 import org.nuxeo.ecm.core.api.DocumentModel;
25 import org.nuxeo.ecm.core.api.DocumentModelList;
26 import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
27 import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
28 import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
29 import org.nuxeo.ecm.core.event.Event;
30 import org.nuxeo.ecm.core.event.EventListener;
31 import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
32 import org.nuxeo.ecm.core.versioning.VersioningService;
33
34 public abstract class AbstractUpdateObjectLocationValues implements EventListener {
35
36     // FIXME: We might experiment here with using log4j instead of Apache Commons Logging;
37     // am using the latter to follow Ray's pattern for now
38     private final static Log logger = LogFactory.getLog(AbstractUpdateObjectLocationValues.class);
39     
40     // FIXME: Make the following message, or its equivalent, a constant usable by all event listeners
41     private final static String NO_FURTHER_PROCESSING_MESSAGE =
42             "This event listener will not continue processing this event ...";
43     
44     private final static GregorianCalendar EARLIEST_COMPARISON_DATE = new GregorianCalendar(1600, 1, 1);
45     private final static String RELATIONS_COMMON_SCHEMA = "relations_common"; // FIXME: Get from external constant
46     
47     private final static String COLLECTIONOBJECT_DOCTYPE = CollectionObjectConstants.NUXEO_DOCTYPE;
48     private final static String RELATION_DOCTYPE = RelationConstants.NUXEO_DOCTYPE;//"Relation"; // FIXME: Get from external constant
49     private final static String MOVEMENT_DOCTYPE = MovementConstants.NUXEO_DOCTYPE;
50     
51     private final static String SUBJECT_CSID_PROPERTY = "subjectCsid"; // FIXME: Get from external constant
52     private final static String OBJECT_CSID_PROPERTY = "objectCsid"; // FIXME: Get from external constant
53     private final static String SUBJECT_DOCTYPE_PROPERTY = "subjectDocumentType"; // FIXME: Get from external constant
54     private final static String OBJECT_DOCTYPE_PROPERTY = "objectDocumentType"; // FIXME: Get from external constant
55     protected final static String COLLECTIONOBJECTS_COMMON_SCHEMA = "collectionobjects_common"; // FIXME: Get from external constant
56     protected final static String COMPUTED_CURRENT_LOCATION_PROPERTY = "computedCurrentLocation"; // FIXME: Create and then get from external constant
57     private final static String CURRENT_LOCATION_ELEMENT_NAME = "currentLocation"; // From movement_commons schema.  FIXME: Get from external constant that already exists
58     protected final static String MOVEMENTS_COMMON_SCHEMA = "movements_common"; // FIXME: Get from external constant
59     private final static String LOCATION_DATE_PROPERTY = "locationDate"; // FIXME: Get from external constant
60     protected final static String CURRENT_LOCATION_PROPERTY = "currentLocation"; // FIXME: Get from external constant
61     protected final static String COLLECTIONSPACE_CORE_SCHEMA = "collectionspace_core"; // FIXME: Get from external constant
62     protected final static String CREATED_AT_PROPERTY = "createdAt"; // FIXME: Get from external constant
63     protected final static String UPDATED_AT_PROPERTY = "updatedAt"; // FIXME: Get from external constant
64     
65     // Use this meta URN/refname to mark computed locations that are indeterminate
66     private final static String INDETERMINATE_ID = "indeterminate";
67     protected final static String INDETERMINATE_LOCATION = RefName.buildAuthorityItem(INDETERMINATE_ID, LocationAuthorityClient.SERVICE_NAME, INDETERMINATE_ID,
68                 INDETERMINATE_ID, "~Indeterminate Location~").toString();
69     
70     // SQL clauses
71     private final static String NONVERSIONED_NONPROXY_DOCUMENT_WHERE_CLAUSE_FRAGMENT =
72             "AND ecm:isCheckedInVersion = 0"
73             + " AND ecm:isProxy = 0 ";
74     private final static String ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT =
75             "AND (ecm:currentLifeCycleState <> 'deleted') "
76             + NONVERSIONED_NONPROXY_DOCUMENT_WHERE_CLAUSE_FRAGMENT;
77
78         private static final String IGNORE_LOCATION_UPDATE_EVENT_LABEL = "IGNORE_LOCATION_UPDATE_EVENT";
79         private static final String IGNORE_LOCATION_UPDATE_EVENT_OPTION = ScopeType.DEFAULT.getScopePrefix() + "IGNORE_LOCATION_UPDATE_EVENT";
80     
81     public enum EventNotificationDocumentType {
82         // Document type about which we've received a notification
83
84         MOVEMENT, RELATION, COLLECTIONOBJECT;
85     }
86     
87     private static void dumpEvent(Event event, String message) {
88         if (logger.isDebugEnabled()) {
89                 DocumentEventContext docEventContext = (DocumentEventContext) event.getContext();
90                 DocumentModel docModel = docEventContext.getSourceDocument();
91                 String eventType = event.getName();
92                 String csid = NuxeoUtils.getCsid(docModel);
93
94                 logger.debug(String.format("### %s:", message != null ? message : "Unspecified"));
95                 logger.debug(String.format("### \t-Event type: %s", eventType));
96                 
97                 logger.debug("### \t-Target documment:");
98                 logger.debug(String.format("### \t\tCSID=%s", csid));
99                 logger.debug(String.format("### \t\tDocType=%s", docModel.getDocumentType().getName()));
100                 
101                 if (documentMatchesType(docModel, RELATION_DOCTYPE)) {
102                     String subjectDocType = (String) docModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
103                     String objectDocType = (String) docModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);                 
104                 String subjectCsid = (String) docModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
105                 String objectCsid = (String) docModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
106                         logger.debug(String.format("\tRelation info subject=%s:%s\tobject=%s:%s",
107                                         subjectCsid, subjectDocType, objectCsid, objectDocType));
108                 } else if (documentMatchesType(docModel, MOVEMENT_DOCTYPE)) {
109                     String currentLocation = (String) docModel.getProperty(MOVEMENTS_COMMON_SCHEMA, CURRENT_LOCATION_ELEMENT_NAME);
110                     GregorianCalendar locationDate = (GregorianCalendar) docModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
111                         logger.debug("### \t-Movement Info:");
112                         logger.debug(String.format("### \t\tCSID=%s", csid));
113                         logger.debug(String.format("### \t\tlocation=%s", currentLocation != null ? currentLocation : "null"));
114                         if (locationDate != null) {
115                                 logger.debug(String.format("### \t\tdate=%1$tm-%1$te-%1$tY", locationDate != null ? locationDate : ""));
116                         } else {
117                                 logger.debug(String.format("### \t\tdate=<empty>"));
118                         }
119                 } else {
120                         logger.debug(String.format("### Ignoring Update Location event: %s", eventType));
121                 }
122         }
123     }
124     
125     /*
126      * Figure out if we should ignore this event.
127      */
128     private boolean shouldIgnoreEvent(DocumentEventContext docEventContext, String ignoreEventLabel) {
129         boolean result = false;
130         
131         Boolean shouldIgnoreEvent = (Boolean) docEventContext.getProperties().get(ignoreEventLabel);
132         if (shouldIgnoreEvent != null && shouldIgnoreEvent) {
133                 result = true;
134         }
135
136         return result;
137     }
138
139     @Override
140     public void handleEvent(Event event) throws ClientException {
141         dumpEvent(event, "Update Location");
142         // Ensure we have all the event data we need to proceed.
143         if (event.getContext() == null || !(event.getContext() instanceof DocumentEventContext)) {
144             return;
145         }
146
147         DocumentEventContext docEventContext = (DocumentEventContext) event.getContext();
148         DocumentModel eventDocModel = docEventContext.getSourceDocument();        
149         String eventType = event.getName();
150         boolean isAboutToBeRemovedEvent = eventType.equals(DocumentEventTypes.ABOUT_TO_REMOVE);
151         
152         //
153         // This event handler itself sometimes triggers additional events.  To prevent unnecessary cascading event handling, this event
154         // handler sets a flag in the document model indicating we should ignore cascading events.  This method checks that flag and
155         // exits if it is set.
156         if (shouldIgnoreEvent(docEventContext, IGNORE_LOCATION_UPDATE_EVENT_LABEL) == true) {
157                 return;
158         }        
159
160         //
161         // Ensure this event relates to a relationship record (between cataloging and movement records) or a movement record.  If so, get the CSID
162         // of the corresponding movement record.  Otherwise, exit.
163         //
164         String eventMovementCsid = null;
165         Enum<EventNotificationDocumentType> notificationDocumentType;
166         if (documentMatchesType(eventDocModel, RELATION_DOCTYPE)) {
167             notificationDocumentType = EventNotificationDocumentType.RELATION;
168             // Ensure this relationship record is a CollectionObject/Movement tuple.
169             eventMovementCsid = getCsidForDesiredDocTypeFromRelation(eventDocModel, MOVEMENT_DOCTYPE, COLLECTIONOBJECT_DOCTYPE);
170             if (Tools.isBlank(eventMovementCsid)) {
171                 return;
172             }
173         } else if (documentMatchesType(eventDocModel, MOVEMENT_DOCTYPE)) {
174             notificationDocumentType = EventNotificationDocumentType.MOVEMENT;
175             // Otherwise, get a Movement CSID directly from the Movement record.
176             eventMovementCsid = NuxeoUtils.getCsid(eventDocModel);
177             if (Tools.isBlank(eventMovementCsid)) {
178                 logger.warn("Could not obtain CSID for Movement record from document event.");
179                 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
180                 return;
181             }
182         } else if (documentMatchesType(eventDocModel, COLLECTIONOBJECT_DOCTYPE) &&
183                 eventType.equals(DocumentEventTypes.DOCUMENT_UPDATED)) {
184                 notificationDocumentType = EventNotificationDocumentType.COLLECTIONOBJECT;
185         } else {
186                 // We don't need to handle this event.
187             return;
188         }
189
190         // Note: currently, all Document lifecycle transitions on
191         // the relevant doctype(s) are handled by this event handler,
192         // not just transitions between 'soft deleted' and active states.
193         //
194         // We are assuming that we'll want to re-compute current locations
195         // for related CollectionObjects on all such transitions, as the
196         // semantics of such transitions are opaque to this event handler,
197         // because arbitrary workflows can be bound to those doctype(s).
198         //
199         // If we need to filter out some of those lifecycle transitions,
200         // such as excluding transitions to the 'locked' workflow state; or,
201         // alternately, if we want to restrict this event handler's
202         // scope to handle only transitions into the 'soft deleted' state,
203         // we can add additional checks for doing so at this point in the code.
204
205         //
206         // Get a list of all the CollectionObject records affected by this event.
207         // 
208         CoreSessionInterface session = new CoreSessionWrapper(docEventContext.getCoreSession()); // NOTE: All Nuxeo sessions that get passed around to CollectionSpace code need to be wrapped inside of a CoreSessionWrapper
209         Set<String> collectionObjectCsids = new HashSet<>();
210         if (notificationDocumentType == EventNotificationDocumentType.RELATION) {
211             String relatedCollectionObjectCsid = getCsidForDesiredDocTypeFromRelation(eventDocModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
212             collectionObjectCsids.add(relatedCollectionObjectCsid);
213         } else if (notificationDocumentType == EventNotificationDocumentType.MOVEMENT) {
214             collectionObjectCsids.addAll(getCollectionObjectCsidsRelatedToMovement(eventMovementCsid, session));
215         } else if (notificationDocumentType == EventNotificationDocumentType.COLLECTIONOBJECT) {
216                 collectionObjectCsids.add(NuxeoUtils.getCsid(eventDocModel));
217         } else {
218             // This event did not involve a document relevant to us.
219             return;
220         }
221
222         //
223         // If we found no collectionobject records needing updating, then we're done.
224         //
225         if (collectionObjectCsids.isEmpty() == true) {
226             return;
227         }
228         
229         //
230         // Now iterate through the list of affected CollectionObjects found.
231         // For each CollectionObject, obtain its most recent, related Movement record,
232         // and update update the Computed Current Location field if needed.
233         //
234         DocumentModel collectionObjectDocModel;
235         DocumentModel mostRecentMovementDocModel;
236         for (String collectionObjectCsid : collectionObjectCsids) {            
237             collectionObjectDocModel = getCurrentDocModelFromCsid(session, collectionObjectCsid);
238             if (isActiveDocument(collectionObjectDocModel) == true) {           
239                     //
240                     // Get the CollectionObject's most recent, valid related Movement to use for computing the
241                     // object's current location.
242                     //
243                                 String mostRecentLocation = getMostRecentMovement(event, session, collectionObjectCsid,
244                                                 isAboutToBeRemovedEvent, eventMovementCsid);
245                     //
246                     // Update the CollectionObject's Computed Current Location field with the Movement record's location
247                                 //
248                     boolean didLocationChange = updateCollectionObjectLocation(collectionObjectDocModel, mostRecentLocation);
249                     
250                     //
251                     // If the location changed, save/persist the change to the repository and log the change.
252                     //
253                 if (didLocationChange == true) {
254                         persisLocationChange(session, collectionObjectDocModel);
255                     //
256                     // Log an INFO message if we've changed the cataloging record's location
257                     //              
258                     if (logger.isInfoEnabled()) {
259                                 String computedCurrentLocationRefName =
260                                         (String) collectionObjectDocModel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY);
261                                 logger.info(String.format("Updating cataloging record=%s current location to %s",
262                                                 NuxeoUtils.getCsid(collectionObjectDocModel), computedCurrentLocationRefName));
263                     }
264                 }
265             }
266         }
267     }
268     
269     //
270     // Disable update/documentModified events and persist the location change.
271     //
272     private void persisLocationChange(CoreSessionInterface session, DocumentModel collectionObjectDocModel) {
273         
274         //
275         // Set a flag in the document model indicating that we want to ignore the update event that
276         // will be triggered by this save/persist request.
277         ScopedMap contextData = collectionObjectDocModel.getContextData();
278         contextData.putIfAbsent(IGNORE_LOCATION_UPDATE_EVENT_OPTION, true);
279
280         //
281         // Save/Persist the document to the DB
282         session.saveDocument(collectionObjectDocModel);
283         
284         //
285         // Clear the flag we set to ignore events triggered by our save request.
286         contextData.remove(IGNORE_LOCATION_UPDATE_EVENT_OPTION);
287     }
288
289     /**
290      * Returns the CSIDs of active CollectionObject records related to a Movement record.
291      *
292      * @param movementCsid the CSID of a Movement record.
293      * @param coreSession a repository session.
294      * @throws ClientException
295      * @return the CSIDs of the CollectionObject records, if any, which are
296      * related to the Movement record.
297      */
298     private Set<String> getCollectionObjectCsidsRelatedToMovement(String movementCsid,
299             CoreSessionInterface coreSession) throws ClientException {
300
301         Set<String> csids = new HashSet<>();
302
303         // Via an NXQL query, get a list of active relation records where:
304         // * This movement record's CSID is the subject CSID of the relation,
305         //   and its object document type is a CollectionObject doctype;
306         // or
307         // * This movement record's CSID is the object CSID of the relation,
308         //   and its subject document type is a CollectionObject doctype.
309         //
310         // Some values below are hard-coded for readability, rather than
311         // being obtained from constants.
312         String query = String.format(
313                 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
314                 + "("
315                 + "  (%2$s:subjectCsid = '%3$s' "
316                 + "  AND %2$s:objectDocumentType = '%4$s') "
317                 + " OR "
318                 + "  (%2$s:objectCsid = '%3$s' "
319                 + "  AND %2$s:subjectDocumentType = '%4$s') "
320                 + ")"
321                 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
322                 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, movementCsid, COLLECTIONOBJECT_DOCTYPE);
323         
324         DocumentModelList relationDocModels = coreSession.query(query);
325         if (relationDocModels == null || relationDocModels.isEmpty()) {
326             return csids;
327         }
328         
329         // Iterate through the list of Relation records found and build
330         // a list of CollectionObject CSIDs, by extracting the relevant CSIDs
331         // from those Relation records.
332         String csid;
333         for (DocumentModel relationDocModel : relationDocModels) {
334             csid = getCsidForDesiredDocTypeFromRelation(relationDocModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
335             if (Tools.notBlank(csid)) {
336                 csids.add(csid);
337             }
338         }
339         
340         return csids;
341     }
342
343 // FIXME: Generic methods like many of those below might be split off from
344 // this specific event listener/handler, into an event handler utilities
345 // class, base classes, or otherwise.
346 //
347 // FIXME: Identify whether the equivalent of the documentMatchesType utility
348 // method is already implemented and substitute a call to the latter if so.
349 // This may well already exist.
350     /**
351      * Identifies whether a document matches a supplied document type.
352      *
353      * @param docModel a document model.
354      * @param docType a document type string.
355      * @return true if the document matches the supplied document type; false if
356      * it does not.
357      */
358     protected static boolean documentMatchesType(DocumentModel docModel, String docType) {
359         if (docModel == null || Tools.isBlank(docType)) {
360             return false;
361         }
362         if (docModel.getType().startsWith(docType)) {
363             return true;
364         } else {
365             return false;
366         }
367     }
368
369     protected static boolean isActiveDocument(DocumentModel docModel) {
370         return isActiveDocument(docModel, false, null);
371     }
372     
373     /**
374      * Identifies whether a document is an active document; currently, whether
375      * it is not in a 'deleted' workflow state.
376      *
377      * @param docModel
378      * @return true if the document is an active document; false if it is not.
379      */
380     protected static boolean isActiveDocument(DocumentModel docModel, boolean isAboutToBeRemovedEvent, String aboutToBeRemovedCsid) {
381         boolean isActiveDocument = false;
382
383         if (docModel != null) {         
384                 try {
385                     if (!docModel.getCurrentLifeCycleState().contains(WorkflowClient.WORKFLOWSTATE_DELETED)) {
386                         isActiveDocument = true;
387                     }
388                 } catch (ClientException ce) {
389                     logger.warn("Error while identifying whether document is an active document: ", ce);
390                 }
391                 //
392                 // If doc model is the target of the "aboutToBeRemoved" event, mark it as not active.
393                 //
394                 if (isAboutToBeRemovedEvent && Tools.notBlank(aboutToBeRemovedCsid)) {
395                         if (NuxeoUtils.getCsid(docModel).equalsIgnoreCase(aboutToBeRemovedCsid)) {
396                                 isActiveDocument = false;
397                         }
398                 }
399         }
400         
401         return isActiveDocument;
402     }
403
404     /**
405      * Returns the current document model for a record identified by a CSID.
406      *
407      * Excludes documents which have been versioned (i.e. are a non-current
408      * version of a document), are a proxy for another document, or are
409      * un-retrievable via their CSIDs.
410      *
411      * @param session a repository session.
412      * @param collectionObjectCsid a CollectionObject identifier (CSID)
413      * @return a document model for the document identified by the supplied
414      * CSID.
415      */
416     protected static DocumentModel getCurrentDocModelFromCsid(CoreSessionInterface session, String collectionObjectCsid) {
417         DocumentModelList docModelList = null;
418         
419         try {
420             final String query = "SELECT * FROM "
421                     + NuxeoUtils.BASE_DOCUMENT_TYPE
422                     + " WHERE "
423                     + NuxeoUtils.getByNameWhereClause(collectionObjectCsid)
424                     + " "
425                     + NONVERSIONED_NONPROXY_DOCUMENT_WHERE_CLAUSE_FRAGMENT;
426             docModelList = session.query(query);
427         } catch (Exception e) {
428             logger.warn("Exception in query to get active document model for CollectionObject: ", e);
429         }
430         
431         if (docModelList == null || docModelList.isEmpty()) {
432             logger.warn("Could not get active document models for CollectionObject(s).");
433             return null;
434         } else if (docModelList.size() != 1) {
435             logger.error("Found more than 1 active document with CSID=" + collectionObjectCsid);
436             return null;
437         }
438         
439         return docModelList.get(0);
440     }
441     
442     //
443     // Returns true if this event is for the creation of a new relationship record
444     //
445     private static boolean isCreatingNewRelationship(Event event) {
446         boolean result = false;
447         
448         DocumentModel docModel = ((DocumentEventContext)event.getContext()).getSourceDocument();
449         if (event.getName().equals(DocumentEventTypes.DOCUMENT_CREATED) && documentMatchesType(docModel, RELATION_DOCTYPE)) {
450                 result = true;
451         }
452         
453         return result;
454     }
455
456     // FIXME: A quick first pass, using an only partly query-based technique for
457     // getting the most recent Movement record related to a CollectionObject,
458     // augmented by procedural code.
459     //
460     // Could be replaced by a potentially more performant method, based on a query.
461     //
462     // E.g. the following is a sample CMIS query for retrieving Movement records
463     // related to a CollectionObject, which might serve as the basis for that query.
464     /*
465      "SELECT DOC.nuxeo:pathSegment, DOC.dc:title, REL.dc:title,"
466      + "REL.relations_common:objectCsid, REL.relations_common:subjectCsid FROM Movement DOC "
467      + "JOIN Relation REL ON REL.relations_common:objectCsid = DOC.nuxeo:pathSegment "
468      + "WHERE REL.relations_common:subjectCsid = '5b4c617e-53a0-484b-804e' "
469      + "AND DOC.nuxeo:isVersion = false "
470      + "ORDER BY DOC.collectionspace_core:updatedAt DESC";
471      */
472     /**
473      * Returns the most recent Movement record related to a CollectionObject.
474      *
475      * This method currently returns the related Movement record with the latest
476      * (i.e. most recent in time) Location Date field value.
477      *
478      * @param session a repository session.
479      * @param collectionObjectCsid a CollectionObject identifier (CSID)
480      * @param isAboutToBeRemovedEvent whether the current event involves a
481      * record that is slated for removal (hard deletion)
482      * @param movementCsidToFilter the CSID of a Movement record slated for
483      * deletion, or of a Movement record referenced by a Relation record slated
484      * for deletion. This record should be filtered out, prior to returning the
485      * most recent Movement record.
486      * @throws ClientException
487      * @return the most recent Movement record related to the CollectionObject
488      * identified by the supplied CSID.
489      */
490     protected static String getMostRecentMovement(Event event,
491                 CoreSessionInterface session, String collectionObjectCsid,
492             boolean isAboutToBeRemovedEvent, String eventMovementCsid)
493             throws ClientException {
494         //
495         // Assume we can determine the most recent location by creating an indeterminate result
496         //
497                 String result = INDETERMINATE_LOCATION;
498                 
499         //
500         // Get active Relation records involving Movement records related to this CollectionObject.
501         //
502         String query = String.format(
503                 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
504                 + "("
505                 + "  (%2$s:subjectCsid = '%3$s' "
506                 + "  AND %2$s:objectDocumentType = '%4$s') "
507                 + " OR "
508                 + "  (%2$s:objectCsid = '%3$s' "
509                 + "  AND %2$s:subjectDocumentType = '%4$s') "
510                 + ")"
511                 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
512                 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, collectionObjectCsid, MOVEMENT_DOCTYPE);
513         logger.trace("query=" + query);
514
515         DocumentModelList relationDocModels = session.query(query);
516         if (isCreatingNewRelationship(event) == true) {
517                 DocumentModel newRelation = ((DocumentEventContext)event.getContext()).getSourceDocument();
518                 relationDocModels.add(newRelation);
519         }
520                 
521         //
522         // Remove redundant document models from the list.
523         //
524         relationDocModels = removeRedundantRelations(relationDocModels);        
525         
526         //
527         // Remove relationships that are with inactive movement records
528         //
529         relationDocModels = removeInactiveRelations(session, relationDocModels, isAboutToBeRemovedEvent, eventMovementCsid);
530         
531         //
532         // If there are no candidate relationships after we removed the duplicates and inactive ones,
533         // throw an exception.
534         //
535         if (relationDocModels == null || relationDocModels.size() == 0) {
536                 return result;
537         }
538         
539         //
540         // If there is only one related movement record, then return it as the most recent
541         // movement record -but only if it's current location element is not empty.
542         //
543         if (relationDocModels.size() == 1) {
544                 DocumentModel relationDocModel = relationDocModels.get(0);
545                 DocumentModel movementDocModel = getMovementDocModelFromRelation(session, relationDocModel);
546             String location = (String) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, CURRENT_LOCATION_ELEMENT_NAME);
547             
548             if (Tools.isBlank(location) == false) {
549                 result = location;
550             } else { // currentLocation must be set
551                 logger.error(String.format("Movement record=%s is missing its required location value and so is excluded from the computation of cataloging record=%s's current location.",
552                                 NuxeoUtils.getCsid(movementDocModel), collectionObjectCsid));
553             }
554
555             return result;
556         }
557         
558         //
559         // Iterate through the list (>2) of related movement records, to find the related
560         // Movement record with the most recent location date.
561         //
562         GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
563         GregorianCalendar mostRecentUpdatedDate = EARLIEST_COMPARISON_DATE;
564
565         for (DocumentModel relationDocModel : relationDocModels) {
566             String relatedMovementCsid;
567             DocumentModel movementDocModel;
568                 //
569                 // The movement record is either the subject or object of the relationship, but not both.
570                 //
571             relatedMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
572             if (relatedMovementCsid.equals(collectionObjectCsid)) {
573                 relatedMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
574             }
575             movementDocModel = getCurrentDocModelFromCsid(session, relatedMovementCsid);
576             String location = (String) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, CURRENT_LOCATION_ELEMENT_NAME);
577             
578             //
579             // If the current Movement record lacks a location date, it cannot
580             // be established as the most recent Movement record; skip over it.
581             //
582             GregorianCalendar locationDate = (GregorianCalendar) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
583             if (locationDate == null) {
584                 logger.info(String.format("Movement record=%s has no location date and so is excluded from computation of cataloging record=%s current location.",
585                                 NuxeoUtils.getCsid(movementDocModel), collectionObjectCsid));
586                 continue;
587             }
588             
589             GregorianCalendar updatedDate = (GregorianCalendar) movementDocModel.getProperty(COLLECTIONSPACE_CORE_SCHEMA, UPDATED_AT_PROPERTY);
590             if (locationDate.after(mostRecentLocationDate)) {
591                 mostRecentLocationDate = locationDate;
592                 mostRecentUpdatedDate = updatedDate;
593                 result = location;
594             } else if (locationDate.compareTo(mostRecentLocationDate) == 0) {
595                 // If the current Movement record's location date is identical
596                 // to that of the (at this time) most recent Movement record, then
597                 // instead compare the two records using their update date values
598                 if (updatedDate.after(mostRecentUpdatedDate)) {
599                     // The most recent location date value doesn't need to be
600                     // updated here, as the two records' values are identical
601                     mostRecentUpdatedDate = updatedDate;
602                     result = location;
603                 }
604             }
605         }
606         
607         return result;
608     }
609     
610         //
611     // This method assumes that the relation passed into this method is between a Movement record
612     // and a CollectionObject (cataloging) record.
613     //
614     private static DocumentModel getMovementDocModelFromRelation(CoreSessionInterface session, DocumentModel relationDocModel) {
615         String movementCsid = null;
616         
617         String subjectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
618         if (subjectDocType.endsWith(MOVEMENT_DOCTYPE)) {
619                 movementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
620         } else {
621                 movementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
622         }
623
624                 return getCurrentDocModelFromCsid(session, movementCsid);
625         }
626
627         //
628     // Compares two Relation document models to see if they're either identical or
629     // reciprocal equivalents. 
630     //
631     private static boolean compareRelationDocModels(DocumentModel r1, DocumentModel r2) {
632         boolean result = false;
633         
634         String r1_subjectDocType = (String) r1.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
635         String r1_objectDocType = (String) r1.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
636         String r1_subjectCsid = (String) r1.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
637         String r1_objectCsid = (String) r1.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
638
639         String r2_subjectDocType = (String) r2.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
640         String r2_objectDocType = (String) r2.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
641         String r2_subjectCsid = (String) r2.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
642         String r2_objectCsid = (String) r2.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
643         
644         // Check to see if they're identical
645         if (r1_subjectDocType.equalsIgnoreCase(r2_subjectDocType) && r1_objectDocType.equalsIgnoreCase(r2_objectDocType)
646                         && r1_subjectCsid.equalsIgnoreCase(r2_subjectCsid) && r1_objectCsid.equalsIgnoreCase(r2_objectCsid)) {
647                 return true;
648         }
649         
650         // Check to see if they're reciprocal
651         if (r1_subjectDocType.equalsIgnoreCase(r2_objectDocType) && r1_objectDocType.equalsIgnoreCase(r2_subjectDocType)
652                         && r1_subjectCsid.equalsIgnoreCase(r2_objectCsid) && r1_objectCsid.equalsIgnoreCase(r2_subjectCsid)) {
653                 return true;
654         }
655
656         return result;
657     }
658
659     //
660     // Return a Relation document model list with redundant (either identical or reciprocal) relations removed.
661     //
662     private static DocumentModelList removeRedundantRelations(DocumentModelList relationDocModelList) {
663         DocumentModelList resultList = null;
664         
665         if (relationDocModelList != null && relationDocModelList.size() > 0) {
666                 resultList = new DocumentModelListImpl();
667                 for (DocumentModel relationDocModel : relationDocModelList) {
668                         if (existsInResultList(resultList, relationDocModel) == false) {
669                                 resultList.add(relationDocModel);
670                         }
671                 }
672         }
673         
674                 // TODO Auto-generated method stub
675                 return resultList;
676         }
677     
678     //
679     // Return just the list of active relationships with active Movement records.  A value of 'true' for the 'isAboutToBeRemovedEvent'
680     // argument indicates that relationships with the 'movementCsid' record should be considered inactive.
681     //
682     private static DocumentModelList removeInactiveRelations(CoreSessionInterface session,
683                 DocumentModelList relationDocModelList,
684                 boolean isAboutToBeRemovedEvent,
685                 String eventMovementCsid) {
686         DocumentModelList resultList = null;
687         
688         if (relationDocModelList != null && relationDocModelList.size() > 0) {
689                 resultList = new DocumentModelListImpl();
690                 for (DocumentModel relationDocModel : relationDocModelList) {
691                     String movementCsid = getCsidForDesiredDocTypeFromRelation(relationDocModel, MOVEMENT_DOCTYPE, COLLECTIONOBJECT_DOCTYPE);
692                     DocumentModel movementDocModel = getCurrentDocModelFromCsid(session, movementCsid);
693                     if (isActiveDocument(movementDocModel, isAboutToBeRemovedEvent, eventMovementCsid) == true) {
694                                 resultList.add(relationDocModel);
695                     } else {
696                         logger.debug(String.format("Disqualified relationship=%s with Movement record=%s from current location computation.",
697                                         NuxeoUtils.getCsid(relationDocModel), movementCsid));
698                     }
699                 }
700         }
701         
702                 return resultList;
703         }
704     
705
706     //
707     // Check to see if the Relation (or its equivalent reciprocal) is already in the list.
708     //
709         private static boolean existsInResultList(DocumentModelList relationDocModelList, DocumentModel relationDocModel) {
710                 boolean result = false;
711                 
712         for (DocumentModel target : relationDocModelList) {
713                 if (compareRelationDocModels(relationDocModel, target) == true) {
714                         result = true;
715                         break;
716                 }
717         }
718                 
719                 return result;
720         }
721
722         /**
723      * Returns the CSID for a desired document type from a Relation record,
724      * where the relationship involves two specified document types.
725      *
726      * @param relationDocModel a document model for a Relation record.
727      * @param desiredDocType a desired document type.
728      * @param relatedDocType a related document type.
729      * @throws ClientException
730      * 
731      * @return the CSID from the desired document type in the relation. Returns
732      * null if the Relation record does not involve both the desired
733      * and related document types.
734      */
735     protected static String getCsidForDesiredDocTypeFromRelation(DocumentModel relationDocModel,
736             String desiredDocType, String relatedDocType) throws ClientException {
737         String csid = null;
738         String subjectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
739         String objectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
740         
741         if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(relatedDocType)) {  // Use startsWith() method, because customized tenant type names differ in their suffix.
742             csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
743         } else if (subjectDocType.startsWith(relatedDocType) && objectDocType.startsWith(desiredDocType)) {
744             csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
745         }
746         
747         return csid;
748     }
749
750     // The following method can be extended by sub-classes to update
751     // different/multiple values; e.g. values for moveable locations ("crates").
752     /**
753      * Updates a CollectionObject record with selected values from a Movement
754      * record.
755      *
756      * @param collectionObjectDocModel a document model for a CollectionObject
757      * record.
758      * @param movementDocModel a document model for a Movement record.
759      * @return a potentially updated document model for the CollectionObject
760      * record.
761      * @throws ClientException
762      */
763     protected abstract boolean updateCollectionObjectLocation(DocumentModel collectionObjectDocModel,
764                 String movementRecordsLocation)
765             throws ClientException;
766 }