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";
34 // ####################################################################
35 // FIXME: Per Rick, what happens if a relation record is updated,
36 // that either adds or removes a relation between a Movement
37 // record and a CollectionObject record? Do we need to listen
38 // for that event as well and update the CollectionObject record's
39 // computedCurrentLocation accordingly?
41 // The following code is currently checking only for creates or
42 // updates to Movement records.
43 // ####################################################################
45 public void handleEvent(Event event) throws ClientException {
47 logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
49 // FIXME: Check for database product type here.
50 // If our database type is one for which we don't yet
51 // have tested SQL code to perform this operation, return here.
53 EventContext eventContext = event.getContext();
54 if (eventContext == null) {
57 DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
58 DocumentModel docModel = docEventContext.getSourceDocument();
59 if (isMovementDocument(docModel) && isActiveDocument(docModel)) {
60 logger.debug("A create or update event for an active Movement document was received by UpdateObjectLocationOnMove ...");
62 // Test whether a SQL function exists to supply the computed
63 // current location of a CollectionObject.
64 if (storedFunctionExists(STORED_FUNCTION_NAME)) {
65 logger.debug("Stored function " + STORED_FUNCTION_NAME + "exists.");
67 logger.debug("Stored function " + STORED_FUNCTION_NAME + "does NOT exist.");
69 // If the function does not exist in the database, load the
70 // SQL command to create that function from a resource
71 // available to this class, and run a JDBC command to create
72 // that function in the database.
74 // For now, assume this function will be created in the
77 // FIXME: Future work to create per-tenant repositories will
78 // likely require that our JDBC statements connect to the
79 // appropriate tenant-specific database.
81 // It doesn't appear we can reliably create this function via
82 // 'ant create_nuxeo db' during the build process, because
83 // there's a substantial likelihood at that point that
84 // tables referred to by the function (e.g. movements_common
85 // and collectionobjects_common) will not yet exist.
86 // (PostgreSQL will not permit the function to be created if
87 // any of its referred-to tables do not exist.)
88 String sqlResourcePath =
89 DATABASE_RESOURCE_DIRECTORY_NAME + "/"
90 + DATABASE_SYSTEM_NAME + "/"
91 + STORED_FUNCTION_NAME + ".sql";
92 String sql = getStringFromResource(sqlResourcePath);
93 if (Tools.isBlank(sql)) {
94 logger.warn("Could not create stored function to update computed current location.");
95 logger.warn("Actions in this event listener will NOT be performed, as a result of a previous error.");
99 logger.debug("After reading stored function command from resource path.");
100 logger.debug("sql=" + sql);
102 // FIXME: Execute SQL command here to create stored function.
106 // Get this Movement record's CSID via the document model.
108 // Find every CollectionObject record related to this Movement record:
110 // Via an NXQL query, get a list of (non-deleted) relation records where:
111 // * This movement record's CSID is the subject CSID of the relation.
112 // * The object document type is a CollectionObject doctype.
114 // Iterate through that list of Relation records and build a list of
115 // CollectionObject CSIDs, by extracting the object CSIDs of those records.
117 // For each such CollectionObject:
119 // Verify that the CollectionObject record is active (use isActiveDocument(), below).
121 // Via a JDBC call, invoke the SQL function to supply the last
122 // identified location of that CollectionObject, giving it the CSID
123 // of the CollectionObject record as an argument.
125 // Check that the SQL function's returned value, which is expected
126 // to be a reference (refName) to a storage location authority term,
129 // * Capable of being successfully parsed by an authority item parser,
130 // returning a non-null parse result.
132 // Compare that returned value to the value in the
133 // lastIdentifiedLocation field of that CollectionObject
135 // If the two values differ, update the CollectionObject record,
136 // setting the value of the lastIdentifiedLocation field of that
137 // CollectionObject record to the value returned from the SQL function.
145 * Identifies whether a document is a Movement document
147 * @param docModel a document model
148 * @return true if the document is a Movement document; false if it is not.
150 private boolean isMovementDocument(DocumentModel docModel) {
151 return documentMatchesType(docModel, MovementConstants.NUXEO_DOCTYPE);
154 // FIXME: Generic methods like many of those below might be split off,
155 // into an event utilities class, base classes, or otherwise. - ADR 2012-12-05
157 // FIXME: Identify whether the equivalent of the documentMatchesType utility
158 // method is already implemented and substitute a call to the latter if so.
159 // This may well already exist.
161 * Identifies whether a document matches a supplied document type.
163 * @param docModel a document model.
164 * @param docType a document type string.
165 * @return true if the document matches the supplied document type; false if
168 private boolean documentMatchesType(DocumentModel docModel, String docType) {
169 if (docModel == null || Tools.isBlank(docType)) {
172 if (docModel.getType().startsWith(docType)) {
180 * Identifies whether a document is an active document; that is, if it is
181 * not a versioned record; not a proxy (symbolic link to an actual record);
182 * and not in the 'deleted' workflow state.
184 * (A note relating the latter: Nuxeo appears to send 'documentModified'
185 * events even on workflow transitions, such when records are 'soft deleted'
186 * by being transitioned to the 'deleted' workflow state.)
189 * @return true if the document is an active document; false if it is not.
191 private boolean isActiveDocument(DocumentModel docModel) {
192 if (docModel == null) {
195 boolean isActiveDocument = false;
197 if (!docModel.isVersion()
198 && !docModel.isProxy()
199 && !docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
200 isActiveDocument = true;
202 } catch (ClientException ce) {
203 logger.warn("Error while identifying whether document is an active document: ", ce);
205 return isActiveDocument;
208 // FIXME: The following method is specific to PostgreSQL, because of
209 // the SQL command executed; it may need to be generalized.
210 // Note: It may be necessary in some cases to provide additional
211 // parameters beyond a function name (such as a function signature)
212 // to uniquely identify a function. So far, however, this need
213 // hasn't arisen in our specific use case here.
215 * Identifies whether a stored function exists in a database.
217 * @param functionname the name of the function.
218 * @return true if the function exists in the database; false if it does
221 private boolean storedFunctionExists(String functionname) {
222 if (Tools.isBlank(functionname)) {
225 boolean storedFunctionExists = false;
226 String sql = "SELECT proname FROM pg_proc WHERE proname='" + functionname + "'";
227 Connection conn = null;
228 Statement stmt = null;
231 conn = JDBCTools.getConnection(JDBCTools.getDataSource(JDBCTools.NUXEO_REPOSITORY_NAME));
232 stmt = conn.createStatement();
233 rs = stmt.executeQuery(sql);
235 storedFunctionExists = true;
240 } catch (Exception e) {
241 logger.debug("Error when identifying whether stored function " + functionname + "exists :", e);
253 } catch (SQLException sqle) {
254 logger.debug("SQL Exception closing statement/connection in "
255 + "UpdateObjectLocationOnMove.storedFunctionExists: "
256 + sqle.getLocalizedMessage());
259 return storedFunctionExists;
263 * Returns a string representation of the contents of an input stream.
265 * @param instream an input stream.
266 * @return a string representation of the contents of the input stream.
267 * @throws an IOException if an error occurs when reading the input stream.
269 private String stringFromInputStream(InputStream instream) throws IOException {
270 if (instream == null) {
272 BufferedReader bufreader = new BufferedReader(new InputStreamReader(instream));
273 StringBuilder sb = new StringBuilder();
275 while (line != null) {
277 line = bufreader.readLine();
278 sb.append("\n"); // FIXME: Get appropriate EOL separator rather than hard-coding
280 return sb.toString();
284 * Returns a string representation of a resource available to the current
287 * @param resourcePath a path to the resource.
288 * @return a string representation of the resource. Returns null if the
289 * resource cannot be read, or if it cannot be successfully represented as a
292 private String getStringFromResource(String resourcePath) {
294 ClassLoader classLoader = getClass().getClassLoader();
295 InputStream instream = classLoader.getResourceAsStream(resourcePath);
296 if (instream == null) {
297 logger.warn("Could not read from resource from path " + resourcePath);
301 str = stringFromInputStream(instream);
302 } catch (IOException ioe) {
303 logger.warn("Could not create string from stream: ", ioe);