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