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