]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
99164cc77977a4db999bda80ecbd6d881b681f49
[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.Set;
6
7 import org.apache.commons.logging.Log;
8 import org.apache.commons.logging.LogFactory;
9
10 import org.collectionspace.services.client.workflow.WorkflowClient;
11 import org.collectionspace.services.common.api.Tools;
12 import org.collectionspace.services.movement.nuxeo.MovementConstants;
13 import org.collectionspace.services.nuxeo.client.java.CoreSessionInterface;
14 import org.collectionspace.services.nuxeo.client.java.CoreSessionWrapper;
15 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
16
17 import org.nuxeo.ecm.core.api.ClientException;
18 import org.nuxeo.ecm.core.api.DocumentModel;
19 import org.nuxeo.ecm.core.api.DocumentModelList;
20 import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
21 import org.nuxeo.ecm.core.event.Event;
22 import org.nuxeo.ecm.core.event.EventContext;
23 import org.nuxeo.ecm.core.event.EventListener;
24 import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
25
26 public abstract class AbstractUpdateObjectLocationValues implements EventListener {
27
28     // FIXME: We might experiment here with using log4j instead of Apache Commons Logging;
29     // am using the latter to follow Ray's pattern for now
30     private final static Log logger = LogFactory.getLog(AbstractUpdateObjectLocationValues.class);
31     // FIXME: Make the following message, or its equivalent, a constant usable by all event listeners
32     private final static String NO_FURTHER_PROCESSING_MESSAGE =
33             "This event listener will not continue processing this event ...";
34     private final static GregorianCalendar EARLIEST_COMPARISON_DATE = new GregorianCalendar(1600, 1, 1);
35     private final static String RELATIONS_COMMON_SCHEMA = "relations_common"; // FIXME: Get from external constant
36     private final static String RELATION_DOCTYPE = "Relation"; // FIXME: Get from external constant
37     private final static String SUBJECT_CSID_PROPERTY = "subjectCsid"; // FIXME: Get from external constant
38     private final static String OBJECT_CSID_PROPERTY = "objectCsid"; // FIXME: Get from external constant
39     private final static String SUBJECT_DOCTYPE_PROPERTY = "subjectDocumentType"; // FIXME: Get from external constant
40     private final static String OBJECT_DOCTYPE_PROPERTY = "objectDocumentType"; // FIXME: Get from external constant
41     protected final static String COLLECTIONOBJECTS_COMMON_SCHEMA = "collectionobjects_common"; // FIXME: Get from external constant
42     private final static String COLLECTIONOBJECT_DOCTYPE = "CollectionObject"; // FIXME: Get from external constant
43     protected final static String COMPUTED_CURRENT_LOCATION_PROPERTY = "computedCurrentLocation"; // FIXME: Create and then get from external constant
44     protected final static String MOVEMENTS_COMMON_SCHEMA = "movements_common"; // FIXME: Get from external constant
45     private final static String MOVEMENT_DOCTYPE = MovementConstants.NUXEO_DOCTYPE;
46     private final static String LOCATION_DATE_PROPERTY = "locationDate"; // FIXME: Get from external constant
47     protected final static String CURRENT_LOCATION_PROPERTY = "currentLocation"; // FIXME: Get from external constant
48     protected final static String COLLECTIONSPACE_CORE_SCHEMA = "collectionspace_core"; // FIXME: Get from external constant
49     protected final static String CREATED_AT_PROPERTY = "createdAt"; // FIXME: Get from external constant
50     protected final static String UPDATED_AT_PROPERTY = "updatedAt"; // FIXME: Get from external constant
51     private final static String NONVERSIONED_NONPROXY_DOCUMENT_WHERE_CLAUSE_FRAGMENT =
52             "AND ecm:isCheckedInVersion = 0"
53             + "AND ecm:isProxy = 0 ";
54     private final static String ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT =
55             "AND (ecm:currentLifeCycleState <> 'deleted') "
56             + NONVERSIONED_NONPROXY_DOCUMENT_WHERE_CLAUSE_FRAGMENT;
57     private boolean isAboutToBeRemovedEvent = false;
58     private String movementCsidToFilter;
59     private String eventType;
60     
61     public enum EventNotificationDocumentType {
62         // Document type about which we've received a notification
63
64         MOVEMENT, RELATION;
65     }
66
67     @Override
68     public void handleEvent(Event event) throws ClientException {
69
70         logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
71
72         EventContext eventContext = event.getContext();
73         if (eventContext == null) {
74             return;
75         }
76
77         if (!(eventContext instanceof DocumentEventContext)) {
78             return;
79         }
80         DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
81         DocumentModel docModel = docEventContext.getSourceDocument();
82
83         if (logger.isTraceEnabled()) {
84             eventType = event.getName();
85             logger.trace("A(n) " + eventType + " event was received by UpdateObjectLocationOnMove ...");
86         }
87
88         if (eventType.equals(DocumentEventTypes.ABOUT_TO_REMOVE)) {
89             isAboutToBeRemovedEvent = true;
90         }
91
92         // If this document event involves a Relation record, does this pertain to
93         // a relationship between a Movement record and a CollectionObject record?
94         //
95         // If not, we're not interested in processing this document event
96         // in this event handler, as it will have no bearing on updating a
97         // computed current location for a CollectionObject.
98
99         //
100         // (The rest of the code flow below is then identical to that which
101         // is followed when this document event involves a Movement record.)
102         String movementCsid = "";
103         Enum<EventNotificationDocumentType> notificationDocumentType;
104         if (documentMatchesType(docModel, RELATION_DOCTYPE)) {
105             if (logger.isTraceEnabled()) {
106                 logger.trace("An event involving a Relation document was received by UpdateObjectLocationOnMove ...");
107             }
108             // Get a Movement CSID from the Relation record.
109             //
110             // If we can't get it - if this Relation doesn't involve a
111             // Movement - then we don't have a pertinent relation record
112             // that can be processed by this event listener / handler.)
113             movementCsid = getCsidForDesiredDocTypeFromRelation(docModel, MOVEMENT_DOCTYPE, COLLECTIONOBJECT_DOCTYPE);
114             if (Tools.isBlank(movementCsid)) {
115                 logger.warn("Could not obtain CSID for Movement record from document event.");
116                 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
117                 return;
118             }
119             // If this Relation record is about to be (hard) deleted, set aside
120             // its CSID so it can filtered out in all subsequent processing.
121             if (isAboutToBeRemovedEvent) {
122                 movementCsidToFilter = movementCsid;
123             }
124             notificationDocumentType = EventNotificationDocumentType.RELATION;
125         } else if (documentMatchesType(docModel, MOVEMENT_DOCTYPE)) {
126             // Otherwise, get a Movement CSID directly from the Movement record.
127             if (logger.isTraceEnabled()) {
128                 logger.trace("An event involving a Movement document was received by UpdateObjectLocationOnMove ...");
129             }
130             // FIXME: exclude update events for Movement records here, if we can
131             // identify that we'll still be properly handling update events
132             // that include a relations list as part of the update payload,
133             // perhaps because that may trigger a separate event notification.
134             movementCsid = NuxeoUtils.getCsid(docModel);
135             if (Tools.isBlank(movementCsid)) {
136                 logger.warn("Could not obtain CSID for Movement record from document event.");
137                 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
138                 return;
139             }
140             // If this Movement record is about to be (hard) deleted, set aside
141             // its CSID so it can filtered out in all subsequent processing.
142             if (isAboutToBeRemovedEvent) {
143                 movementCsidToFilter = movementCsid;
144             }
145             notificationDocumentType = EventNotificationDocumentType.MOVEMENT;
146         } else {
147             if (logger.isTraceEnabled()) {
148                 logger.trace("This event did not involve a document relevant to UpdateObjectLocationOnMove ...");
149             }
150             return;
151         }
152
153         // Note: currently, all Document lifecycle transitions on
154         // the relevant doctype(s) are handled by this event handler,
155         // not just transitions between 'soft deleted' and active states.
156         //
157         // We are assuming that we'll want to re-compute current locations
158         // for related CollectionObjects on all such transitions, as the
159         // semantics of such transitions are opaque to this event handler,
160         // because arbitrary workflows can be bound to those doctype(s).
161         //
162         // If we need to filter out some of those lifecycle transitions,
163         // such as excluding transitions to the 'locked' workflow state; or,
164         // alternately, if we want to restrict this event handler's
165         // scope to handle only transitions into the 'soft deleted' state,
166         // we can add additional checks for doing so at this point in the code.
167
168         // For debugging
169         if (logger.isTraceEnabled()) {
170             logger.trace("Movement CSID=" + movementCsid);
171             logger.trace("Notification document type=" + notificationDocumentType.name());
172         }
173
174         // All Nuxeo sessions that get passed around to CollectionSpace code need to be
175         // wrapped inside of a CoreSessionWrapper
176         CoreSessionInterface coreSession = new CoreSessionWrapper(docEventContext.getCoreSession());
177         Set<String> collectionObjectCsids = new HashSet<>();
178
179         if (notificationDocumentType == EventNotificationDocumentType.RELATION) {
180             String relatedCollectionObjectCsid =
181                     getCsidForDesiredDocTypeFromRelation(docModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
182             collectionObjectCsids.add(relatedCollectionObjectCsid);
183         } else if (notificationDocumentType == EventNotificationDocumentType.MOVEMENT) {
184             collectionObjectCsids.addAll(getCollectionObjectCsidsRelatedToMovement(movementCsid, coreSession));
185         }
186
187         if (collectionObjectCsids.isEmpty()) {
188             if (logger.isTraceEnabled()) {
189                 logger.trace("Could not obtain any CSIDs of related CollectionObject records.");
190                 logger.trace(NO_FURTHER_PROCESSING_MESSAGE);
191             }
192             return;
193         } else {
194             if (logger.isTraceEnabled()) {
195                 logger.trace("Found " + collectionObjectCsids.size() + " CSID(s) of related CollectionObject records.");
196             }
197         }
198         // Iterate through the list of CollectionObject CSIDs found.
199         // For each CollectionObject, obtain its most recent, related Movement,
200         // and update relevant field(s) with values from that Movement record.
201         DocumentModel collectionObjectDocModel;
202         DocumentModel mostRecentMovementDocModel;
203         for (String collectionObjectCsid : collectionObjectCsids) {
204             if (logger.isTraceEnabled()) {
205                 logger.trace("CollectionObject CSID=" + collectionObjectCsid);
206             }
207             // Verify that the CollectionObject is both retrievable and active.
208             collectionObjectDocModel = getCurrentDocModelFromCsid(coreSession, collectionObjectCsid);
209             if (collectionObjectDocModel == null) {
210                 if (logger.isTraceEnabled()) {
211                     logger.trace("CollectionObject is not current (i.e. is a non-current version), is a proxy, or is unretrievable.");
212                 }
213                 continue;
214             }
215             // Verify that the CollectionObject record is active.
216             if (!isActiveDocument(collectionObjectDocModel)) {
217                 if (logger.isTraceEnabled()) {
218                     logger.trace("CollectionObject is inactive (i.e. deleted or in an otherwise inactive lifestyle state).");
219                 }
220                 continue;
221             }
222             // Get the CollectionObject's most recent, related Movement.
223             mostRecentMovementDocModel = getMostRecentMovement(coreSession, collectionObjectCsid,
224                     isAboutToBeRemovedEvent, movementCsidToFilter);
225             if (mostRecentMovementDocModel == null) {
226                 continue;
227             }
228             // Update the CollectionObject with values from that Movement.
229             collectionObjectDocModel =
230                     updateCollectionObjectValuesFromMovement(collectionObjectDocModel, mostRecentMovementDocModel);
231             if (logger.isTraceEnabled()) {
232                 String computedCurrentLocationRefName =
233                         (String) collectionObjectDocModel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA,
234                         COMPUTED_CURRENT_LOCATION_PROPERTY);
235                 logger.trace("computedCurrentLocation refName after value update=" + computedCurrentLocationRefName);
236             }
237             coreSession.saveDocument(collectionObjectDocModel);
238         }
239     }
240
241     /**
242      * Returns the CSIDs of CollectionObject records that are related to a
243      * Movement record.
244      *
245      * @param movementCsid the CSID of a Movement record.
246      * @param coreSession a repository session.
247      * @throws ClientException
248      * @return the CSIDs of the CollectionObject records, if any, which are
249      * related to the Movement record.
250      */
251     private Set<String> getCollectionObjectCsidsRelatedToMovement(String movementCsid,
252             CoreSessionInterface coreSession) throws ClientException {
253
254         Set<String> csids = new HashSet<>();
255
256         // Via an NXQL query, get a list of active relation records where:
257         // * This movement record's CSID is the subject CSID of the relation,
258         //   and its object document type is a CollectionObject doctype;
259         // or
260         // * This movement record's CSID is the object CSID of the relation,
261         //   and its subject document type is a CollectionObject doctype.
262         //
263         // Some values below are hard-coded for readability, rather than
264         // being obtained from constants.
265         String query = String.format(
266                 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
267                 + "("
268                 + "  (%2$s:subjectCsid = '%3$s' "
269                 + "  AND %2$s:objectDocumentType = '%4$s') "
270                 + " OR "
271                 + "  (%2$s:objectCsid = '%3$s' "
272                 + "  AND %2$s:subjectDocumentType = '%4$s') "
273                 + ")"
274                 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
275                 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, movementCsid, COLLECTIONOBJECT_DOCTYPE);
276         DocumentModelList relationDocModels = coreSession.query(query);
277         if (relationDocModels == null || relationDocModels.isEmpty()) {
278             // Encountering a Movement record that is not related to any
279             // CollectionObject is potentially a normal occurrence, so no
280             // error messages are logged here when we stop handling this event.
281             return csids;
282         }
283         // Iterate through the list of Relation records found and build
284         // a list of CollectionObject CSIDs, by extracting the relevant CSIDs
285         // from those Relation records.
286         String csid;
287         for (DocumentModel relationDocModel : relationDocModels) {
288             csid = getCsidForDesiredDocTypeFromRelation(relationDocModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
289             if (Tools.notBlank(csid)) {
290                 csids.add(csid);
291             }
292         }
293         return csids;
294     }
295
296 // FIXME: Generic methods like many of those below might be split off from
297 // this specific event listener/handler, into an event handler utilities
298 // class, base classes, or otherwise.
299 //
300 // FIXME: Identify whether the equivalent of the documentMatchesType utility
301 // method is already implemented and substitute a call to the latter if so.
302 // This may well already exist.
303     /**
304      * Identifies whether a document matches a supplied document type.
305      *
306      * @param docModel a document model.
307      * @param docType a document type string.
308      * @return true if the document matches the supplied document type; false if
309      * it does not.
310      */
311     protected static boolean documentMatchesType(DocumentModel docModel, String docType) {
312         if (docModel == null || Tools.isBlank(docType)) {
313             return false;
314         }
315         if (docModel.getType().startsWith(docType)) {
316             return true;
317         } else {
318             return false;
319         }
320     }
321
322     /**
323      * Identifies whether a document is an active document; currently, whether
324      * it is not in the 'deleted' workflow state.
325      *
326      * @param docModel
327      * @return true if the document is an active document; false if it is not.
328      */
329     protected static boolean isActiveDocument(DocumentModel docModel) {
330         if (docModel == null) {
331             return false;
332         }
333         boolean isActiveDocument = false;
334         try {
335             if (!docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
336                 isActiveDocument = true;
337             }
338         } catch (ClientException ce) {
339             logger.warn("Error while identifying whether document is an active document: ", ce);
340         }
341         return isActiveDocument;
342     }
343
344     /**
345      * Returns the current document model for a record identified by a CSID.
346      *
347      * Excludes documents which have been versioned (i.e. are a non-current
348      * version of a document), are a proxy for another document, or are
349      * un-retrievable via their CSIDs.
350      *
351      * @param session a repository session.
352      * @param collectionObjectCsid a CollectionObject identifier (CSID)
353      * @return a document model for the document identified by the supplied
354      * CSID.
355      */
356     protected static DocumentModel getCurrentDocModelFromCsid(CoreSessionInterface session, String collectionObjectCsid) {
357         DocumentModelList collectionObjectDocModels = null;
358         try {
359             final String query = "SELECT * FROM "
360                     + NuxeoUtils.BASE_DOCUMENT_TYPE
361                     + " WHERE "
362                     + NuxeoUtils.getByNameWhereClause(collectionObjectCsid)
363                     + " "
364                     + NONVERSIONED_NONPROXY_DOCUMENT_WHERE_CLAUSE_FRAGMENT;
365             collectionObjectDocModels = session.query(query);
366         } catch (Exception e) {
367             logger.warn("Exception in query to get active document model for CollectionObject: ", e);
368         }
369         if (collectionObjectDocModels == null || collectionObjectDocModels.isEmpty()) {
370             logger.warn("Could not get active document models for CollectionObject(s).");
371             return null;
372         } else if (collectionObjectDocModels.size() != 1) {
373             logger.debug("Found more than 1 active document with CSID=" + collectionObjectCsid);
374             return null;
375         }
376         return collectionObjectDocModels.get(0);
377     }
378
379     // FIXME: A quick first pass, using an only partly query-based technique for
380     // getting the most recent Movement record related to a CollectionObject,
381     // augmented by procedural code.
382     //
383     // Could be replaced by a potentially more performant method, based on a query.
384     //
385     // E.g. the following is a sample CMIS query for retrieving Movement records
386     // related to a CollectionObject, which might serve as the basis for that query.
387     /*
388      "SELECT DOC.nuxeo:pathSegment, DOC.dc:title, REL.dc:title,"
389      + "REL.relations_common:objectCsid, REL.relations_common:subjectCsid FROM Movement DOC "
390      + "JOIN Relation REL ON REL.relations_common:objectCsid = DOC.nuxeo:pathSegment "
391      + "WHERE REL.relations_common:subjectCsid = '5b4c617e-53a0-484b-804e' "
392      + "AND DOC.nuxeo:isVersion = false "
393      + "ORDER BY DOC.collectionspace_core:updatedAt DESC";
394      */
395     /**
396      * Returns the most recent Movement record related to a CollectionObject.
397      *
398      * This method currently returns the related Movement record with the latest
399      * (i.e. most recent in time) Location Date field value.
400      *
401      * @param session a repository session.
402      * @param collectionObjectCsid a CollectionObject identifier (CSID)
403      * @param isAboutToBeRemovedEvent whether the current event involves a
404      * record that is slated for removal (hard deletion)
405      * @param movementCsidToFilter the CSID of a Movement record slated for
406      * deletion. This record should be filtered out, prior to returning the most
407      * recent Movement record.
408      * @throws ClientException
409      * @return the most recent Movement record related to the CollectionObject
410      * identified by the supplied CSID.
411      */
412     protected static DocumentModel getMostRecentMovement(CoreSession session, String collectionObjectCsid,
413             boolean isAboutToBeRemovedEvent, String aboutToBeRemovedMovementCsidToFilter)
414             throws ClientException {
415         DocumentModel mostRecentMovementDocModel = null;
416         // Get Relation records for Movements related to this CollectionObject.
417         //
418         // Some values below are hard-coded for readability, rather than
419         // being obtained from constants.
420         String query = String.format(
421                 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
422                 + "("
423                 + "  (%2$s:subjectCsid = '%3$s' "
424                 + "  AND %2$s:objectDocumentType = '%4$s') "
425                 + " OR "
426                 + "  (%2$s:objectCsid = '%3$s' "
427                 + "  AND %2$s:subjectDocumentType = '%4$s') "
428                 + ")"
429                 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
430                 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, collectionObjectCsid, MOVEMENT_DOCTYPE);
431         if (logger.isTraceEnabled()) {
432             logger.trace("query=" + query);
433         }
434         DocumentModelList relationDocModels = session.query(query);
435         if (relationDocModels == null || relationDocModels.isEmpty()) {
436             logger.warn("Unexpectedly found no relations to Movement records to/from to this CollectionObject record.");
437             return mostRecentMovementDocModel;
438         } else {
439             if (logger.isTraceEnabled()) {
440                 logger.trace("Found " + relationDocModels.size() + " relations to Movement records to/from this CollectionObject record.");
441             }
442         }
443         // Iterate through related movement records, to find the related
444         // Movement record with the most recent location date.
445         GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
446         // Note: the following value is used to compare any two records, rather
447         // than to identify the most recent value so far encountered. Thus, it may
448         // occasionally be set backward or forward in time, within the loop below.
449         GregorianCalendar comparisonUpdatedDate = EARLIEST_COMPARISON_DATE;
450         DocumentModel movementDocModel;
451         Set<String> alreadyProcessedMovementCsids = new HashSet<>();
452         String relatedMovementCsid;
453         for (DocumentModel relationDocModel : relationDocModels) {
454             // Due to the 'OR' operator in the query above, related Movement
455             // record CSIDs may reside in either the subject or object CSID fields
456             // of the relation record. Whichever CSID value doesn't match the
457             // CollectionObject's CSID is inferred to be the related Movement record's CSID.
458             relatedMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
459             if (relatedMovementCsid.equals(collectionObjectCsid)) {
460                 relatedMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
461             }
462             if (Tools.isBlank(relatedMovementCsid)) {
463                 continue;
464             }
465             // Because of the OR clause used in the query above, there may be
466             // two or more Relation records returned in the query results that
467             // reference the same Movement record, as either the subject
468             // or object of a relation to the same CollectionObject record;
469             // we need to filter out those duplicates.
470             if (alreadyProcessedMovementCsids.contains(relatedMovementCsid)) {
471                 continue;
472             } else {
473                 alreadyProcessedMovementCsids.add(relatedMovementCsid);
474             }
475             if (logger.isTraceEnabled()) {
476                 logger.trace("Related movement CSID=" + relatedMovementCsid);
477             }
478             // If our event involves a Movement record that is about to be
479             // (hard) deleted, filter out that record.
480             if (isAboutToBeRemovedEvent && Tools.notBlank(aboutToBeRemovedMovementCsidToFilter)) {
481                 if (relatedMovementCsid.equals(aboutToBeRemovedMovementCsidToFilter)) {
482                     if (logger.isTraceEnabled()) {
483                         logger.trace("Skipping about-to-be-deleted Movement record or referenced, related Movement record ...");
484                     }
485                     continue;
486                 }
487             }
488             movementDocModel = getCurrentDocModelFromCsid(session, relatedMovementCsid);
489             if (movementDocModel == null) {
490                 if (logger.isTraceEnabled()) {
491                     logger.trace("Movement is not current (i.e. is a non-current version), is a proxy, or is unretrievable.");
492                 }
493                 continue;
494             }
495             // Verify that the Movement record is active.
496             if (!isActiveDocument(movementDocModel)) {
497                 if (logger.isTraceEnabled()) {
498                     logger.trace("Movement is inactive (i.e. is deleted or in another inactive lifestyle state).");
499                 }
500                 continue;
501             }
502             GregorianCalendar locationDate =
503                     (GregorianCalendar) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
504             // If the current Movement record lacks a location date, it cannot
505             // be established as the most recent Movement record; skip over it.
506             if (locationDate == null) {
507                 continue;
508             }
509             GregorianCalendar updatedDate =
510                     (GregorianCalendar) movementDocModel.getProperty(COLLECTIONSPACE_CORE_SCHEMA, UPDATED_AT_PROPERTY);
511             if (locationDate.after(mostRecentLocationDate)) {
512                 mostRecentLocationDate = locationDate;
513                 if (updatedDate != null) {
514                     comparisonUpdatedDate = updatedDate;
515                 }
516                 mostRecentMovementDocModel = movementDocModel;
517                 // If the current Movement record's location date is identical
518                 // to that of the (at this time) most recent Movement record, then
519                 // instead compare the two records using their update date values
520             } else if (locationDate.compareTo(mostRecentLocationDate) == 0) {
521                 if (updatedDate != null && updatedDate.after(comparisonUpdatedDate)) {
522                     // The most recent location date value doesn't need to be
523                     // updated here, as the two records' values are identical
524                     comparisonUpdatedDate = updatedDate;
525                     mostRecentMovementDocModel = movementDocModel;
526                 }
527             }
528         }
529         return mostRecentMovementDocModel;
530     }
531
532     /**
533      * Returns the CSID for a desired document type from a Relation record,
534      * where the relationship involves two specified, different document types.
535      *
536      * @param relationDocModel a document model for a Relation record.
537      * @param desiredDocType a desired document type.
538      * @param relatedDocType a related document type.
539      * @throws ClientException
540      * @return the CSID from the desired document type in the relation. Returns
541      * an empty string if the Relation record does not involve both the desired
542      * and related document types, or if the desired document type is at both
543      * ends of the relation.
544      */
545     protected static String getCsidForDesiredDocTypeFromRelation(DocumentModel relationDocModel,
546             String desiredDocType, String relatedDocType) throws ClientException {
547         String csid = "";
548         String subjectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
549         String objectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
550         if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(desiredDocType)) {
551             return csid;
552         }
553         if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(relatedDocType)) {
554             csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
555         } else if (subjectDocType.startsWith(relatedDocType) && objectDocType.startsWith(desiredDocType)) {
556             csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
557         }
558         return csid;
559     }
560
561     // The following method can be extended by sub-classes to update
562     // different/multiple values; e.g. values for moveable locations ("crates").
563     /**
564      * Updates a CollectionObject record with selected values from a Movement
565      * record.
566      *
567      * @param collectionObjectDocModel a document model for a CollectionObject
568      * record.
569      * @param movementDocModel a document model for a Movement record.
570      * @return a potentially updated document model for the CollectionObject
571      * record.
572      * @throws ClientException
573      */
574     protected abstract DocumentModel updateCollectionObjectValuesFromMovement(DocumentModel collectionObjectDocModel,
575             DocumentModel movementDocModel)
576             throws ClientException;
577 }