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