1 package org.collectionspace.services.listener;
3 import java.sql.Connection;
4 import java.sql.ResultSet;
5 import java.sql.SQLException;
6 import java.sql.Statement;
9 import javax.sql.DataSource;
11 import org.apache.commons.logging.Log;
12 import org.apache.commons.logging.LogFactory;
14 import org.collectionspace.services.client.workflow.WorkflowClient;
15 import org.collectionspace.services.common.api.Tools;
16 import org.collectionspace.services.common.storage.DatabaseProductType;
17 import org.collectionspace.services.common.storage.JDBCTools;
18 import org.collectionspace.services.movement.nuxeo.MovementConstants;
20 import org.nuxeo.ecm.core.api.ClientException;
21 import org.nuxeo.ecm.core.api.DocumentModel;
22 import org.nuxeo.ecm.core.event.Event;
23 import org.nuxeo.ecm.core.event.EventContext;
24 import org.nuxeo.ecm.core.event.EventListener;
25 import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
27 public class UpdateObjectLocationOnMove implements EventListener {
29 // FIXME: We might experiment here with using log4j instead of Apache Commons Logging;
30 // am using the latter to follow Ray's pattern for now
31 final Log logger = LogFactory.getLog(UpdateObjectLocationOnMove.class);
32 private final String DATABASE_RESOURCE_DIRECTORY_NAME = "db";
33 // FIXME: Currently hard-coded; get this from JDBC utilities or equivalent
34 private final String DATABASE_SYSTEM_NAME = "postgresql";
35 private final String STORED_FUNCTION_NAME = "computecurrentlocation";
37 // ####################################################################
38 // FIXME: Per Rick, what happens if a relation record is updated,
39 // that either adds or removes a relation between a Movement
40 // record and a CollectionObject record? Do we need to listen
41 // for that event as well and update the CollectionObject record's
42 // computedCurrentLocation accordingly?
44 // The following code is currently checking only for creates or
45 // updates to Movement records.
46 // ####################################################################
48 public void handleEvent(Event event) throws ClientException {
50 logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
52 // FIXME: Check for database product type here.
53 // If our database type is one for which we don't yet
54 // have tested SQL code to perform this operation, return here.
56 EventContext eventContext = event.getContext();
57 if (eventContext == null) {
60 DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
61 DocumentModel docModel = docEventContext.getSourceDocument();
62 if (isMovementDocument(docModel) && isActiveDocument(docModel)) {
63 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.
69 if (storedFunctionExists(STORED_FUNCTION_NAME)) {
70 logger.debug("Stored function " + STORED_FUNCTION_NAME + "exists.");
72 logger.debug("Stored function " + STORED_FUNCTION_NAME + "does NOT exist.");
75 // FIXME: For incremental debugging, as we work through implementing
79 // If it does not, load the function from the resources available
80 // to this class, and run a JDBC command to create that function.
82 // At the moment, that function is named computeCurrentLocation(),
83 // and resides in the resources of the current module.
85 // For now, assume this function will exist in the 'nuxeo' database;
86 // future work to create per-tenant repositories will likely require that
87 // our JDBC statements connect to the appropriate tenant-specific database.
89 // It doesn't appear we can create this function via 'ant create_nuxeo db'
90 // during the build process, because there's a substantial likelihood at
91 // that point that tables referred to by the function (movements_common
92 // and collectionobjects_common) will not exist, and PostgreSQL will not
93 // permit the function to be created if that is the case.
96 ClassLoader classLoader = getClass().getClassLoader();
97 String functionResourcePath =
98 DATABASE_RESOURCE_DIRECTORY_NAME + "/"
99 + DATABASE_SYSTEM_NAME + "/"
100 + STORED_FUNCTION_NAME + ".sql";
101 classLoader.getResourceAsStream(functionResourcePath);
104 // If the create attempt fails, bail (return) from this method.
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.
143 * Identifies whether a document is a Movement document
145 * @param docModel a document model
146 * @return true if the document is a Movement document; false if it is not.
148 private boolean isMovementDocument(DocumentModel docModel) {
149 return documentMatchesType(docModel, MovementConstants.NUXEO_DOCTYPE);
152 // FIXME: Generic methods like the following might be split off
153 // into an event utilities class. - ADR 2012-12-05
154 // FIXME: Identify whether the equivalent of this utility method is
155 // already implemented and substitute a call to the latter if so.
157 * Identifies whether a document matches a supplied document type.
159 * @param docModel a document model.
160 * @param docType a document type string.
161 * @return true if the document matches the supplied document type; false if
164 private boolean documentMatchesType(DocumentModel docModel, String docType) {
165 if (docModel == null || Tools.isBlank(docType)) {
168 if (docModel.getType().startsWith(docType)) {
176 * Identifies whether a document is an active document; that is, if it is
177 * not a versioned record; not a proxy (symbolic link to an actual record);
178 * and not in the 'deleted' workflow state.
180 * (A note relating the latter: Nuxeo appears to send 'documentModified'
181 * events even on workflow transitions, such when records are 'soft deleted'
182 * by being transitioned to the 'deleted' workflow state.)
185 * @return true if the document is an active document; false if it is not.
187 private boolean isActiveDocument(DocumentModel docModel) {
188 if (docModel == null) {
191 boolean isActiveDocument = false;
193 if (!docModel.isVersion()
194 && !docModel.isProxy()
195 && !docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
196 isActiveDocument = true;
198 } catch (ClientException ce) {
199 logger.warn("Error while identifying whether document is an active document: ", ce);
201 return isActiveDocument;
204 private boolean storedFunctionExists(String functionname) {
205 if (Tools.isBlank(functionname)) {
209 boolean storedFunctionExists = false;
210 String sql = "SELECT proname FROM pg_proc WHERE proname='" + functionname + "'";
211 Connection conn = null;
212 Statement stmt = null;
216 conn = JDBCTools.getConnection(JDBCTools.getDataSource(JDBCTools.NUXEO_REPOSITORY_NAME));
217 stmt = conn.createStatement();
218 rs = stmt.executeQuery(sql);
220 storedFunctionExists = true;
225 } catch (Exception e) {
226 logger.debug("Error when identifying whether stored function " + functionname + "exists :", e);
238 } catch (SQLException sqle) {
239 logger.debug("SQL Exception closing statement/connection in UpdateObjectLocationOnMove.storedFunctionExists: " + sqle.getLocalizedMessage());
242 return storedFunctionExists;