1 package org.collectionspace.services.listener;
3 import java.util.GregorianCalendar;
4 import java.util.HashSet;
8 import org.apache.commons.logging.Log;
9 import org.apache.commons.logging.LogFactory;
11 import org.collectionspace.services.client.LocationAuthorityClient;
12 import org.collectionspace.services.client.workflow.WorkflowClient;
13 import org.collectionspace.services.collectionobject.nuxeo.CollectionObjectConstants;
14 import org.collectionspace.services.common.api.Tools;
15 import org.collectionspace.services.common.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;
31 public abstract class AbstractUpdateObjectLocationValues extends AbstractCSEventListenerImpl {
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);
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 ...";
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
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;
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
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();
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;
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";
78 public enum EventNotificationDocumentType {
79 // Document type about which we've received a notification
81 MOVEMENT, RELATION, COLLECTIONOBJECT;
84 private static void logEvent(Event event, String message) {
85 logEvent(event, message, false);
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);
95 logger.debug(String.format("### %s:", message != null ? message : "Unspecified"));
96 logger.debug(String.format("### \t-Event type: %s", eventType));
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()));
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 : ""));
118 logger.debug(String.format("### \t\tdate=<empty>"));
121 logger.debug(String.format("### Ignoring Update Location event: %s", eventType));
127 * Figure out if we should ignore this event.
129 private boolean shouldIgnoreEvent(DocumentEventContext docEventContext, String ignoreEventLabel) {
130 boolean result = false;
132 Boolean shouldIgnoreEvent = (Boolean) docEventContext.getProperties().get(ignoreEventLabel);
133 if (shouldIgnoreEvent != null && shouldIgnoreEvent) {
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);
150 Map<String, String> params = this.getParams(event); // Will be null if no params were configured.
151 logEvent(event, "Update Location");
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);
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) {
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.
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)) {
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);
188 } else if (documentMatchesType(eventDocModel, COLLECTIONOBJECT_DOCTYPE) &&
189 eventType.equals(DocumentEventTypes.DOCUMENT_UPDATED)) {
190 notificationDocumentType = EventNotificationDocumentType.COLLECTIONOBJECT;
192 // We don't need to handle this event.
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.
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).
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.
212 // Get a list of all the CollectionObject records affected by this event.
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));
224 // This event did not involve a document relevant to us.
229 // If we found no collectionobject records needing updating, then we're done.
231 if (collectionObjectCsids.isEmpty() == true) {
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.
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);
247 // Get the CollectionObject's most recent, valid related Movement to use for computing the
248 // object's current location.
250 String mostRecentLocation = getMostRecentLocation(event, session, collectionObjectCsid,
251 isAboutToBeRemovedEvent, eventMovementCsid);
253 // Update the CollectionObject's Computed Current Location field with the Movement record's location
255 boolean didLocationChange = updateCollectionObjectLocation(collectionObjectDocModel, movementDocModel, mostRecentLocation);
258 // If the location changed, save/persist the change to the repository and log the change.
260 if (didLocationChange == true) {
261 persistLocationChange(session, collectionObjectDocModel);
263 // Log an INFO message if we've changed the cataloging record's location
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));
277 // Disable update/documentModified events and persist the location change.
279 private void persistLocationChange(CoreSessionInterface session, DocumentModel collectionObjectDocModel) {
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);
287 // Save/Persist the document to the DB
288 session.saveDocument(collectionObjectDocModel);
291 // Clear the flag we set to ignore events triggered by our save request.
292 clearDocModelContextProperty(collectionObjectDocModel, IGNORE_LOCATION_UPDATE_EVENT_LABEL);
296 * Returns the CSIDs of active CollectionObject records related to a Movement record.
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
305 private Set<String> getCollectionObjectCsidsRelatedToMovement(String movementCsid,
306 CoreSessionInterface coreSession) throws ClientException {
308 Set<String> csids = new HashSet<>();
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;
314 // * This movement record's CSID is the object CSID of the relation,
315 // and its subject document type is a CollectionObject doctype.
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 "
322 + " (%2$s:subjectCsid = '%3$s' "
323 + " AND %2$s:objectDocumentType = '%4$s') "
325 + " (%2$s:objectCsid = '%3$s' "
326 + " AND %2$s:subjectDocumentType = '%4$s') "
328 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
329 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, movementCsid, COLLECTIONOBJECT_DOCTYPE);
331 DocumentModelList relationDocModels = null;
333 relationDocModels = coreSession.query(query);
334 } catch (DocumentException e) {
338 if (relationDocModels == null || relationDocModels.isEmpty()) {
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.
346 for (DocumentModel relationDocModel : relationDocModels) {
347 csid = getCsidForDesiredDocTypeFromRelation(relationDocModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
348 if (Tools.notBlank(csid)) {
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.
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.
364 * Identifies whether a document matches a supplied document type.
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
371 protected static boolean documentMatchesType(DocumentModel docModel, String docType) {
372 if (docModel == null || Tools.isBlank(docType)) {
375 if (docModel.getType().startsWith(docType)) {
382 protected static boolean isActiveDocument(DocumentModel docModel) {
383 return isActiveDocument(docModel, false, null);
387 * Identifies whether a document is an active document; currently, whether
388 * it is not in a 'deleted' workflow state.
391 * @return true if the document is an active document; false if it is not.
393 protected static boolean isActiveDocument(DocumentModel docModel, boolean isAboutToBeRemovedEvent, String aboutToBeRemovedCsid) {
394 boolean isActiveDocument = false;
396 if (docModel != null) {
397 if (!docModel.getCurrentLifeCycleState().contains(WorkflowClient.WORKFLOWSTATE_DELETED)) {
398 isActiveDocument = true;
401 // If doc model is the target of the "aboutToBeRemoved" event, mark it as not active.
403 if (isAboutToBeRemovedEvent && Tools.notBlank(aboutToBeRemovedCsid)) {
404 if (NuxeoUtils.getCsid(docModel).equalsIgnoreCase(aboutToBeRemovedCsid)) {
405 isActiveDocument = false;
410 return isActiveDocument;
414 * Returns the current document model for a record identified by a CSID.
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.
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
425 protected static DocumentModel getCurrentDocModelFromCsid(CoreSessionInterface session, String csid) {
426 DocumentModelList docModelList = null;
428 if (Tools.isEmpty(csid)) {
433 final String query = "SELECT * FROM "
434 + NuxeoUtils.BASE_DOCUMENT_TYPE
436 + NuxeoUtils.getByNameWhereClause(csid)
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);
444 if (docModelList == null || docModelList.isEmpty()) {
445 logger.warn("Could not get active document models for CSID=" + csid);
447 } else if (docModelList.size() != 1) {
448 logger.error("Found more than 1 active document with CSID=" + csid);
452 return docModelList.get(0);
456 // Returns true if this event is for the creation of a new relationship record
458 private static boolean isCreatingNewRelationship(Event event) {
459 boolean result = false;
461 DocumentModel docModel = ((DocumentEventContext)event.getContext()).getSourceDocument();
462 if (event.getName().equals(DocumentEventTypes.DOCUMENT_CREATED) && documentMatchesType(docModel, RELATION_DOCTYPE)) {
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.
473 // Could be replaced by a potentially more performant method, based on a query.
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.
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";
486 * Returns the most recent Movement record related to a CollectionObject.
488 * This method currently returns the related Movement record with the latest
489 * (i.e. most recent in time) Location Date field value.
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
504 protected String getMostRecentLocation(Event event,
505 CoreSessionInterface session, String collectionObjectCsid,
506 boolean isAboutToBeRemovedEvent, String eventMovementCsid) throws ClientException {
508 // Assume we can determine the most recent location by creating an indeterminate result
510 String result = INDETERMINATE_LOCATION;
513 // Get active Relation records involving Movement records related to this CollectionObject.
515 String query = String.format(
516 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
518 + " (%2$s:subjectCsid = '%3$s' "
519 + " AND %2$s:objectDocumentType = '%4$s') "
521 + " (%2$s:objectCsid = '%3$s' "
522 + " AND %2$s:subjectDocumentType = '%4$s') "
524 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
525 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, collectionObjectCsid, MOVEMENT_DOCTYPE);
526 logger.trace("query=" + query);
528 DocumentModelList relationDocModels;
530 relationDocModels = session.query(query);
531 } catch (DocumentException e) {
536 if (isCreatingNewRelationship(event) == true) {
537 DocumentModel newRelation = ((DocumentEventContext)event.getContext()).getSourceDocument();
538 relationDocModels.add(newRelation);
542 // Remove redundant document models from the list.
544 relationDocModels = removeRedundantRelations(relationDocModels);
547 // Remove relationships that are with inactive movement records
549 relationDocModels = removeInactiveRelations(session, relationDocModels, isAboutToBeRemovedEvent, eventMovementCsid);
552 // If there are no candidate relationships after we removed the duplicates and inactive ones,
553 // throw an exception.
555 if (relationDocModels == null || relationDocModels.size() == 0) {
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.
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);
568 if (Tools.isBlank(location) == false) {
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));
579 // Iterate through the list (>2) of related movement records, to find the related
580 // Movement record with the most recent location date.
582 GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
583 GregorianCalendar mostRecentUpdatedDate = EARLIEST_COMPARISON_DATE;
585 for (DocumentModel relationDocModel : relationDocModels) {
586 String relatedMovementCsid;
587 DocumentModel movementDocModel;
589 // The movement record is either the subject or object of the relationship, but not both.
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);
595 movementDocModel = getCurrentDocModelFromCsid(session, relatedMovementCsid);
596 String location = (String) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, CURRENT_LOCATION_ELEMENT_NAME);
599 // If the current Movement record lacks a location date, it cannot
600 // be established as the most recent Movement record; skip over it.
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));
609 GregorianCalendar updatedDate = (GregorianCalendar) movementDocModel.getProperty(COLLECTIONSPACE_CORE_SCHEMA, UPDATED_AT_PROPERTY);
610 if (locationDate.after(mostRecentLocationDate)) {
611 mostRecentLocationDate = locationDate;
612 mostRecentUpdatedDate = updatedDate;
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;
631 // This method assumes that the relation passed into this method is between a Movement record
632 // and a CollectionObject (cataloging) record.
634 private static DocumentModel getMovementDocModelFromRelation(CoreSessionInterface session, DocumentModel relationDocModel) {
635 String movementCsid = null;
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);
641 movementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
644 return getCurrentDocModelFromCsid(session, movementCsid);
648 // Compares two Relation document models to see if they're either identical or
649 // reciprocal equivalents.
651 private static boolean compareRelationDocModels(DocumentModel r1, DocumentModel r2) {
652 boolean result = false;
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);
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);
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)) {
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)) {
680 // Return a Relation document model list with redundant (either identical or reciprocal) relations removed.
682 private static DocumentModelList removeRedundantRelations(DocumentModelList relationDocModelList) {
683 DocumentModelList resultList = null;
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);
694 // TODO Auto-generated method stub
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.
702 private static DocumentModelList removeInactiveRelations(CoreSessionInterface session,
703 DocumentModelList relationDocModelList,
704 boolean isAboutToBeRemovedEvent,
705 String eventMovementCsid) {
706 DocumentModelList resultList = null;
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);
716 logger.debug(String.format("Disqualified relationship=%s with Movement record=%s from current location computation.",
717 NuxeoUtils.getCsid(relationDocModel), movementCsid));
727 // Check to see if the Relation (or its equivalent reciprocal) is already in the list.
729 private static boolean existsInResultList(DocumentModelList relationDocModelList, DocumentModel relationDocModel) {
730 boolean result = false;
732 for (DocumentModel target : relationDocModelList) {
733 if (compareRelationDocModels(relationDocModel, target) == true) {
743 * Returns the CSID for a desired document type from a Relation record,
744 * where the relationship involves two specified document types.
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
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.
755 protected static String getCsidForDesiredDocTypeFromRelation(DocumentModel relationDocModel,
756 String desiredDocType, String relatedDocType) {
758 String subjectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
759 String objectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
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);
770 // The following method can be extended by sub-classes to update
771 // different/multiple values; e.g. values for moveable locations ("crates").
773 * Updates a CollectionObject record with selected values from a Movement
776 * @param collectionObjectDocModel a document model for a CollectionObject
778 * @param movementDocModel a document model for a Movement record.
779 * @return a potentially updated document model for the CollectionObject
781 * @throws ClientException
783 protected abstract boolean updateCollectionObjectLocation(DocumentModel collectionObjectDocModel,
784 DocumentModel movmentDocModel,
785 String movementRecordsLocation);