1 package org.collectionspace.services.listener;
3 import java.util.GregorianCalendar;
4 import java.util.HashSet;
7 import org.apache.commons.logging.Log;
8 import org.apache.commons.logging.LogFactory;
9 import org.collectionspace.services.client.AbstractCommonListUtils;
10 import org.collectionspace.services.client.workflow.WorkflowClient;
11 import org.collectionspace.services.common.api.Tools;
12 import org.collectionspace.services.common.api.Tools.NoRelatedRecordsException;
13 import org.collectionspace.services.movement.nuxeo.MovementConstants;
14 import org.collectionspace.services.nuxeo.client.java.CoreSessionInterface;
15 import org.collectionspace.services.nuxeo.client.java.CoreSessionWrapper;
16 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
18 import org.nuxeo.ecm.core.api.ClientException;
19 import org.nuxeo.ecm.core.api.DocumentModel;
20 import org.nuxeo.ecm.core.api.DocumentModelList;
21 import org.nuxeo.ecm.core.api.event.DocumentEventTypes;
22 import org.nuxeo.ecm.core.api.impl.DocumentModelImpl;
23 import org.nuxeo.ecm.core.api.impl.DocumentModelListImpl;
24 import org.nuxeo.ecm.core.event.Event;
25 import org.nuxeo.ecm.core.event.EventContext;
26 import org.nuxeo.ecm.core.event.EventListener;
27 import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
29 public abstract class AbstractUpdateObjectLocationValues implements EventListener {
31 // FIXME: We might experiment here with using log4j instead of Apache Commons Logging;
32 // am using the latter to follow Ray's pattern for now
33 private final static Log logger = LogFactory.getLog(AbstractUpdateObjectLocationValues.class);
34 // FIXME: Make the following message, or its equivalent, a constant usable by all event listeners
35 private final static String NO_FURTHER_PROCESSING_MESSAGE =
36 "This event listener will not continue processing this event ...";
37 private final static GregorianCalendar EARLIEST_COMPARISON_DATE = new GregorianCalendar(1600, 1, 1);
38 private final static String RELATIONS_COMMON_SCHEMA = "relations_common"; // FIXME: Get from external constant
39 private final static String RELATION_DOCTYPE = "Relation"; // FIXME: Get from external constant
40 private final static String SUBJECT_CSID_PROPERTY = "subjectCsid"; // FIXME: Get from external constant
41 private final static String OBJECT_CSID_PROPERTY = "objectCsid"; // FIXME: Get from external constant
42 private final static String SUBJECT_DOCTYPE_PROPERTY = "subjectDocumentType"; // FIXME: Get from external constant
43 private final static String OBJECT_DOCTYPE_PROPERTY = "objectDocumentType"; // FIXME: Get from external constant
44 protected final static String COLLECTIONOBJECTS_COMMON_SCHEMA = "collectionobjects_common"; // FIXME: Get from external constant
45 private final static String COLLECTIONOBJECT_DOCTYPE = "CollectionObject"; // FIXME: Get from external constant
46 protected final static String COMPUTED_CURRENT_LOCATION_PROPERTY = "computedCurrentLocation"; // FIXME: Create and then get from external constant
47 private final static String CURRENT_LOCATION_ELEMENT_NAME = "currentLocation"; // From movement_commons schema. FIXME: Get from external constant that already exists
48 protected final static String MOVEMENTS_COMMON_SCHEMA = "movements_common"; // FIXME: Get from external constant
49 private final static String MOVEMENT_DOCTYPE = MovementConstants.NUXEO_DOCTYPE;
50 private final static String LOCATION_DATE_PROPERTY = "locationDate"; // FIXME: Get from external constant
51 protected final static String CURRENT_LOCATION_PROPERTY = "currentLocation"; // FIXME: Get from external constant
52 protected final static String COLLECTIONSPACE_CORE_SCHEMA = "collectionspace_core"; // FIXME: Get from external constant
53 protected final static String CREATED_AT_PROPERTY = "createdAt"; // FIXME: Get from external constant
54 protected final static String UPDATED_AT_PROPERTY = "updatedAt"; // FIXME: Get from external constant
55 private final static String NONVERSIONED_NONPROXY_DOCUMENT_WHERE_CLAUSE_FRAGMENT =
56 "AND ecm:isCheckedInVersion = 0"
57 + " AND ecm:isProxy = 0 ";
58 private final static String ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT =
59 "AND (ecm:currentLifeCycleState <> 'deleted') "
60 + NONVERSIONED_NONPROXY_DOCUMENT_WHERE_CLAUSE_FRAGMENT;
62 public enum EventNotificationDocumentType {
63 // Document type about which we've received a notification
69 public void handleEvent(Event event) throws ClientException {
71 boolean isAboutToBeRemovedEvent = false;
72 String movementCsidToFilter = "";
73 String eventType = "";
75 logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
77 EventContext eventContext = event.getContext();
78 if (eventContext == null) {
82 if (!(eventContext instanceof DocumentEventContext)) {
85 DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
86 DocumentModel docModel = docEventContext.getSourceDocument();
88 eventType = event.getName();
89 if (logger.isTraceEnabled()) {
90 logger.trace("A(n) " + eventType + " event was received by UpdateObjectLocationOnMove ...");
92 if (eventType.equals(DocumentEventTypes.ABOUT_TO_REMOVE)) {
93 isAboutToBeRemovedEvent = true;
96 // If this document event involves a Relation record, does this pertain to
97 // a relationship between a Movement record and a CollectionObject record?
99 // If not, we're not interested in processing this document event
100 // in this event handler, as it will have no bearing on updating a
101 // computed current location for a CollectionObject.
104 // (The rest of the code flow below is then identical to that which
105 // is followed when this document event involves a Movement record.)
106 String movementCsid = "";
107 Enum<EventNotificationDocumentType> notificationDocumentType;
108 if (documentMatchesType(docModel, RELATION_DOCTYPE)) {
109 if (logger.isTraceEnabled()) {
110 logger.trace("An event involving a Relation document was received by UpdateObjectLocationOnMove ...");
112 // Get a Movement CSID from the Relation record.
114 // If we can't get it - if this Relation doesn't involve a
115 // Movement - then we don't have a pertinent relation record
116 // that can be processed by this event listener / handler.)
117 movementCsid = getCsidForDesiredDocTypeFromRelation(docModel, MOVEMENT_DOCTYPE, COLLECTIONOBJECT_DOCTYPE);
118 if (Tools.isBlank(movementCsid)) {
119 logger.warn("Could not obtain CSID for Movement record from document event.");
120 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
123 // If this Relation record is about to be (hard) deleted, set aside
124 // the CSID for the Movement record to which it pertains, so it can
125 // be filtered out in all subsequent processing of the pertinent
126 // Cataloging record's computed current location.
127 if (isAboutToBeRemovedEvent) {
128 movementCsidToFilter = movementCsid;
130 notificationDocumentType = EventNotificationDocumentType.RELATION;
131 } else if (documentMatchesType(docModel, MOVEMENT_DOCTYPE)) {
132 // Otherwise, get a Movement CSID directly from the Movement record.
133 if (logger.isTraceEnabled()) {
134 logger.trace("An event involving a Movement document was received by UpdateObjectLocationOnMove ...");
136 // FIXME: exclude update events for Movement records here, if we can
137 // identify that we'll still be properly handling update events
138 // that include a relations list as part of the update payload,
139 // perhaps because that may trigger a separate event notification.
140 movementCsid = NuxeoUtils.getCsid(docModel);
141 if (Tools.isBlank(movementCsid)) {
142 logger.warn("Could not obtain CSID for Movement record from document event.");
143 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
146 // If this Movement record is about to be (hard) deleted, set aside
147 // its CSID so it can be filtered out in all subsequent processing
148 // of the pertinent Cataloging record's computed current location.
149 if (isAboutToBeRemovedEvent) {
150 movementCsidToFilter = movementCsid;
152 notificationDocumentType = EventNotificationDocumentType.MOVEMENT;
154 if (logger.isTraceEnabled()) {
155 logger.trace("This event did not involve a document relevant to UpdateObjectLocationOnMove ...");
160 // Note: currently, all Document lifecycle transitions on
161 // the relevant doctype(s) are handled by this event handler,
162 // not just transitions between 'soft deleted' and active states.
164 // We are assuming that we'll want to re-compute current locations
165 // for related CollectionObjects on all such transitions, as the
166 // semantics of such transitions are opaque to this event handler,
167 // because arbitrary workflows can be bound to those doctype(s).
169 // If we need to filter out some of those lifecycle transitions,
170 // such as excluding transitions to the 'locked' workflow state; or,
171 // alternately, if we want to restrict this event handler's
172 // scope to handle only transitions into the 'soft deleted' state,
173 // we can add additional checks for doing so at this point in the code.
176 if (logger.isTraceEnabled()) {
177 logger.trace("Movement CSID=" + movementCsid);
178 logger.trace("Notification document type=" + notificationDocumentType.name());
181 // All Nuxeo sessions that get passed around to CollectionSpace code need to be
182 // wrapped inside of a CoreSessionWrapper
183 CoreSessionInterface coreSession = new CoreSessionWrapper(docEventContext.getCoreSession());
184 Set<String> collectionObjectCsids = new HashSet<>();
186 if (notificationDocumentType == EventNotificationDocumentType.RELATION) {
187 String relatedCollectionObjectCsid =
188 getCsidForDesiredDocTypeFromRelation(docModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
189 collectionObjectCsids.add(relatedCollectionObjectCsid);
190 } else if (notificationDocumentType == EventNotificationDocumentType.MOVEMENT) {
191 collectionObjectCsids.addAll(getCollectionObjectCsidsRelatedToMovement(movementCsid, coreSession));
194 if (collectionObjectCsids.isEmpty()) {
195 if (logger.isTraceEnabled()) {
196 logger.trace("Could not obtain any CSIDs of related CollectionObject records.");
197 logger.trace(NO_FURTHER_PROCESSING_MESSAGE);
201 if (logger.isTraceEnabled()) {
202 logger.trace("Found " + collectionObjectCsids.size() + " CSID(s) of related CollectionObject records.");
205 // Iterate through the list of CollectionObject CSIDs found.
206 // For each CollectionObject, obtain its most recent, related Movement,
207 // and update relevant field(s) with values from that Movement record.
208 DocumentModel collectionObjectDocModel;
209 DocumentModel mostRecentMovementDocModel;
210 for (String collectionObjectCsid : collectionObjectCsids) {
211 if (logger.isTraceEnabled()) {
212 logger.trace("CollectionObject CSID=" + collectionObjectCsid);
214 // Verify that the CollectionObject is both retrievable and active.
215 collectionObjectDocModel = getCurrentDocModelFromCsid(coreSession, collectionObjectCsid);
216 if (collectionObjectDocModel == null) {
217 if (logger.isTraceEnabled()) {
218 logger.trace("CollectionObject is not current (i.e. is a non-current version), is a proxy, or is unretrievable.");
222 // Verify that the CollectionObject record is active.
223 if (!isActiveDocument(collectionObjectDocModel)) {
224 if (logger.isTraceEnabled()) {
225 logger.trace("CollectionObject is inactive (i.e. deleted or in an otherwise inactive lifestyle state).");
229 // Get the CollectionObject's most recent, related Movement.
232 mostRecentMovementDocModel = getMostRecentMovement(coreSession, collectionObjectCsid,
233 isAboutToBeRemovedEvent, movementCsidToFilter);
234 if (mostRecentMovementDocModel == null) {
235 // This means we couldn't figure out which Movement record to use.
238 } catch (NoRelatedRecordsException e) {
239 // This means there were NO active Movement records to use.
240 mostRecentMovementDocModel = new DocumentModelImpl(MOVEMENT_DOCTYPE); // Create an empty document model
243 // Update the CollectionObject with values from that Movement.
244 collectionObjectDocModel =
245 updateCollectionObjectValuesFromMovement(collectionObjectDocModel, mostRecentMovementDocModel);
246 if (logger.isTraceEnabled()) {
247 String computedCurrentLocationRefName =
248 (String) collectionObjectDocModel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA,
249 COMPUTED_CURRENT_LOCATION_PROPERTY);
250 logger.trace("computedCurrentLocation refName after value update=" + computedCurrentLocationRefName);
253 coreSession.saveDocument(collectionObjectDocModel);
258 * Returns the CSIDs of CollectionObject records that are related to a
261 * @param movementCsid the CSID of a Movement record.
262 * @param coreSession a repository session.
263 * @throws ClientException
264 * @return the CSIDs of the CollectionObject records, if any, which are
265 * related to the Movement record.
267 private Set<String> getCollectionObjectCsidsRelatedToMovement(String movementCsid,
268 CoreSessionInterface coreSession) throws ClientException {
270 Set<String> csids = new HashSet<>();
272 // Via an NXQL query, get a list of active relation records where:
273 // * This movement record's CSID is the subject CSID of the relation,
274 // and its object document type is a CollectionObject doctype;
276 // * This movement record's CSID is the object CSID of the relation,
277 // and its subject document type is a CollectionObject doctype.
279 // Some values below are hard-coded for readability, rather than
280 // being obtained from constants.
281 String query = String.format(
282 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
284 + " (%2$s:subjectCsid = '%3$s' "
285 + " AND %2$s:objectDocumentType = '%4$s') "
287 + " (%2$s:objectCsid = '%3$s' "
288 + " AND %2$s:subjectDocumentType = '%4$s') "
290 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
291 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, movementCsid, COLLECTIONOBJECT_DOCTYPE);
292 DocumentModelList relationDocModels = coreSession.query(query);
293 if (relationDocModels == null || relationDocModels.isEmpty()) {
294 // Encountering a Movement record that is not related to any
295 // CollectionObject is potentially a normal occurrence, so no
296 // error messages are logged here when we stop handling this event.
299 // Iterate through the list of Relation records found and build
300 // a list of CollectionObject CSIDs, by extracting the relevant CSIDs
301 // from those Relation records.
303 for (DocumentModel relationDocModel : relationDocModels) {
304 csid = getCsidForDesiredDocTypeFromRelation(relationDocModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
305 if (Tools.notBlank(csid)) {
312 // FIXME: Generic methods like many of those below might be split off from
313 // this specific event listener/handler, into an event handler utilities
314 // class, base classes, or otherwise.
316 // FIXME: Identify whether the equivalent of the documentMatchesType utility
317 // method is already implemented and substitute a call to the latter if so.
318 // This may well already exist.
320 * Identifies whether a document matches a supplied document type.
322 * @param docModel a document model.
323 * @param docType a document type string.
324 * @return true if the document matches the supplied document type; false if
327 protected static boolean documentMatchesType(DocumentModel docModel, String docType) {
328 if (docModel == null || Tools.isBlank(docType)) {
331 if (docModel.getType().startsWith(docType)) {
339 * Identifies whether a document is an active document; currently, whether
340 * it is not in a 'deleted' workflow state.
343 * @return true if the document is an active document; false if it is not.
345 protected static boolean isActiveDocument(DocumentModel docModel) {
346 if (docModel == null) {
349 boolean isActiveDocument = false;
351 if (!docModel.getCurrentLifeCycleState().contains(WorkflowClient.WORKFLOWSTATE_DELETED)) {
352 isActiveDocument = true;
354 } catch (ClientException ce) {
355 logger.warn("Error while identifying whether document is an active document: ", ce);
357 return isActiveDocument;
361 * Returns the current document model for a record identified by a CSID.
363 * Excludes documents which have been versioned (i.e. are a non-current
364 * version of a document), are a proxy for another document, or are
365 * un-retrievable via their CSIDs.
367 * @param session a repository session.
368 * @param collectionObjectCsid a CollectionObject identifier (CSID)
369 * @return a document model for the document identified by the supplied
372 protected static DocumentModel getCurrentDocModelFromCsid(CoreSessionInterface session, String collectionObjectCsid) {
373 DocumentModelList docModelList = null;
375 final String query = "SELECT * FROM "
376 + NuxeoUtils.BASE_DOCUMENT_TYPE
378 + NuxeoUtils.getByNameWhereClause(collectionObjectCsid)
380 + NONVERSIONED_NONPROXY_DOCUMENT_WHERE_CLAUSE_FRAGMENT;
381 docModelList = session.query(query);
382 } catch (Exception e) {
383 logger.warn("Exception in query to get active document model for CollectionObject: ", e);
385 if (docModelList == null || docModelList.isEmpty()) {
386 logger.warn("Could not get active document models for CollectionObject(s).");
388 } else if (docModelList.size() != 1) {
389 logger.debug("Found more than 1 active document with CSID=" + collectionObjectCsid);
392 return docModelList.get(0);
395 // FIXME: A quick first pass, using an only partly query-based technique for
396 // getting the most recent Movement record related to a CollectionObject,
397 // augmented by procedural code.
399 // Could be replaced by a potentially more performant method, based on a query.
401 // E.g. the following is a sample CMIS query for retrieving Movement records
402 // related to a CollectionObject, which might serve as the basis for that query.
404 "SELECT DOC.nuxeo:pathSegment, DOC.dc:title, REL.dc:title,"
405 + "REL.relations_common:objectCsid, REL.relations_common:subjectCsid FROM Movement DOC "
406 + "JOIN Relation REL ON REL.relations_common:objectCsid = DOC.nuxeo:pathSegment "
407 + "WHERE REL.relations_common:subjectCsid = '5b4c617e-53a0-484b-804e' "
408 + "AND DOC.nuxeo:isVersion = false "
409 + "ORDER BY DOC.collectionspace_core:updatedAt DESC";
412 * Returns the most recent Movement record related to a CollectionObject.
414 * This method currently returns the related Movement record with the latest
415 * (i.e. most recent in time) Location Date field value.
417 * @param session a repository session.
418 * @param collectionObjectCsid a CollectionObject identifier (CSID)
419 * @param isAboutToBeRemovedEvent whether the current event involves a
420 * record that is slated for removal (hard deletion)
421 * @param movementCsidToFilter the CSID of a Movement record slated for
422 * deletion, or of a Movement record referenced by a Relation record slated
423 * for deletion. This record should be filtered out, prior to returning the
424 * most recent Movement record.
425 * @throws ClientException
426 * @return the most recent Movement record related to the CollectionObject
427 * identified by the supplied CSID.
429 protected static DocumentModel getMostRecentMovement(CoreSessionInterface session, String collectionObjectCsid,
430 boolean isAboutToBeRemovedEvent, String aboutToBeRemovedMovementCsidToFilter)
431 throws ClientException, Tools.NoRelatedRecordsException {
432 DocumentModel mostRecentMovementDocModel = null;
433 // Get Relation records for Movements related to this CollectionObject.
435 // Some values below are hard-coded for readability, rather than
436 // being obtained from constants.
437 String query = String.format(
438 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
440 + " (%2$s:subjectCsid = '%3$s' "
441 + " AND %2$s:objectDocumentType = '%4$s') "
443 + " (%2$s:objectCsid = '%3$s' "
444 + " AND %2$s:subjectDocumentType = '%4$s') "
446 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
447 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, collectionObjectCsid, MOVEMENT_DOCTYPE);
448 if (logger.isTraceEnabled()) {
449 logger.trace("query=" + query);
451 DocumentModelList relationDocModels = session.query(query);
452 if (relationDocModels == null || relationDocModels.isEmpty()) {
453 logger.warn("Unexpectedly found no relations to Movement records to/from to this CollectionObject record.");
454 return mostRecentMovementDocModel;
456 if (logger.isTraceEnabled()) {
457 logger.trace("Found " + relationDocModels.size() + " relations to Movement records to/from this CollectionObject record.");
462 // Remove redundant document models from the list.
464 relationDocModels = removeRedundantRelations(relationDocModels);
467 // Remove relationships with inactive movement records
469 relationDocModels = removeRelationsWithInactiveMovements(session, relationDocModels);
471 if (relationDocModels == null || relationDocModels.size() == 0) throw new Tools.NoRelatedRecordsException();
474 // If there is only one related movement record, then return it as the most recent
475 // movement record -if it's current location element is not empty.
477 if (relationDocModels.size() == 1) {
478 DocumentModel relationDocModel = relationDocModels.get(0);
479 DocumentModel movementDocModel = getMovementDocModelFromRelation(session, relationDocModel);
480 String currentLocation = (String) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, CURRENT_LOCATION_ELEMENT_NAME);
481 if (Tools.isBlank(currentLocation) || !isActiveDocument(movementDocModel)) { // currentLocation must be set and record must be active
482 movementDocModel = null;
485 return movementDocModel;
488 // Iterate through related movement records, to find the related
489 // Movement record with the most recent location date.
490 GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
491 // Note: the following value is used to compare any two records, rather
492 // than to identify the most recent value so far encountered. Thus, it may
493 // occasionally be set backward or forward in time, within the loop below.
494 GregorianCalendar comparisonUpdatedDate = EARLIEST_COMPARISON_DATE;
495 DocumentModel movementDocModel;
496 Set<String> alreadyProcessedMovementCsids = new HashSet<>();
497 String relatedMovementCsid;
498 for (DocumentModel relationDocModel : relationDocModels) {
499 // Due to the 'OR' operator in the query above, related Movement
500 // record CSIDs may reside in either the subject or object CSID fields
501 // of the relation record. Whichever CSID value doesn't match the
502 // CollectionObject's CSID is inferred to be the related Movement record's CSID.
503 relatedMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
504 if (relatedMovementCsid.equals(collectionObjectCsid)) {
505 relatedMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
507 if (Tools.isBlank(relatedMovementCsid)) {
510 // Because of the OR clause used in the query above, there may be
511 // two or more Relation records returned in the query results that
512 // reference the same Movement record, as either the subject
513 // or object of a relation to the same CollectionObject record;
514 // we need to filter out those duplicates.
515 if (alreadyProcessedMovementCsids.contains(relatedMovementCsid)) {
518 alreadyProcessedMovementCsids.add(relatedMovementCsid);
520 if (logger.isTraceEnabled()) {
521 logger.trace("Related movement CSID=" + relatedMovementCsid);
523 // If our event involves a Movement record that is about to be
524 // (hard) deleted, or a Movement record referenced by a Relation
525 // record that is about to be (hard) deleted, filter out that record.
526 if (isAboutToBeRemovedEvent && Tools.notBlank(aboutToBeRemovedMovementCsidToFilter)) {
527 if (relatedMovementCsid.equals(aboutToBeRemovedMovementCsidToFilter)) {
528 if (logger.isTraceEnabled()) {
529 logger.trace("Skipping about-to-be-deleted Movement record or referenced, related Movement record ...");
534 movementDocModel = getCurrentDocModelFromCsid(session, relatedMovementCsid);
535 if (movementDocModel == null) {
536 if (logger.isTraceEnabled()) {
537 logger.trace("Movement is not current (i.e. is a non-current version), is a proxy, or is unretrievable.");
541 // Verify that the Movement record is active.
542 if (!isActiveDocument(movementDocModel)) {
543 if (logger.isTraceEnabled()) {
544 logger.trace("Movement is inactive (i.e. is deleted or in another inactive lifestyle state).");
548 GregorianCalendar locationDate =
549 (GregorianCalendar) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
550 // If the current Movement record lacks a location date, it cannot
551 // be established as the most recent Movement record; skip over it.
552 if (locationDate == null) {
555 GregorianCalendar updatedDate =
556 (GregorianCalendar) movementDocModel.getProperty(COLLECTIONSPACE_CORE_SCHEMA, UPDATED_AT_PROPERTY);
557 if (locationDate.after(mostRecentLocationDate)) {
558 mostRecentLocationDate = locationDate;
559 if (updatedDate != null) {
560 comparisonUpdatedDate = updatedDate;
562 mostRecentMovementDocModel = movementDocModel;
563 // If the current Movement record's location date is identical
564 // to that of the (at this time) most recent Movement record, then
565 // instead compare the two records using their update date values
566 } else if (locationDate.compareTo(mostRecentLocationDate) == 0) {
567 if (updatedDate != null && updatedDate.after(comparisonUpdatedDate)) {
568 // The most recent location date value doesn't need to be
569 // updated here, as the two records' values are identical
570 comparisonUpdatedDate = updatedDate;
571 mostRecentMovementDocModel = movementDocModel;
576 return mostRecentMovementDocModel;
580 // This method assumes that the relation passed into this method is between a Movement record
581 // and a CollectionObject (cataloging) record.
583 private static DocumentModel getMovementDocModelFromRelation(CoreSessionInterface session, DocumentModel relationDocModel) {
584 String movementCsid = null;
586 String subjectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
587 if (subjectDocType.endsWith(MOVEMENT_DOCTYPE)) {
588 movementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
590 movementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
593 return getCurrentDocModelFromCsid(session, movementCsid);
597 // Compares two Relation document models to see if they're either identical or
598 // reciprocal equivalents.
600 private static boolean compareRelationDocModels(DocumentModel r1, DocumentModel r2) {
601 boolean result = false;
603 String r1_subjectDocType = (String) r1.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
604 String r1_objectDocType = (String) r1.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
605 String r1_subjectCsid = (String) r1.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
606 String r1_objectCsid = (String) r1.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
608 String r2_subjectDocType = (String) r2.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
609 String r2_objectDocType = (String) r2.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
610 String r2_subjectCsid = (String) r2.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
611 String r2_objectCsid = (String) r2.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
613 // Check to see if they're identical
614 if (r1_subjectDocType.equalsIgnoreCase(r2_subjectDocType) && r1_objectDocType.equalsIgnoreCase(r2_objectDocType)
615 && r1_subjectCsid.equalsIgnoreCase(r2_subjectCsid) && r1_objectCsid.equalsIgnoreCase(r2_objectCsid)) {
619 // Check to see if they're reciprocal
620 if (r1_subjectDocType.equalsIgnoreCase(r2_objectDocType) && r1_objectDocType.equalsIgnoreCase(r2_subjectDocType)
621 && r1_subjectCsid.equalsIgnoreCase(r2_objectCsid) && r1_objectCsid.equalsIgnoreCase(r2_subjectCsid)) {
629 // Return a Relation document model list with redundant (either identical or reciprocal) relations removed.
631 private static DocumentModelList removeRedundantRelations(DocumentModelList relationDocModelList) {
632 DocumentModelList resultList = new DocumentModelListImpl();
633 for (DocumentModel relationDocModel : relationDocModelList) {
634 if (existsInResultList(resultList, relationDocModel) == false) {
635 resultList.add(relationDocModel);
638 // TODO Auto-generated method stub
643 // Return just the list of relationships with active Movement records
645 private static DocumentModelList removeRelationsWithInactiveMovements(CoreSessionInterface session, DocumentModelList relationDocModelList) {
646 DocumentModelList resultList = new DocumentModelListImpl();
648 for (DocumentModel relationDocModel : relationDocModelList) {
649 String movementCsid = getCsidForDesiredDocTypeFromRelation(relationDocModel, MOVEMENT_DOCTYPE, COLLECTIONOBJECT_DOCTYPE);
650 DocumentModel movementDocModel = getCurrentDocModelFromCsid(session, movementCsid);
651 if (isActiveDocument(movementDocModel) == true) {
652 resultList.add(relationDocModel);
654 logger.trace(String.format("Disqualified relationship with inactive Movement record=%s.", movementCsid));
663 // Check to see if the Relation (or its equivalent reciprocal) is already in the list.
665 private static boolean existsInResultList(DocumentModelList relationDocModelList, DocumentModel relationDocModel) {
666 boolean result = false;
668 for (DocumentModel target : relationDocModelList) {
669 if (compareRelationDocModels(relationDocModel, target) == true) {
679 * Returns the CSID for a desired document type from a Relation record,
680 * where the relationship involves two specified, different document types.
682 * @param relationDocModel a document model for a Relation record.
683 * @param desiredDocType a desired document type.
684 * @param relatedDocType a related document type.
685 * @throws ClientException
686 * @return the CSID from the desired document type in the relation. Returns
687 * an empty string if the Relation record does not involve both the desired
688 * and related document types, or if the desired document type is at both
689 * ends of the relation.
691 protected static String getCsidForDesiredDocTypeFromRelation(DocumentModel relationDocModel,
692 String desiredDocType, String relatedDocType) throws ClientException {
694 String subjectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
695 String objectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
696 if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(desiredDocType)) {
699 if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(relatedDocType)) {
700 csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
701 } else if (subjectDocType.startsWith(relatedDocType) && objectDocType.startsWith(desiredDocType)) {
702 csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
707 // The following method can be extended by sub-classes to update
708 // different/multiple values; e.g. values for moveable locations ("crates").
710 * Updates a CollectionObject record with selected values from a Movement
713 * @param collectionObjectDocModel a document model for a CollectionObject
715 * @param movementDocModel a document model for a Movement record.
716 * @return a potentially updated document model for the CollectionObject
718 * @throws ClientException
720 protected abstract DocumentModel updateCollectionObjectValuesFromMovement(DocumentModel collectionObjectDocModel,
721 DocumentModel movementDocModel)
722 throws ClientException;