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