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