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