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