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