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