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