1 package org.collectionspace.services.listener;
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;
28 public class UpdateObjectLocationOnMove implements EventListener {
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");
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?
55 // The following code is currently checking only for creates or
56 // updates to Movement records.
57 // ####################################################################
59 public void handleEvent(Event event) throws ClientException {
61 logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
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.
67 EventContext eventContext = event.getContext();
68 if (eventContext == null) {
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 ...");
76 // Test whether a SQL function exists to supply the computed
77 // current location of a CollectionObject.
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.
84 // For now, assume this function will be created in the
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.
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.");
109 result = JDBCTools.executeUpdate(JDBCTools.getDataSource(JDBCTools.NUXEO_REPOSITORY_NAME), sql);
110 } catch (Exception e) {
112 // FIXME: Need to verify that the original '-1' value is preserved if an Exception is caught here.
114 logger.debug("Result of executeUpdate=" + result);
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.");
120 logger.debug("Stored function " + STORED_FUNCTION_NAME + " was successfully created.");
123 logger.debug("Stored function " + STORED_FUNCTION_NAME + " exists.");
126 String movementCsid = NuxeoUtils.getCsid(docModel);
127 logger.debug("Movement record CSID=" + movementCsid);
131 // Find CollectionObject records that are related to this Movement record:
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.
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.
141 // buildNXQLQuery(List<String> docTypes, QueryContext queryContext);
143 // Iterate through that list of Relation records and build a list of
144 // CollectionObject CSIDs, by extracting the object CSIDs of those records.
146 // For each such CollectionObject:
148 ArrayList<String> collectionObjectCsids = new ArrayList<String>();
149 collectionObjectCsids.add("5b4c617e-53a0-484b-804e"); // FIXME: Hard-coded for testing
151 DocumentModel collectionObjectDocModel = null;
152 String computedCurrentLocationRefName = "";
153 for (String csid : collectionObjectCsids) {
155 // Verify that the CollectionObject record is active (use isActiveDocument(), below).
159 collectionObjectDocModel = NuxeoUtils.getDocFromCsid(null, null, csid);
160 } catch (Exception e) {
161 logger.warn("Exception in getDocFromCsid: ", e);
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);
170 // Check that the SQL function's returned value, which is expected
171 // to be a reference (refName) to a storage location authority term,
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.");
182 // Compare that returned value to the value in the
183 // computedCurrentLocation field of that CollectionObject
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.
195 * Identifies whether a document is a Movement document
197 * @param docModel a document model
198 * @return true if the document is a Movement document; false if it is not.
200 private boolean isMovementDocument(DocumentModel docModel) {
201 return documentMatchesType(docModel, MovementConstants.NUXEO_DOCTYPE);
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
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.
211 * Identifies whether a document matches a supplied document type.
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
218 private boolean documentMatchesType(DocumentModel docModel, String docType) {
219 if (docModel == null || Tools.isBlank(docType)) {
222 if (docModel.getType().startsWith(docType)) {
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.
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.)
239 * @return true if the document is an active document; false if it is not.
241 private boolean isActiveDocument(DocumentModel docModel) {
242 if (docModel == null) {
245 boolean isActiveDocument = false;
247 if (!docModel.isVersion()
248 && !docModel.isProxy()
249 && !docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
250 isActiveDocument = true;
252 } catch (ClientException ce) {
253 logger.warn("Error while identifying whether document is an active document: ", ce);
255 return isActiveDocument;
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.
265 * Identifies whether a stored function exists in a database.
267 * @param functionname the name of the function.
268 * @return true if the function exists in the database; false if it does
271 private boolean storedFunctionExists(String functionname) {
272 if (Tools.isBlank(functionname)) {
275 boolean storedFunctionExists = false;
276 String sql = "SELECT proname FROM pg_proc WHERE proname='" + functionname + "'";
277 Connection conn = null;
278 Statement stmt = null;
281 conn = JDBCTools.getConnection(JDBCTools.getDataSource(JDBCTools.NUXEO_REPOSITORY_NAME));
282 stmt = conn.createStatement();
283 rs = stmt.executeQuery(sql);
285 storedFunctionExists = true;
290 } catch (Exception e) {
291 logger.debug("Error when identifying whether stored function " + functionname + "exists :", e);
303 } catch (SQLException sqle) {
304 logger.debug("SQL Exception closing statement/connection in "
305 + "UpdateObjectLocationOnMove.storedFunctionExists: "
306 + sqle.getLocalizedMessage());
309 return storedFunctionExists;
313 * Returns the computed current location of a CollectionObject (aka
314 * Cataloging) record.
316 * @param csid the CSID of a CollectionObject record.
319 private String computeCurrentLocation(String csid) {
320 String computedCurrentLocation = "";
321 if (Tools.isBlank(csid)) {
322 return computedCurrentLocation;
324 String sql = String.format("SELECT %1$s('%2$s')", STORED_FUNCTION_NAME, csid);
325 Connection conn = null;
326 Statement stmt = null;
329 conn = JDBCTools.getConnection(JDBCTools.getDataSource(JDBCTools.NUXEO_REPOSITORY_NAME));
330 stmt = conn.createStatement();
331 rs = stmt.executeQuery(sql);
333 computedCurrentLocation = rs.getString(COMPUTED_CURRENT_LOCATION_COLUMN);
338 } catch (Exception e) {
339 logger.debug("Error when attempting to obtain the computed current location of an object :", e);
351 } catch (SQLException sqle) {
352 logger.debug("SQL Exception closing statement/connection in "
353 + "UpdateObjectLocationOnMove.computeCurrentLocation: "
354 + sqle.getLocalizedMessage());
357 return computedCurrentLocation;
361 * Returns a string representation of the contents of an input stream.
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.
367 private String stringFromInputStream(InputStream instream) throws IOException {
368 if (instream == null) {
370 BufferedReader bufreader = new BufferedReader(new InputStreamReader(instream));
371 StringBuilder sb = new StringBuilder();
373 while (line != null) {
375 line = bufreader.readLine();
376 sb.append(LINE_SEPARATOR);
378 return sb.toString();
382 * Returns a string representation of a resource available to the current
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
390 private String getStringFromResource(String resourcePath) {
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);
399 str = stringFromInputStream(instream);
400 } catch (IOException ioe) {
401 logger.warn("Could not create string from stream: ", ioe);
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));