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