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