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