]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
cbb23bd1c89e2164c398528029a134a68dbfee22
[tmp/jakarta-migration.git] /
1 package org.collectionspace.services.listener;
2
3 import java.util.GregorianCalendar;
4 import java.util.HashSet;
5 import java.util.Set;
6
7 import org.apache.commons.logging.Log;
8 import org.apache.commons.logging.LogFactory;
9
10 import org.collectionspace.services.client.workflow.WorkflowClient;
11 import org.collectionspace.services.common.api.Tools;
12 import org.collectionspace.services.movement.nuxeo.MovementConstants;
13 import org.collectionspace.services.nuxeo.client.java.CoreSessionInterface;
14 import org.collectionspace.services.nuxeo.client.java.CoreSessionWrapper;
15 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
16
17 import org.nuxeo.ecm.core.api.ClientException;
18 import org.nuxeo.ecm.core.api.DocumentModel;
19 import org.nuxeo.ecm.core.api.DocumentModelList;
20 import org.nuxeo.ecm.core.event.Event;
21 import org.nuxeo.ecm.core.event.EventContext;
22 import org.nuxeo.ecm.core.event.EventListener;
23 import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
24
25 public abstract class AbstractUpdateObjectLocationValues implements EventListener {
26
27     // FIXME: We might experiment here with using log4j instead of Apache Commons Logging;
28     // am using the latter to follow Ray's pattern for now
29     private final static Log logger = LogFactory.getLog(AbstractUpdateObjectLocationValues.class);
30     // FIXME: Make the following message, or its equivalent, a constant usable by all event listeners
31     private final static String NO_FURTHER_PROCESSING_MESSAGE =
32             "This event listener will not continue processing this event ...";
33     private final static GregorianCalendar EARLIEST_COMPARISON_DATE = new GregorianCalendar(1600, 1, 1);
34     private final static String RELATIONS_COMMON_SCHEMA = "relations_common"; // FIXME: Get from external constant
35     private final static String RELATION_DOCTYPE = "Relation"; // FIXME: Get from external constant
36     private final static String SUBJECT_CSID_PROPERTY = "subjectCsid"; // FIXME: Get from external constant
37     private final static String OBJECT_CSID_PROPERTY = "objectCsid"; // FIXME: Get from external constant
38     private final static String SUBJECT_DOCTYPE_PROPERTY = "subjectDocumentType"; // FIXME: Get from external constant
39     private final static String OBJECT_DOCTYPE_PROPERTY = "objectDocumentType"; // FIXME: Get from external constant
40     protected final static String COLLECTIONOBJECTS_COMMON_SCHEMA = "collectionobjects_common"; // FIXME: Get from external constant
41     private final static String COLLECTIONOBJECT_DOCTYPE = "CollectionObject"; // FIXME: Get from external constant
42     protected final static String COMPUTED_CURRENT_LOCATION_PROPERTY = "computedCurrentLocation"; // FIXME: Create and then get from external constant
43     protected final static String MOVEMENTS_COMMON_SCHEMA = "movements_common"; // FIXME: Get from external constant
44     private final static String MOVEMENT_DOCTYPE = MovementConstants.NUXEO_DOCTYPE;
45     private final static String LOCATION_DATE_PROPERTY = "locationDate"; // FIXME: Get from external constant
46     protected final static String CURRENT_LOCATION_PROPERTY = "currentLocation"; // FIXME: Get from external constant
47     protected final static String COLLECTIONSPACE_CORE_SCHEMA = "collectionspace_core"; // FIXME: Get from external constant
48     protected final static String CREATED_AT_PROPERTY = "createdAt"; // FIXME: Get from external constant
49     protected final static String UPDATED_AT_PROPERTY = "updatedAt"; // FIXME: Get from external constant
50     private final static String NONVERSIONED_NONPROXY_DOCUMENT_WHERE_CLAUSE_FRAGMENT =
51             "AND ecm:isCheckedInVersion = 0"
52             + "AND ecm:isProxy = 0 ";
53     private final static String ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT =
54             "AND (ecm:currentLifeCycleState <> 'deleted') "
55             + NONVERSIONED_NONPROXY_DOCUMENT_WHERE_CLAUSE_FRAGMENT;
56
57     public enum EventNotificationDocumentType {
58         // Document type about which we've received a notification
59
60         MOVEMENT, RELATION;
61     }
62
63     @Override
64     public void handleEvent(Event event) throws ClientException {
65
66         logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
67
68         EventContext eventContext = event.getContext();
69         if (eventContext == null) {
70             return;
71         }
72
73         if (!(eventContext instanceof DocumentEventContext)) {
74             return;
75         }
76         DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
77         DocumentModel docModel = docEventContext.getSourceDocument();
78
79         // If this document event involves a Relation record, does this pertain to
80         // a relationship between a Movement record and a CollectionObject record?
81         //
82         // If not, we're not interested in processing this document event
83         // in this event handler, as it will have no bearing on updating a
84         // computed current location for a CollectionObject.
85
86         //
87         // (The rest of the code flow below is then identical to that which
88         // is followed when this document event involves a Movement record.
89         String movementCsid = "";
90         Enum<EventNotificationDocumentType> notificationDocumentType;
91         if (documentMatchesType(docModel, RELATION_DOCTYPE)) {
92             if (logger.isTraceEnabled()) {
93                 logger.trace("An event involving a Relation document was received by UpdateObjectLocationOnMove ...");
94             }
95             // Get a Movement CSID from the Relation record. (If we can't
96             // get it, then we don't have a pertinent relation record.)
97             movementCsid = getCsidForDesiredDocTypeFromRelation(docModel, MOVEMENT_DOCTYPE, COLLECTIONOBJECT_DOCTYPE);
98             if (Tools.isBlank(movementCsid)) {
99                 logger.warn("Could not obtain CSID for Movement record from document event.");
100                 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
101                 return;
102             }
103             notificationDocumentType = EventNotificationDocumentType.RELATION;
104         } else if (documentMatchesType(docModel, MOVEMENT_DOCTYPE)) {
105             // Otherwise, get a Movement CSID directly from the Movement record.
106             if (logger.isTraceEnabled()) {
107                 logger.trace("An event involving a Movement document was received by UpdateObjectLocationOnMove ...");
108             }
109             // FIXME: exclude update events for Movement records here, if we can
110             // identify that we'll still be properly handling update events
111             // that include a relations list as part of the update payload,
112             // perhaps because that may trigger a separate event notification.
113             movementCsid = NuxeoUtils.getCsid(docModel);
114             if (Tools.isBlank(movementCsid)) {
115                 logger.warn("Could not obtain CSID for Movement record from document event.");
116                 logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
117                 return;
118             }
119             notificationDocumentType = EventNotificationDocumentType.MOVEMENT;
120         } else {
121             if (logger.isTraceEnabled()) {
122                 logger.trace("This event did not involve a document relevant to UpdateObjectLocationOnMove ...");
123             }
124             return;
125         }
126
127         // Note: currently, all Document lifecycle transitions on
128         // the relevant doctype(s) are handled by this event handler,
129         // not just transitions between 'soft deleted' and active states.
130         //
131         // We are assuming that we'll want to re-compute current locations
132         // for related CollectionObjects on all such transitions, as the
133         // semantics of such transitions are opaque to this event handler,
134         // because arbitrary workflows can be bound to those doctype(s).
135         //
136         // If we need to filter out some of those lifecycle transitions,
137         // such as excluding transitions to the 'locked' workflow state; or,
138         // alternately, if we want to restrict this event handler's
139         // scope to handle only transitions into the 'soft deleted' state,
140         // we can add additional checks for doing so at this point in the code.
141
142         // For debugging
143         if (logger.isTraceEnabled()) {
144             logger.trace("Movement CSID=" + movementCsid);
145             logger.trace("Notification document type=" + notificationDocumentType.name());
146         }
147
148         // All Nuxeo sessions that get passed around to CollectionSpace code need to be
149         // wrapped inside of a CoreSessionWrapper
150         CoreSessionInterface coreSession = new CoreSessionWrapper(docEventContext.getCoreSession());
151         Set<String> collectionObjectCsids = new HashSet<>();
152
153         if (notificationDocumentType == EventNotificationDocumentType.RELATION) {
154             String relatedCollectionObjectCsid =
155                     getCsidForDesiredDocTypeFromRelation(docModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
156             collectionObjectCsids.add(relatedCollectionObjectCsid);
157         } else if (notificationDocumentType == EventNotificationDocumentType.MOVEMENT) {
158             collectionObjectCsids.addAll(getCollectionObjectCsidsRelatedToMovement(movementCsid, coreSession));
159         }
160
161         if (collectionObjectCsids.isEmpty()) {
162             if (logger.isTraceEnabled()) {
163                 logger.trace("Could not obtain any CSIDs of related CollectionObject records.");
164                 logger.trace(NO_FURTHER_PROCESSING_MESSAGE);
165             }
166             return;
167         } else {
168             if (logger.isTraceEnabled()) {
169                 logger.trace("Found " + collectionObjectCsids.size() + " CSIDs of related CollectionObject records.");
170             }
171         }
172         // Iterate through the list of CollectionObject CSIDs found.
173         // For each CollectionObject, obtain its most recent, related Movement,
174         // and update relevant field(s) with values from that Movement record.
175         DocumentModel collectionObjectDocModel;
176         DocumentModel mostRecentMovementDocModel;
177         for (String collectionObjectCsid : collectionObjectCsids) {
178             if (logger.isTraceEnabled()) {
179                 logger.trace("CollectionObject CSID=" + collectionObjectCsid);
180             }
181             // Verify that the CollectionObject is both retrievable and active.
182             collectionObjectDocModel = getCurrentDocModelFromCsid(coreSession, collectionObjectCsid);
183             if (collectionObjectDocModel == null) {
184                 if (logger.isTraceEnabled()) {
185                     logger.trace("CollectionObject is not current (i.e. is a non-current version), is a proxy, or is unretrievable.");
186                 }
187                 continue;
188             }
189             // Verify that the CollectionObject record is active.
190             if (!isActiveDocument(collectionObjectDocModel)) {
191                 if (logger.isTraceEnabled()) {
192                     logger.trace("CollectionObject is inactive (i.e. deleted or in an otherwise inactive lifestyle state).");
193                 }
194                 continue;
195             }
196             // Get the CollectionObject's most recent, related Movement.
197             mostRecentMovementDocModel = getMostRecentMovement(coreSession, collectionObjectCsid);
198             if (mostRecentMovementDocModel == null) {
199                 continue;
200             }
201             // Update the CollectionObject with values from that Movement.
202             collectionObjectDocModel =
203                     updateCollectionObjectValuesFromMovement(collectionObjectDocModel, mostRecentMovementDocModel);
204             coreSession.saveDocument(collectionObjectDocModel);
205         }
206     }
207
208     /**
209      * Returns the CSIDs of CollectionObject records that are related to a
210      * Movement record.
211      *
212      * @param movementCsid the CSID of a Movement record.
213      * @param coreSession a repository session.
214      * @throws ClientException
215      * @return the CSIDs of the CollectionObject records, if any, which are
216      * related to the Movement record.
217      */
218     private Set<String> getCollectionObjectCsidsRelatedToMovement(String movementCsid,
219             CoreSessionInterface coreSession) throws ClientException {
220
221         Set<String> csids = new HashSet<>();
222
223         // Via an NXQL query, get a list of active relation records where:
224         // * This movement record's CSID is the subject CSID of the relation,
225         //   and its object document type is a CollectionObject doctype;
226         // or
227         // * This movement record's CSID is the object CSID of the relation,
228         //   and its subject document type is a CollectionObject doctype.
229         //
230         // Some values below are hard-coded for readability, rather than
231         // being obtained from constants.
232         String query = String.format(
233                 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
234                 + "("
235                 + "  (%2$s:subjectCsid = '%3$s' "
236                 + "  AND %2$s:objectDocumentType = '%4$s') "
237                 + " OR "
238                 + "  (%2$s:objectCsid = '%3$s' "
239                 + "  AND %2$s:subjectDocumentType = '%4$s') "
240                 + ")"
241                 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
242                 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, movementCsid, COLLECTIONOBJECT_DOCTYPE);
243         DocumentModelList relationDocModels = coreSession.query(query);
244         if (relationDocModels == null || relationDocModels.isEmpty()) {
245             // Encountering a Movement record that is not related to any
246             // CollectionObject is potentially a normal occurrence, so no
247             // error messages are logged here when we stop handling this event.
248             return csids;
249         }
250         // Iterate through the list of Relation records found and build
251         // a list of CollectionObject CSIDs, by extracting the relevant CSIDs
252         // from those Relation records.
253         String csid;
254         for (DocumentModel relationDocModel : relationDocModels) {
255             csid = getCsidForDesiredDocTypeFromRelation(relationDocModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
256             if (Tools.notBlank(csid)) {
257                 csids.add(csid);
258             }
259         }
260         return csids;
261     }
262
263 // FIXME: Generic methods like many of those below might be split off from
264 // this specific event listener/handler, into an event handler utilities
265 // class, base classes, or otherwise.
266 //
267 // FIXME: Identify whether the equivalent of the documentMatchesType utility
268 // method is already implemented and substitute a call to the latter if so.
269 // This may well already exist.
270     /**
271      * Identifies whether a document matches a supplied document type.
272      *
273      * @param docModel a document model.
274      * @param docType a document type string.
275      * @return true if the document matches the supplied document type; false if
276      * it does not.
277      */
278     protected static boolean documentMatchesType(DocumentModel docModel, String docType) {
279         if (docModel == null || Tools.isBlank(docType)) {
280             return false;
281         }
282         if (docModel.getType().startsWith(docType)) {
283             return true;
284         } else {
285             return false;
286         }
287     }
288
289     /**
290      * Identifies whether a document is an active document; currently, whether
291      * it is not in the 'deleted' workflow state.
292      *
293      * @param docModel
294      * @return true if the document is an active document; false if it is not.
295      */
296     protected static boolean isActiveDocument(DocumentModel docModel) {
297         if (docModel == null) {
298             return false;
299         }
300         boolean isActiveDocument = false;
301         try {
302             if (!docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
303                 isActiveDocument = true;
304             }
305         } catch (ClientException ce) {
306             logger.warn("Error while identifying whether document is an active document: ", ce);
307         }
308         return isActiveDocument;
309     }
310
311     /**
312      * Returns the current document model for a record identified by a CSID.
313      *
314      * Excludes documents which have been versioned (i.e. are a non-current
315      * version of a document), are a proxy for another document, or are
316      * un-retrievable via their CSIDs.
317      *
318      * @param session a repository session.
319      * @param collectionObjectCsid a CollectionObject identifier (CSID)
320      * @return a document model for the document identified by the supplied
321      * CSID.
322      */
323     protected static DocumentModel getCurrentDocModelFromCsid(CoreSessionInterface session, String collectionObjectCsid) {
324         DocumentModelList collectionObjectDocModels = null;
325         try {
326             final String query = "SELECT * FROM "
327                     + NuxeoUtils.BASE_DOCUMENT_TYPE
328                     + " WHERE "
329                     + NuxeoUtils.getByNameWhereClause(collectionObjectCsid)
330                     + " "
331                     + NONVERSIONED_NONPROXY_DOCUMENT_WHERE_CLAUSE_FRAGMENT;
332             collectionObjectDocModels = session.query(query);
333         } catch (Exception e) {
334             logger.warn("Exception in query to get active document model for CollectionObject: ", e);
335         }
336         if (collectionObjectDocModels == null || collectionObjectDocModels.isEmpty()) {
337             logger.warn("Could not get active document models for CollectionObject(s).");
338             return null;
339         } else if (collectionObjectDocModels.size() != 1) {
340             logger.debug("Found more than 1 active document with CSID=" + collectionObjectCsid);
341             return null;
342         }
343         return collectionObjectDocModels.get(0);
344     }
345
346     // FIXME: A quick first pass, using an only partly query-based technique for
347     // getting the most recent Movement record related to a CollectionObject,
348     // augmented by procedural code.
349     //
350     // Could be replaced by a potentially more performant method, based on a query.
351     //
352     // E.g. the following is a sample CMIS query for retrieving Movement records
353     // related to a CollectionObject, which might serve as the basis for that query.
354     /*
355      "SELECT DOC.nuxeo:pathSegment, DOC.dc:title, REL.dc:title,"
356      + "REL.relations_common:objectCsid, REL.relations_common:subjectCsid FROM Movement DOC "
357      + "JOIN Relation REL ON REL.relations_common:objectCsid = DOC.nuxeo:pathSegment "
358      + "WHERE REL.relations_common:subjectCsid = '5b4c617e-53a0-484b-804e' "
359      + "AND DOC.nuxeo:isVersion = false "
360      + "ORDER BY DOC.collectionspace_core:updatedAt DESC";
361      */
362     /**
363      * Returns the most recent Movement record related to a CollectionObject.
364      *
365      * This method currently returns the related Movement record with the latest
366      * (i.e. most recent in time) Location Date field value.
367      *
368      * @param session a repository session.
369      * @param collectionObjectCsid a CollectionObject identifier (CSID)
370      * @throws ClientException
371      * @return the most recent Movement record related to the CollectionObject
372      * identified by the supplied CSID.
373      */
374     protected static DocumentModel getMostRecentMovement(CoreSessionInterface session, String collectionObjectCsid)
375             throws ClientException {
376         DocumentModel mostRecentMovementDocModel = null;
377         // Get Relation records for Movements related to this CollectionObject.
378         //
379         // Some values below are hard-coded for readability, rather than
380         // being obtained from constants.
381         String query = String.format(
382                 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
383                 + "("
384                 + "  (%2$s:subjectCsid = '%3$s' "
385                 + "  AND %2$s:objectDocumentType = '%4$s') "
386                 + " OR "
387                 + "  (%2$s:objectCsid = '%3$s' "
388                 + "  AND %2$s:subjectDocumentType = '%4$s') "
389                 + ")"
390                 + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
391                 RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, collectionObjectCsid, MOVEMENT_DOCTYPE);
392         if (logger.isTraceEnabled()) {
393             logger.trace("query=" + query);
394         }
395         DocumentModelList relationDocModels = session.query(query);
396         if (relationDocModels == null || relationDocModels.isEmpty()) {
397             logger.warn("Unexpectedly found no relations to Movement records to/from to this CollectionObject record.");
398             return mostRecentMovementDocModel;
399         } else {
400             if (logger.isTraceEnabled()) {
401                 logger.trace("Found " + relationDocModels.size() + " relations to Movement records to/from this CollectionObject record.");
402             }
403         }
404         // Iterate through related movement records, to find the related
405         // Movement record with the most recent location date.
406         GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
407         // Note: the following value is used to compare any two records, rather
408         // than to identify the most recent value so far encountered. Thus, it may
409         // occasionally be set backward or forward in time, within the loop below.
410         GregorianCalendar comparisonUpdatedDate = EARLIEST_COMPARISON_DATE;
411         DocumentModel movementDocModel;
412         Set<String> alreadyProcessedMovementCsids = new HashSet<>();
413         String relatedMovementCsid;
414         for (DocumentModel relationDocModel : relationDocModels) {
415             // Due to the 'OR' operator in the query above, related Movement
416             // record CSIDs may reside in either the subject or object CSID fields
417             // of the relation record. Whichever CSID value doesn't match the
418             // CollectionObject's CSID is inferred to be the related Movement record's CSID.
419             relatedMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
420             if (relatedMovementCsid.equals(collectionObjectCsid)) {
421                 relatedMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
422             }
423             if (Tools.isBlank(relatedMovementCsid)) {
424                 continue;
425             }
426             // Because of the OR clause used in the query above, there may be
427             // two or more Relation records returned in the query results that
428             // reference the same Movement record, as either the subject
429             // or object of a relation to the same CollectionObject record;
430             // we need to filter out those duplicates.
431             if (alreadyProcessedMovementCsids.contains(relatedMovementCsid)) {
432                 continue;
433             } else {
434                 alreadyProcessedMovementCsids.add(relatedMovementCsid);
435             }
436             if (logger.isTraceEnabled()) {
437                 logger.trace("Related movement CSID=" + relatedMovementCsid);
438             }
439             movementDocModel = getCurrentDocModelFromCsid(session, relatedMovementCsid);
440             if (movementDocModel == null) {
441                 if (logger.isTraceEnabled()) {
442                     logger.trace("Movement is not current (i.e. is a non-current version), is a proxy, or is unretrievable.");
443                 }
444                 continue;
445             }
446             // Verify that the Movement record is active.
447             if (!isActiveDocument(movementDocModel)) {
448                 if (logger.isTraceEnabled()) {
449                     logger.trace("Movement is inactive (i.e. is deleted or in another inactive lifestyle state).");
450                 }
451                 continue;
452             }
453             GregorianCalendar locationDate =
454                     (GregorianCalendar) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
455             // If the current Movement record lacks a location date, it cannot
456             // be established as the most recent Movement record; skip over it.
457             if (locationDate == null) {
458                 continue;
459             }
460             GregorianCalendar updatedDate =
461                     (GregorianCalendar) movementDocModel.getProperty(COLLECTIONSPACE_CORE_SCHEMA, UPDATED_AT_PROPERTY);
462             if (locationDate.after(mostRecentLocationDate)) {
463                 mostRecentLocationDate = locationDate;
464                 if (updatedDate != null) {
465                     comparisonUpdatedDate = updatedDate;
466                 }
467                 mostRecentMovementDocModel = movementDocModel;
468                 // If the current Movement record's location date is identical
469                 // to that of the (at this time) most recent Movement record, then
470                 // instead compare the two records using their update date values
471             } else if (locationDate.compareTo(mostRecentLocationDate) == 0) {
472                 if (updatedDate != null && updatedDate.after(comparisonUpdatedDate)) {
473                     // The most recent location date value doesn't need to be
474                     // updated here, as the two records' values are identical
475                     comparisonUpdatedDate = updatedDate;
476                     mostRecentMovementDocModel = movementDocModel;
477                 }
478             }
479         }
480         return mostRecentMovementDocModel;
481     }
482
483     /**
484      * Returns the CSID for a desired document type from a Relation record,
485      * where the relationship involves two specified, different document types.
486      *
487      * @param relationDocModel a document model for a Relation record.
488      * @param desiredDocType a desired document type.
489      * @param relatedDocType a related document type.
490      * @throws ClientException
491      * @return the CSID from the desired document type in the relation. Returns
492      * an empty string if the Relation record does not involve both the desired
493      * and related document types, or if the desired document type is at both
494      * ends of the relation.
495      */
496     protected static String getCsidForDesiredDocTypeFromRelation(DocumentModel relationDocModel,
497             String desiredDocType, String relatedDocType) throws ClientException {
498         String csid = "";
499         String subjectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
500         String objectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
501         if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(desiredDocType)) {
502             return csid;
503         }
504         if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(relatedDocType)) {
505             csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
506         } else if (subjectDocType.startsWith(relatedDocType) && objectDocType.startsWith(desiredDocType)) {
507             csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
508         }
509         return csid;
510     }
511
512     // The following method can be extended by sub-classes to update
513     // different/multiple values; e.g. values for moveable locations ("crates").
514     /**
515      * Updates a CollectionObject record with selected values from a Movement
516      * record.
517      *
518      * @param collectionObjectDocModel a document model for a CollectionObject
519      * record.
520      * @param movementDocModel a document model for a Movement record.
521      * @return a potentially updated document model for the CollectionObject
522      * record.
523      * @throws ClientException
524      */
525     protected abstract DocumentModel updateCollectionObjectValuesFromMovement(DocumentModel collectionObjectDocModel,
526             DocumentModel movementDocModel)
527             throws ClientException;
528 }