]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
eac9aa4059274457fe34dad89bf74ac75b25f3d2
[tmp/jakarta-migration.git] /
1 package org.collectionspace.services.listener;
2
3 import java.io.BufferedReader;
4 import java.io.IOException;
5 import java.io.InputStream;
6 import java.io.InputStreamReader;
7 import java.sql.Connection;
8 import java.sql.ResultSet;
9 import java.sql.SQLException;
10 import java.sql.Statement;
11 import java.util.ArrayList;
12 import java.util.GregorianCalendar;
13 import java.util.HashMap;
14 import java.util.List;
15 import java.util.Map;
16 import org.apache.commons.logging.Log;
17 import org.apache.commons.logging.LogFactory;
18 import org.collectionspace.services.client.workflow.WorkflowClient;
19 import org.collectionspace.services.common.api.GregorianCalendarDateTimeUtils;
20 import org.collectionspace.services.common.api.RefNameUtils;
21 import org.collectionspace.services.common.api.Tools;
22 import org.collectionspace.services.common.storage.JDBCTools;
23 import org.collectionspace.services.movement.nuxeo.MovementConstants;
24 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
25 import org.nuxeo.ecm.core.api.ClientException;
26 import org.nuxeo.ecm.core.api.CoreSession;
27 import org.nuxeo.ecm.core.api.DocumentModel;
28 import org.nuxeo.ecm.core.api.DocumentModelList;
29 import org.nuxeo.ecm.core.event.Event;
30 import org.nuxeo.ecm.core.event.EventContext;
31 import org.nuxeo.ecm.core.event.EventListener;
32 import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
33
34 public class UpdateObjectLocationOnMove implements EventListener {
35
36     // FIXME: We might experiment here with using log4j instead of Apache Commons Logging;
37     // am using the latter to follow Ray's pattern for now
38     private final Log logger = LogFactory.getLog(UpdateObjectLocationOnMove.class);
39     // FIXME: Make the following message, or its equivalent, a constant usable by all event listeners
40     private final String NO_FURTHER_PROCESSING_MESSAGE =
41             "This event listener will not continue processing this event ...";
42     private final List<String> relevantDocTypesList = new ArrayList<String>();
43     GregorianCalendar EARLIEST_COMPARISON_DATE = new GregorianCalendar(1600, 1, 1);
44     private final String DATABASE_RESOURCE_DIRECTORY_NAME = "db";
45     // FIXME: Currently hard-coded; get this database name value from JDBC utilities or equivalent
46     private final String DATABASE_SYSTEM_NAME = "postgresql";
47     private final static String STORED_FUNCTION_NAME = "computecurrentlocation";
48     private final static String SQL_FILENAME_EXTENSION = ".sql";
49     private final String SQL_RESOURCE_PATH =
50             DATABASE_RESOURCE_DIRECTORY_NAME + "/"
51             + DATABASE_SYSTEM_NAME + "/"
52             + STORED_FUNCTION_NAME + SQL_FILENAME_EXTENSION;
53     // The name of the relevant column in the JDBC ResultSet is currently identical
54     // to the function name, regardless of the 'SELECT ... AS' clause in the SQL query.
55     private final static String COMPUTED_CURRENT_LOCATION_COLUMN = STORED_FUNCTION_NAME;
56     // FIXME: Get this line separator value from already-declared constant elsewhere, if available
57     private final String LINE_SEPARATOR = System.getProperty("line.separator");
58     private final static String RELATIONS_COMMON_SCHEMA = "relations_common"; // FIXME: Get from external constant
59     final String RELATION_DOCTYPE = "Relation"; // FIXME: Get from external constant
60     private final static String SUBJECT_CSID_PROPERTY = "subjectCsid"; // FIXME: Get from external constant
61     private final static String OBJECT_CSID_PROPERTY = "objectCsid"; // FIXME: Get from external constant
62     private final static String COLLECTIONOBJECTS_COMMON_SCHEMA = "collectionobjects_common"; // FIXME: Get from external constant
63     final String COLLECTIONOBJECT_DOCTYPE = "CollectionObject"; // FIXME: Get from external constant
64     final String COMPUTED_CURRENT_LOCATION_PROPERTY = "computedCurrentLocation"; // FIXME: Create and then get from external constant
65     final String MOVEMENTS_COMMON_SCHEMA = "movements_common"; // FIXME: Get from external constant
66     private final static String MOVEMENT_DOCTYPE = "Movement"; // FIXME: Get from external constant
67     final String LOCATION_DATE_PROPERTY = "locationDate"; // FIXME: Get from external constant
68     final String CURRENT_LOCATION_PROPERTY = "currentLocation"; // FIXME: Get from external constant
69
70     // ####################################################################
71     // FIXME: Per Rick, what happens if a relation record is updated,
72     // that either adds or removes a relation between a Movement
73     // record and a CollectionObject record?  Do we need to listen
74     // for that event as well and update the CollectionObject record's
75     // computedCurrentLocation accordingly?
76     //
77     // The following code is currently only handling create and
78     // update events affecting Movement records.
79     // ####################################################################
80     // FIXME: We'll likely also need to handle workflow state transition and
81     // deletion events, where the soft or hard deletion of a Movement or
82     // Relation record effectively changes the current location for a CollectionObject.
83     @Override
84     public void handleEvent(Event event) throws ClientException {
85
86         logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
87
88         // FIXME: Check for database product type here.
89         // If our database type is one for which we don't yet
90         // have tested SQL code to perform this operation, return here.
91
92         EventContext eventContext = event.getContext();
93         if (eventContext == null) {
94             return;
95         }
96
97         if (!(eventContext instanceof DocumentEventContext)) {
98             logger.debug("This event does not involve a document ...");
99             logger.debug(NO_FURTHER_PROCESSING_MESSAGE);
100             return;
101         }
102         DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
103         DocumentModel docModel = docEventContext.getSourceDocument();
104
105         // If this event does not involve one of our relevant doctypes,
106         // return without further handling the event.
107         boolean involvesRelevantDocType = false;
108         relevantDocTypesList.add(MovementConstants.NUXEO_DOCTYPE);
109         // FIXME: We will likely need to add the Relation doctype here,
110         // along with additional code to handle such changes.
111         for (String docType : relevantDocTypesList) {
112             if (documentMatchesType(docModel, docType)) {
113                 involvesRelevantDocType = true;
114                 break;
115             }
116         }
117         logger.debug("This event involves a document of type " + docModel.getDocumentType().getName());
118         if (!involvesRelevantDocType) {
119             logger.debug("This event does not involve a document of a relevant type ...");
120             logger.debug(NO_FURTHER_PROCESSING_MESSAGE);
121             return;
122         }
123         if (!isActiveDocument(docModel)) {
124             logger.debug("This event does not involve an active document ...");
125             logger.debug(NO_FURTHER_PROCESSING_MESSAGE);
126             return;
127         }
128
129         logger.debug("An event involving an active document of the relevant type(s) was received by UpdateObjectLocationOnMove ...");
130
131         // Test whether a SQL function exists to supply the computed
132         // current location of a CollectionObject.
133         //
134         // If the function does not exist in the database, load the
135         // SQL command to create that function from a resource
136         // available to this class, and run a JDBC command to create
137         // that function in the database.
138         //
139         // For now, assume this function will be created in the
140         // 'nuxeo' database.
141         //
142         // FIXME: Future work to create per-tenant repositories will
143         // likely require that our JDBC statements connect to the
144         // appropriate tenant-specific database.
145         //
146         // It doesn't appear we can reliably create this function via
147         // 'ant create_nuxeo db' during the build process, because
148         // there's a substantial likelihood at that point that
149         // tables referred to by the function (e.g. movements_common
150         // and collectionobjects_common) will not yet exist.
151         // (PostgreSQL will not permit the function to be created if
152         // any of its referred-to tables do not exist.)
153         if (!storedFunctionExists(STORED_FUNCTION_NAME)) {
154             logger.trace("Stored function " + STORED_FUNCTION_NAME + " does NOT exist in the database.");
155             String sql = getStringFromResource(SQL_RESOURCE_PATH);
156             if (Tools.isBlank(sql)) {
157                 logger.warn("Could not obtain SQL command to create stored function.");
158                 logger.debug(NO_FURTHER_PROCESSING_MESSAGE);
159                 return;
160             }
161
162             int result = -1;
163             try {
164                 result = JDBCTools.executeUpdate(JDBCTools.getDataSource(JDBCTools.NUXEO_REPOSITORY_NAME), sql);
165             } catch (Exception e) {
166                 // Do nothing here
167                 // FIXME: Need to verify that the original '-1' value is preserved if an Exception is caught here.
168             }
169             logger.trace("Result of executeUpdate=" + result);
170             if (result < 0) {
171                 logger.warn("Could not create stored function in the database.");
172                 logger.debug(NO_FURTHER_PROCESSING_MESSAGE);
173                 return;
174             } else {
175                 logger.info("Stored function " + STORED_FUNCTION_NAME + " was successfully created in the database.");
176             }
177         } else {
178             logger.trace("Stored function " + STORED_FUNCTION_NAME + " already exists in the database.");
179         }
180
181         String movementCsid = NuxeoUtils.getCsid(docModel);
182         logger.debug("Movement record CSID=" + movementCsid);
183
184         // FIXME: Temporary, for debugging: check whether the movement record's
185         // location date field value is stale in Nuxeo at this point
186         GregorianCalendar cal = (GregorianCalendar) docModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
187         logger.debug("location date=" + GregorianCalendarDateTimeUtils.formatAsISO8601Date(cal));
188
189         // Find CollectionObject records that are related to this Movement record:
190         //
191         // Via an NXQL query, get a list of (non-deleted) relation records where:
192         // * This movement record's CSID is the subject CSID of the relation.
193         // * The object document type is a CollectionObject doctype.
194         //
195         // Note: this assumes that every such relation is captured by
196         // relations with Movement-as-subject and CollectionObject-as-object,
197         // logic that matches that of the SQL function to obtain the computed
198         // current location of the CollectionObject.
199         //
200         // That may NOT always be the case; it's possible some such relations may
201         // exist only with CollectionObject-as-subject and Movement-as-object.
202         CoreSession coreSession1 = docEventContext.getCoreSession();
203         CoreSession coreSession = docModel.getCoreSession();
204         if (coreSession1 == coreSession || coreSession1.equals(coreSession)) {
205             logger.debug("Core sessions are equal.");
206         } else {
207             logger.debug("Core sessions are NOT equal.");
208         }
209
210         // Check whether closing and opening a transaction here might
211         // flush any hypothetical caching that Nuxeo is doing at this point
212
213         String query = String.format(
214                 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
215                 + "(relations_common:subjectCsid = '%2$s' "
216                 + "AND relations_common:objectDocumentType = '%3$s') "
217                 + "AND (ecm:currentLifeCycleState <> 'deleted') "
218                 + "AND ecm:isProxy = 0 "
219                 + "AND ecm:isCheckedInVersion = 0", RELATION_DOCTYPE, movementCsid, COLLECTIONOBJECT_DOCTYPE);
220         DocumentModelList relatedDocModels = coreSession.query(query);
221         if (relatedDocModels == null || relatedDocModels.isEmpty()) {
222             return;
223         } else {
224             logger.trace("Found " + relatedDocModels.size() + " related documents.");
225         }
226
227         // Iterate through the list of Relation records found and build
228         // a list of CollectionObject CSIDs, by extracting the object CSIDs
229         // from those Relation records.
230
231         // FIXME: The following code might be refactored into a generic 'get property
232         // values from a list of document models' method, if this doesn't already exist.
233         String csid = "";
234         List<String> collectionObjectCsids = new ArrayList<String>();
235         for (DocumentModel relatedDocModel : relatedDocModels) {
236             csid = (String) relatedDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
237             if (Tools.notBlank(csid)) {
238                 collectionObjectCsids.add(csid);
239             }
240         }
241         if (collectionObjectCsids == null || collectionObjectCsids.isEmpty()) {
242             logger.warn("Could not obtain any CSIDs of related CollectionObject records.");
243             logger.debug(NO_FURTHER_PROCESSING_MESSAGE);
244             return;
245         } else {
246             logger.debug("Found " + collectionObjectCsids.size() + " CSIDs of related CollectionObject records.");
247         }
248
249         // Iterate through the list of CollectionObject CSIDs found.
250         DocumentModel collectionObjectDocModel = null;
251         String computedCurrentLocationRefName = "";
252         Map<DocumentModel, String> docModelsToUpdate = new HashMap<DocumentModel, String>();
253         for (String collectionObjectCsid : collectionObjectCsids) {
254
255             // Verify that the CollectionObject record is active.
256             collectionObjectDocModel = getDocModelFromCsid(coreSession, collectionObjectCsid);
257             if (!isActiveDocument(collectionObjectDocModel)) {
258                 continue;
259             }
260
261             // Obtain the computed current location of that CollectionObject.
262             //
263             // JDBC/SQL query method:
264             // computedCurrentLocationRefName = computeCurrentLocation(collectionObjectCsid);
265             //
266             // Nuxeo (NXQL or CMIS) query method, currently with some
267             // non-performant procedural augmentation:
268             computedCurrentLocationRefName = computeCurrentLocation(coreSession, collectionObjectCsid, movementCsid);
269             logger.debug("computedCurrentLocation refName=" + computedCurrentLocationRefName);
270
271             // Check that the value returned from the SQL function, which
272             // is expected to be a reference (refName) to a storage location
273             // authority term, is, at a minimum:
274             // * Non-null and non-blank. (We need to verify this assumption; can a
275             //   CollectionObject's computed current location meaningfully be 'un-set'?)
276             // * Capable of being successfully parsed by an authority item parser;
277             //   that is, returning a non-null parse result.
278             if ((Tools.notBlank(computedCurrentLocationRefName)
279                     && (RefNameUtils.parseAuthorityTermInfo(computedCurrentLocationRefName) != null))) {
280                 logger.debug("refName passes basic validation tests.");
281
282                 // If the value returned from the function passes validation,
283                 // compare that value to the value in the computedCurrentLocation
284                 // field of that CollectionObject
285                 //
286                 // If the CollectionObject does not already have a
287                 // computedCurrentLocation value, or if the two values differ ...
288                 String existingComputedCurrentLocationRefName =
289                         (String) collectionObjectDocModel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY);
290                 if (Tools.isBlank(existingComputedCurrentLocationRefName)
291                         || !computedCurrentLocationRefName.equals(existingComputedCurrentLocationRefName)) {
292                     logger.debug("Existing computedCurrentLocation refName=" + existingComputedCurrentLocationRefName);
293                     logger.debug("computedCurrentLocation refName requires updating.");
294                     // ... identify this CollectionObject's docModel and new field value for subsequent updating
295                     docModelsToUpdate.put(collectionObjectDocModel, computedCurrentLocationRefName);
296                 }
297             } else {
298                 logger.debug("computedCurrentLocation refName does NOT require updating.");
299             }
300
301         }
302
303         // For each CollectionObject record that has been identified for updating,
304         // update its computedCurrentLocation field with its computed current
305         // location value returned from the SQL function.
306         for (Map.Entry<DocumentModel, String> entry : docModelsToUpdate.entrySet()) {
307             DocumentModel dmodel = entry.getKey();
308             String newCurrentLocationValue = entry.getValue();
309             dmodel.setProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY, newCurrentLocationValue);
310             coreSession.saveDocument(dmodel);
311             if (logger.isTraceEnabled()) {
312                 String afterUpdateComputedCurrentLocationRefName =
313                         (String) dmodel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY);
314                 logger.trace("Following update, new computedCurrentLocation refName value=" + afterUpdateComputedCurrentLocationRefName);
315
316             }
317         }
318     }
319
320     // FIXME: Generic methods like many of those below might be split off,
321     // into an event utilities class, base classes, or otherwise. - ADR 2012-12-05
322     //
323     // FIXME: Identify whether the equivalent of the documentMatchesType utility
324     // method is already implemented and substitute a call to the latter if so.
325     // This may well already exist.
326     /**
327      * Identifies whether a document matches a supplied document type.
328      *
329      * @param docModel a document model.
330      * @param docType a document type string.
331      * @return true if the document matches the supplied document type; false if
332      * it does not.
333      */
334     private boolean documentMatchesType(DocumentModel docModel, String docType) {
335         if (docModel == null || Tools.isBlank(docType)) {
336             return false;
337         }
338         if (docModel.getType().startsWith(docType)) {
339             return true;
340         } else {
341             return false;
342         }
343     }
344
345     /**
346      * Identifies whether a document is an active document; that is, if it is
347      * not a versioned record; not a proxy (symbolic link to an actual record);
348      * and not in the 'deleted' workflow state.
349      *
350      * (A note relating the latter: Nuxeo appears to send 'documentModified'
351      * events even on workflow transitions, such when records are 'soft deleted'
352      * by being transitioned to the 'deleted' workflow state.)
353      *
354      * @param docModel
355      * @return true if the document is an active document; false if it is not.
356      */
357     private boolean isActiveDocument(DocumentModel docModel) {
358         if (docModel == null) {
359             return false;
360         }
361         boolean isActiveDocument = false;
362         try {
363             if (!docModel.isVersion()
364                     && !docModel.isProxy()
365                     && !docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
366                 isActiveDocument = true;
367             }
368         } catch (ClientException ce) {
369             logger.warn("Error while identifying whether document is an active document: ", ce);
370         }
371         return isActiveDocument;
372     }
373
374     // FIXME: The following method is specific to PostgreSQL, because of
375     // the SQL command executed; it may need to be generalized.
376     // Note: It may be necessary in some cases to provide additional
377     // parameters beyond a function name (such as a function signature)
378     // to uniquely identify a function. So far, however, this need
379     // hasn't arisen in our specific use case here.
380     /**
381      * Identifies whether a stored function exists in a database.
382      *
383      * @param functionname the name of the function.
384      * @return true if the function exists in the database; false if it does
385      * not.
386      */
387     private boolean storedFunctionExists(String functionname) {
388         if (Tools.isBlank(functionname)) {
389             return false;
390         }
391         boolean storedFunctionExists = false;
392         String sql = "SELECT proname FROM pg_proc WHERE proname='" + functionname + "'";
393         Connection conn = null;
394         Statement stmt = null;
395         ResultSet rs = null;
396         boolean storedAutoCommitState = true;
397         try {
398             conn = JDBCTools.getConnection(JDBCTools.getDataSource(JDBCTools.NUXEO_REPOSITORY_NAME));
399             stmt = conn.createStatement(ResultSet.CONCUR_UPDATABLE, ResultSet.CLOSE_CURSORS_AT_COMMIT);
400             storedAutoCommitState = conn.getAutoCommit();
401             conn.setAutoCommit(true);
402             rs = stmt.executeQuery(sql);
403             if (rs.next()) {
404                 storedFunctionExists = true;
405             }
406             rs.close();
407             stmt.close();
408             conn.setAutoCommit(storedAutoCommitState);
409             conn.close();
410         } catch (Exception e) {
411             logger.debug("Error when identifying whether stored function " + functionname + "exists :", e);
412         } finally {
413             try {
414                 if (rs != null) {
415                     rs.close();
416                 }
417                 if (stmt != null) {
418                     stmt.close();
419                 }
420                 if (conn != null) {
421                     conn.setAutoCommit(storedAutoCommitState);
422                     conn.close();
423                 }
424             } catch (SQLException sqle) {
425                 logger.debug("SQL Exception closing statement/connection in "
426                         + "UpdateObjectLocationOnMove.storedFunctionExists: "
427                         + sqle.getLocalizedMessage());
428             }
429         }
430         return storedFunctionExists;
431     }
432
433     /**
434      * Returns the computed current location of a CollectionObject (aka
435      * Cataloging) record.
436      *
437      * @param csid the CSID of a CollectionObject record.
438      * @return the computed current location of the CollectionObject record.
439      */
440     private String computeCurrentLocation(String csid) {
441         String computedCurrentLocation = "";
442         if (Tools.isBlank(csid)) {
443             return computedCurrentLocation;
444         }
445         String sql = String.format("SELECT %1$s('%2$s')", STORED_FUNCTION_NAME, csid);
446         Connection conn = null;
447         Statement stmt = null;
448         ResultSet rs = null;
449         try {
450             conn = JDBCTools.getConnection(JDBCTools.getDataSource(JDBCTools.NUXEO_REPOSITORY_NAME));
451             stmt = conn.createStatement(ResultSet.CONCUR_UPDATABLE, ResultSet.CLOSE_CURSORS_AT_COMMIT);
452             rs = stmt.executeQuery(sql);
453             if (rs.next()) {
454                 computedCurrentLocation = rs.getString(COMPUTED_CURRENT_LOCATION_COLUMN);
455                 logger.debug("computedCurrentLocation first=" + computedCurrentLocation);
456             }
457             // Experiment with performing an update before the query
458             // as a possible means of refreshing data.
459             String updateSql = getStringFromResource(SQL_RESOURCE_PATH);
460             int result = -1;
461             try {
462                 result = JDBCTools.executeUpdate(JDBCTools.getDataSource(JDBCTools.NUXEO_REPOSITORY_NAME), updateSql);
463             } catch (Exception e) {
464             }
465             logger.trace("Result of executeUpdate=" + result);
466             // String randomSql = String.format("SELECT now()");
467             // rs = stmt.executeQuery(randomSql);
468             // rs.close();
469             stmt.close();
470             stmt = conn.createStatement();
471             rs = stmt.executeQuery(sql);
472             if (rs.next()) {
473                 computedCurrentLocation = rs.getString(COMPUTED_CURRENT_LOCATION_COLUMN);
474                 logger.debug("computedCurrentLocation second=" + computedCurrentLocation);
475             }
476             rs.close();
477             stmt.close();
478             conn.close();
479         } catch (Exception e) {
480             logger.debug("Error when attempting to obtain the computed current location of an object :", e);
481         } finally {
482             try {
483                 if (rs != null) {
484                     rs.close();
485                 }
486                 if (stmt != null) {
487                     stmt.close();
488                 }
489                 if (conn != null) {
490                     conn.close();
491                 }
492             } catch (SQLException sqle) {
493                 logger.debug("SQL Exception closing statement/connection in "
494                         + "UpdateObjectLocationOnMove.computeCurrentLocation: "
495                         + sqle.getLocalizedMessage());
496             }
497         }
498         return computedCurrentLocation;
499     }
500
501     // FIXME: A quick first pass, using an only partly query-based technique for
502     // getting the current location, augmented by procedural code.
503     //
504     // Should be replaced by a more performant method, based entirely, or nearly so,
505     // on a query.
506     //
507     // E.g. a sample CMIS query for retrieving Movement records related to a CollectionObject;
508     // we can see if the ORDER BY clause can refer to a Movement locationDate field.
509     /*
510      "SELECT DOC.nuxeo:pathSegment, DOC.dc:title, REL.dc:title,"
511      + "REL.relations_common:objectCsid, REL.relations_common:subjectCsid FROM Movement DOC "
512      + "JOIN Relation REL ON REL.relations_common:objectCsid = DOC.nuxeo:pathSegment "
513      + "WHERE REL.relations_common:subjectCsid = '5b4c617e-53a0-484b-804e' "
514      + "AND DOC.nuxeo:isVersion = false "
515      + "ORDER BY DOC.collectionspace_core:updatedAt DESC";
516      */
517     private String computeCurrentLocation(CoreSession session, String collectionObjectCsid,
518             String movementCsid) throws ClientException {
519         String computedCurrentLocation = "";
520         // Get Relation records for Movments related to this CollectionObject
521         String query = String.format(
522                 "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
523                 + "(relations_common:subjectCsid = '%2$s' "
524                 + "AND relations_common:objectDocumentType = '%3$s') "
525                 + "AND (ecm:currentLifeCycleState <> 'deleted') "
526                 + "AND ecm:isProxy = 0 "
527                 + "AND ecm:isCheckedInVersion = 0 ",
528                 RELATION_DOCTYPE, collectionObjectCsid, MOVEMENT_DOCTYPE, movementCsid, COLLECTIONOBJECT_DOCTYPE);
529         logger.debug("query=" + query);
530         DocumentModelList relatedDocModels = session.query(query);
531         if (relatedDocModels == null || relatedDocModels.isEmpty()) {
532             logger.trace("Found " + relatedDocModels.size() + " related documents.");
533             return "";
534         } else {
535             logger.trace("Found " + relatedDocModels.size() + " related documents.");
536         }
537         // Get the CollectionObject's current location from the related Movement
538         // record with the most recent location date.
539         GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
540         DocumentModel movementDocModel = null;
541         String csid = "";
542         String location = "";
543         for (DocumentModel relatedDocModel : relatedDocModels) {
544             // The object CSID in the relation is the related Movement record's CSID
545             csid = (String) relatedDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
546             movementDocModel = getDocModelFromCsid(session, csid);
547             GregorianCalendar locationDate = (GregorianCalendar) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
548             if (locationDate == null) {
549                 continue;
550             }
551             if (locationDate.after(mostRecentLocationDate)) {
552                 mostRecentLocationDate = locationDate;
553                 location = (String) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, CURRENT_LOCATION_PROPERTY);
554             }
555             if (Tools.notBlank(location)) {
556                 computedCurrentLocation = location;
557             }
558         }
559         return computedCurrentLocation;
560     }
561
562     /**
563      * Returns a string representation of the contents of an input stream.
564      *
565      * @param instream an input stream.
566      * @return a string representation of the contents of the input stream.
567      * @throws an IOException if an error occurs when reading the input stream.
568      */
569     private String stringFromInputStream(InputStream instream) throws IOException {
570         if (instream == null) {
571         }
572         BufferedReader bufreader = new BufferedReader(new InputStreamReader(instream));
573         StringBuilder sb = new StringBuilder();
574         String line = "";
575         while (line != null) {
576             sb.append(line);
577             line = bufreader.readLine();
578             sb.append(LINE_SEPARATOR);
579         }
580         return sb.toString();
581     }
582
583     /**
584      * Returns a string representation of a resource available to the current
585      * class.
586      *
587      * @param resourcePath a path to the resource.
588      * @return a string representation of the resource. Returns null if the
589      * resource cannot be read, or if it cannot be successfully represented as a
590      * string.
591      */
592     private String getStringFromResource(String resourcePath) {
593         String str = "";
594         ClassLoader classLoader = getClass().getClassLoader();
595         InputStream instream = classLoader.getResourceAsStream(resourcePath);
596         if (instream == null) {
597             logger.warn("Could not read from resource from path " + resourcePath);
598             return null;
599         }
600         try {
601             str = stringFromInputStream(instream);
602         } catch (IOException ioe) {
603             logger.warn("Could not create string from stream: ", ioe);
604             return null;
605         }
606         return str;
607     }
608
609     private DocumentModel getDocModelFromCsid(CoreSession coreSession, String collectionObjectCsid) {
610         DocumentModelList collectionObjectDocModels = null;
611         try {
612             final String query = "SELECT * FROM "
613                     + NuxeoUtils.BASE_DOCUMENT_TYPE
614                     + " WHERE "
615                     + NuxeoUtils.getByNameWhereClause(collectionObjectCsid);
616             collectionObjectDocModels = coreSession.query(query);
617         } catch (Exception e) {
618             logger.warn("Exception in query to get document model for CollectionObject: ", e);
619         }
620         if (collectionObjectDocModels == null || collectionObjectDocModels.isEmpty()) {
621             logger.warn("Could not get document models for CollectionObject(s).");
622         } else if (collectionObjectDocModels.size() != 1) {
623             logger.debug("Found more than 1 document with CSID=" + collectionObjectCsid);
624         }
625         return collectionObjectDocModels.get(0);
626     }
627 }