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.SQLException;
10 import java.sql.Statement;
11 import org.apache.commons.logging.Log;
12 import org.apache.commons.logging.LogFactory;
13 import org.collectionspace.services.client.workflow.WorkflowClient;
14 import org.collectionspace.services.common.api.Tools;
15 import org.collectionspace.services.common.storage.JDBCTools;
16 import org.collectionspace.services.movement.nuxeo.MovementConstants;
17 import org.nuxeo.ecm.core.api.ClientException;
18 import org.nuxeo.ecm.core.api.DocumentModel;
19 import org.nuxeo.ecm.core.event.Event;
20 import org.nuxeo.ecm.core.event.EventContext;
21 import org.nuxeo.ecm.core.event.EventListener;
22 import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
24 public class UpdateObjectLocationOnMove implements EventListener {
26 // FIXME: We might experiment here with using log4j instead of Apache Commons Logging;
27 // am using the latter to follow Ray's pattern for now
28 final Log logger = LogFactory.getLog(UpdateObjectLocationOnMove.class);
29 private final String DATABASE_RESOURCE_DIRECTORY_NAME = "db";
30 // FIXME: Currently hard-coded; get this from JDBC utilities or equivalent
31 private final String DATABASE_SYSTEM_NAME = "postgresql";
32 private final String STORED_FUNCTION_NAME = "computecurrentlocation";
33 private final String SQL_FILENAME_EXTENSION = ".sql";
34 private final String SQL_RESOURCE_PATH =
35 DATABASE_RESOURCE_DIRECTORY_NAME + "/"
36 + DATABASE_SYSTEM_NAME + "/"
37 + STORED_FUNCTION_NAME + SQL_FILENAME_EXTENSION;
39 // ####################################################################
40 // FIXME: Per Rick, what happens if a relation record is updated,
41 // that either adds or removes a relation between a Movement
42 // record and a CollectionObject record? Do we need to listen
43 // for that event as well and update the CollectionObject record's
44 // computedCurrentLocation accordingly?
46 // The following code is currently checking only for creates or
47 // updates to Movement records.
48 // ####################################################################
50 public void handleEvent(Event event) throws ClientException {
52 logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
54 // FIXME: Check for database product type here.
55 // If our database type is one for which we don't yet
56 // have tested SQL code to perform this operation, return here.
58 EventContext eventContext = event.getContext();
59 if (eventContext == null) {
62 DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
63 DocumentModel docModel = docEventContext.getSourceDocument();
64 if (isMovementDocument(docModel) && isActiveDocument(docModel)) {
65 logger.debug("A create or update event for an active Movement document was received by UpdateObjectLocationOnMove ...");
67 // Test whether a SQL function exists to supply the computed
68 // current location of a CollectionObject.
70 // If the function does not exist in the database, load the
71 // SQL command to create that function from a resource
72 // available to this class, and run a JDBC command to create
73 // that function in the database.
75 // For now, assume this function will be created in the
78 // FIXME: Future work to create per-tenant repositories will
79 // likely require that our JDBC statements connect to the
80 // appropriate tenant-specific database.
82 // It doesn't appear we can reliably create this function via
83 // 'ant create_nuxeo db' during the build process, because
84 // there's a substantial likelihood at that point that
85 // tables referred to by the function (e.g. movements_common
86 // and collectionobjects_common) will not yet exist.
87 // (PostgreSQL will not permit the function to be created if
88 // any of its referred-to tables do not exist.)
89 if (!storedFunctionExists(STORED_FUNCTION_NAME)) {
90 logger.debug("Stored function " + STORED_FUNCTION_NAME + " does NOT exist.");
91 String sql = getStringFromResource(SQL_RESOURCE_PATH);
92 if (Tools.isBlank(sql)) {
93 logger.warn("Could not obtain SQL command to create stored function.");
94 logger.warn("Actions in this event listener will NOT be performed, as a result of a previous error.");
98 // FIXME: Remove these temporary log statements after debugging
99 logger.debug("After reading stored function command from resource path.");
100 logger.debug("sql=" + sql);
104 result = JDBCTools.executeUpdate(JDBCTools.getDataSource(JDBCTools.NUXEO_REPOSITORY_NAME), sql);
105 logger.debug("Result of executeUpdate=" + result);
107 logger.warn("Could not create stored function.");
108 logger.warn("Actions in this event listener will NOT be performed, as a result of a previous error.");
111 logger.debug("Stored function " + STORED_FUNCTION_NAME + " was successfully created.");
113 } catch (Exception e) {
114 logger.warn("Could not create stored function: ", e);
115 logger.warn("Actions in this event listener will NOT be performed, as a result of a previous Exception.");
119 logger.debug("Stored function " + STORED_FUNCTION_NAME + " exists.");
124 // Get this Movement record's CSID via the document model.
126 // Find every CollectionObject record related to this Movement record:
128 // Via an NXQL query, get a list of (non-deleted) relation records where:
129 // * This movement record's CSID is the subject CSID of the relation.
130 // * The object document type is a CollectionObject doctype.
132 // Iterate through that list of Relation records and build a list of
133 // CollectionObject CSIDs, by extracting the object CSIDs of those records.
135 // For each such CollectionObject:
137 // Verify that the CollectionObject record is active (use isActiveDocument(), below).
139 // Via a JDBC call, invoke the SQL function to supply the last
140 // identified location of that CollectionObject, giving it the CSID
141 // of the CollectionObject record as an argument.
143 // Check that the SQL function's returned value, which is expected
144 // to be a reference (refName) to a storage location authority term,
147 // * Capable of being successfully parsed by an authority item parser,
148 // returning a non-null parse result.
150 // Compare that returned value to the value in the
151 // lastIdentifiedLocation field of that CollectionObject
153 // If the two values differ, update the CollectionObject record,
154 // setting the value of the lastIdentifiedLocation field of that
155 // CollectionObject record to the value returned from the SQL function.
161 * Identifies whether a document is a Movement document
163 * @param docModel a document model
164 * @return true if the document is a Movement document; false if it is not.
166 private boolean isMovementDocument(DocumentModel docModel) {
167 return documentMatchesType(docModel, MovementConstants.NUXEO_DOCTYPE);
170 // FIXME: Generic methods like many of those below might be split off,
171 // into an event utilities class, base classes, or otherwise. - ADR 2012-12-05
173 // FIXME: Identify whether the equivalent of the documentMatchesType utility
174 // method is already implemented and substitute a call to the latter if so.
175 // This may well already exist.
177 * Identifies whether a document matches a supplied document type.
179 * @param docModel a document model.
180 * @param docType a document type string.
181 * @return true if the document matches the supplied document type; false if
184 private boolean documentMatchesType(DocumentModel docModel, String docType) {
185 if (docModel == null || Tools.isBlank(docType)) {
188 if (docModel.getType().startsWith(docType)) {
196 * Identifies whether a document is an active document; that is, if it is
197 * not a versioned record; not a proxy (symbolic link to an actual record);
198 * and not in the 'deleted' workflow state.
200 * (A note relating the latter: Nuxeo appears to send 'documentModified'
201 * events even on workflow transitions, such when records are 'soft deleted'
202 * by being transitioned to the 'deleted' workflow state.)
205 * @return true if the document is an active document; false if it is not.
207 private boolean isActiveDocument(DocumentModel docModel) {
208 if (docModel == null) {
211 boolean isActiveDocument = false;
213 if (!docModel.isVersion()
214 && !docModel.isProxy()
215 && !docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
216 isActiveDocument = true;
218 } catch (ClientException ce) {
219 logger.warn("Error while identifying whether document is an active document: ", ce);
221 return isActiveDocument;
224 // FIXME: The following method is specific to PostgreSQL, because of
225 // the SQL command executed; it may need to be generalized.
226 // Note: It may be necessary in some cases to provide additional
227 // parameters beyond a function name (such as a function signature)
228 // to uniquely identify a function. So far, however, this need
229 // hasn't arisen in our specific use case here.
231 * Identifies whether a stored function exists in a database.
233 * @param functionname the name of the function.
234 * @return true if the function exists in the database; false if it does
237 private boolean storedFunctionExists(String functionname) {
238 if (Tools.isBlank(functionname)) {
241 boolean storedFunctionExists = false;
242 String sql = "SELECT proname FROM pg_proc WHERE proname='" + functionname + "'";
243 Connection conn = null;
244 Statement stmt = null;
247 conn = JDBCTools.getConnection(JDBCTools.getDataSource(JDBCTools.NUXEO_REPOSITORY_NAME));
248 stmt = conn.createStatement();
249 rs = stmt.executeQuery(sql);
251 storedFunctionExists = true;
256 } catch (Exception e) {
257 logger.debug("Error when identifying whether stored function " + functionname + "exists :", e);
269 } catch (SQLException sqle) {
270 logger.debug("SQL Exception closing statement/connection in "
271 + "UpdateObjectLocationOnMove.storedFunctionExists: "
272 + sqle.getLocalizedMessage());
275 return storedFunctionExists;
279 * Returns a string representation of the contents of an input stream.
281 * @param instream an input stream.
282 * @return a string representation of the contents of the input stream.
283 * @throws an IOException if an error occurs when reading the input stream.
285 private String stringFromInputStream(InputStream instream) throws IOException {
286 if (instream == null) {
288 BufferedReader bufreader = new BufferedReader(new InputStreamReader(instream));
289 StringBuilder sb = new StringBuilder();
291 while (line != null) {
293 line = bufreader.readLine();
294 sb.append("\n"); // FIXME: Get appropriate EOL separator rather than hard-coding
296 return sb.toString();
300 * Returns a string representation of a resource available to the current
303 * @param resourcePath a path to the resource.
304 * @return a string representation of the resource. Returns null if the
305 * resource cannot be read, or if it cannot be successfully represented as a
308 private String getStringFromResource(String resourcePath) {
310 ClassLoader classLoader = getClass().getClassLoader();
311 InputStream instream = classLoader.getResourceAsStream(resourcePath);
312 if (instream == null) {
313 logger.warn("Could not read from resource from path " + resourcePath);
317 str = stringFromInputStream(instream);
318 } catch (IOException ioe) {
319 logger.warn("Could not create string from stream: ", ioe);