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 ...");
64 // Test whether a SQL function exists to supply the computed
65 // current location of a CollectionObject.
66 if (storedFunctionExists(STORED_FUNCTION_NAME)) {
67 logger.debug("Stored function " + STORED_FUNCTION_NAME + "exists.");
69 logger.debug("Stored function " + STORED_FUNCTION_NAME + "does NOT exist.");
71 // FIXME: For incremental debugging, as we work through implementing
74 // If it does not, load the function from the resources available
75 // to this class, and run a JDBC command to create that function.
77 // At the moment, that function is named computeCurrentLocation(),
78 // and resides in the resources of the current module.
80 // For now, assume this function will exist in the 'nuxeo' database;
81 // future work to create per-tenant repositories will likely require that
82 // our JDBC statements connect to the appropriate tenant-specific database.
84 // It doesn't appear we can create this function via 'ant create_nuxeo db'
85 // during the build process, because there's a substantial likelihood at
86 // that point that tables referred to by the function (movements_common
87 // and collectionobjects_common) will not exist, and PostgreSQL will not
88 // permit the function to be created if that is the case.
90 ClassLoader classLoader = getClass().getClassLoader();
91 String functionResourcePath =
92 DATABASE_RESOURCE_DIRECTORY_NAME + "/"
93 + DATABASE_SYSTEM_NAME + "/"
94 + STORED_FUNCTION_NAME + ".sql";
95 InputStream instream = classLoader.getResourceAsStream(functionResourcePath);
96 if (instream == null ) {
97 logger.warn("Could not read stored function command from resource path " + functionResourcePath);
101 sql = stringFromInputStream(instream);
102 } catch (IOException ioe) {
103 logger.warn("Could not create string from stream: ", ioe);
105 if (Tools.isBlank(sql)) {
106 logger.warn("Could not create stored function to update computed current location.");
107 logger.warn("This .");
111 logger.debug("After reading stored function command from resource path.");
112 logger.debug("sql="+sql);
114 // FIXME: Execute SQL command here to create stored function.
122 * Identifies whether a document is a Movement document
124 * @param docModel a document model
125 * @return true if the document is a Movement document; false if it is not.
127 private boolean isMovementDocument(DocumentModel docModel) {
128 return documentMatchesType(docModel, MovementConstants.NUXEO_DOCTYPE);
131 // FIXME: Generic methods like the following might be split off
132 // into an event utilities class. - ADR 2012-12-05
133 // FIXME: Identify whether the equivalent of this utility method is
134 // already implemented and substitute a call to the latter if so.
136 * Identifies whether a document matches a supplied document type.
138 * @param docModel a document model.
139 * @param docType a document type string.
140 * @return true if the document matches the supplied document type; false if
143 private boolean documentMatchesType(DocumentModel docModel, String docType) {
144 if (docModel == null || Tools.isBlank(docType)) {
147 if (docModel.getType().startsWith(docType)) {
155 * Identifies whether a document is an active document; that is, if it is
156 * not a versioned record; not a proxy (symbolic link to an actual record);
157 * and not in the 'deleted' workflow state.
159 * (A note relating the latter: Nuxeo appears to send 'documentModified'
160 * events even on workflow transitions, such when records are 'soft deleted'
161 * by being transitioned to the 'deleted' workflow state.)
164 * @return true if the document is an active document; false if it is not.
166 private boolean isActiveDocument(DocumentModel docModel) {
167 if (docModel == null) {
170 boolean isActiveDocument = false;
172 if (!docModel.isVersion()
173 && !docModel.isProxy()
174 && !docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
175 isActiveDocument = true;
177 } catch (ClientException ce) {
178 logger.warn("Error while identifying whether document is an active document: ", ce);
180 return isActiveDocument;
183 private boolean storedFunctionExists(String functionname) {
184 if (Tools.isBlank(functionname)) {
188 boolean storedFunctionExists = false;
189 String sql = "SELECT proname FROM pg_proc WHERE proname='" + functionname + "'";
190 Connection conn = null;
191 Statement stmt = null;
195 conn = JDBCTools.getConnection(JDBCTools.getDataSource(JDBCTools.NUXEO_REPOSITORY_NAME));
196 stmt = conn.createStatement();
197 rs = stmt.executeQuery(sql);
199 storedFunctionExists = true;
204 } catch (Exception e) {
205 logger.debug("Error when identifying whether stored function " + functionname + "exists :", e);
217 } catch (SQLException sqle) {
218 logger.debug("SQL Exception closing statement/connection in UpdateObjectLocationOnMove.storedFunctionExists: " + sqle.getLocalizedMessage());
221 return storedFunctionExists;
224 private String stringFromInputStream(InputStream instream) throws IOException {
225 if (instream == null) {
227 BufferedReader bufreader = new BufferedReader(new InputStreamReader(instream));
228 StringBuilder sb = new StringBuilder();
230 while (line != null) {
232 line = bufreader.readLine();
233 sb.append("\n"); // FIXME: Get appropriate EOL separator rather than hard-coding
235 return sb.toString();