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