--- /dev/null
+package org.collectionspace.services.listener;
+
+import java.util.GregorianCalendar;
+import java.util.HashSet;
+import java.util.Set;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.collectionspace.services.client.workflow.WorkflowClient;
+import org.collectionspace.services.common.api.Tools;
+import org.collectionspace.services.movement.nuxeo.MovementConstants;
+import org.collectionspace.services.nuxeo.util.NuxeoUtils;
+import org.nuxeo.ecm.core.api.ClientException;
+import org.nuxeo.ecm.core.api.CoreSession;
+import org.nuxeo.ecm.core.api.DocumentModel;
+import org.nuxeo.ecm.core.api.DocumentModelList;
+import org.nuxeo.ecm.core.event.Event;
+import org.nuxeo.ecm.core.event.EventContext;
+import org.nuxeo.ecm.core.event.EventListener;
+import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
+
+public abstract class AbstractUpdateObjectLocationValues implements EventListener {
+
+ // FIXME: We might experiment here with using log4j instead of Apache Commons Logging;
+ // am using the latter to follow Ray's pattern for now
+ private final static Log logger = LogFactory.getLog(AbstractUpdateObjectLocationValues.class);
+ // FIXME: Make the following message, or its equivalent, a constant usable by all event listeners
+ private final static String NO_FURTHER_PROCESSING_MESSAGE =
+ "This event listener will not continue processing this event ...";
+ private final static GregorianCalendar EARLIEST_COMPARISON_DATE = new GregorianCalendar(1600, 1, 1);
+ private final static String RELATIONS_COMMON_SCHEMA = "relations_common"; // FIXME: Get from external constant
+ private final static String RELATION_DOCTYPE = "Relation"; // FIXME: Get from external constant
+ private final static String SUBJECT_CSID_PROPERTY = "subjectCsid"; // FIXME: Get from external constant
+ private final static String OBJECT_CSID_PROPERTY = "objectCsid"; // FIXME: Get from external constant
+ private final static String SUBJECT_DOCTYPE_PROPERTY = "subjectDocumentType"; // FIXME: Get from external constant
+ private final static String OBJECT_DOCTYPE_PROPERTY = "objectDocumentType"; // FIXME: Get from external constant
+ protected final static String COLLECTIONOBJECTS_COMMON_SCHEMA = "collectionobjects_common"; // FIXME: Get from external constant
+ private final static String COLLECTIONOBJECT_DOCTYPE = "CollectionObject"; // FIXME: Get from external constant
+ protected final static String COMPUTED_CURRENT_LOCATION_PROPERTY = "computedCurrentLocation"; // FIXME: Create and then get from external constant
+ private final static String MOVEMENTS_COMMON_SCHEMA = "movements_common"; // FIXME: Get from external constant
+ private final static String MOVEMENT_DOCTYPE = MovementConstants.NUXEO_DOCTYPE;
+ private final static String LOCATION_DATE_PROPERTY = "locationDate"; // FIXME: Get from external constant
+ private final static String CURRENT_LOCATION_PROPERTY = "currentLocation"; // FIXME: Get from external constant
+ private final static String ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT =
+ "AND (ecm:currentLifeCycleState <> 'deleted') "
+ + "AND ecm:isProxy = 0 "
+ + "AND ecm:isCheckedInVersion = 0";
+
+ @Override
+ public void handleEvent(Event event) throws ClientException {
+
+ logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
+
+ EventContext eventContext = event.getContext();
+ if (eventContext == null) {
+ return;
+ }
+
+ if (!(eventContext instanceof DocumentEventContext)) {
+ return;
+ }
+ DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
+ DocumentModel docModel = docEventContext.getSourceDocument();
+
+ // If this document event involves a Relation record, does this pertain to
+ // a relationship between a Movement record and a CollectionObject record?
+ //
+ // If not, we're not interested in processing this document event
+ // in this event handler, as it will have no bearing on updating a
+ // computed current location for a CollectionObject.
+
+ //
+ // (The rest of the code flow below is then identical to that which
+ // is followed when this document event involves a Movement record.)
+ String movementCsid = "";
+ if (documentMatchesType(docModel, RELATION_DOCTYPE)) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("An event involving a Relation document was received by UpdateObjectLocationOnMove ...");
+ }
+ // Get a Movement CSID from the Relation record. (If we can't
+ // get it, then we don't have a pertinent relation record.)
+ movementCsid = getCsidForDesiredDocType(docModel, MOVEMENT_DOCTYPE, COLLECTIONOBJECT_DOCTYPE);
+ if (Tools.isBlank(movementCsid)) {
+ logger.warn("Could not obtain CSID for Movement record from document event.");
+ logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
+ return;
+ }
+ } else if (documentMatchesType(docModel, MOVEMENT_DOCTYPE)) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("An event involving a Movement document was received by UpdateObjectLocationOnMove ...");
+ }
+ // Otherwise, get a Movement CSID directly from the Movement record.
+ movementCsid = NuxeoUtils.getCsid(docModel);
+ if (Tools.isBlank(movementCsid)) {
+ logger.warn("Could not obtain CSID for Movement record from document event.");
+ logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
+ return;
+ }
+ } else {
+ if (logger.isTraceEnabled()) {
+ logger.trace("This event did not involve a document relevant to UpdateObjectLocationOnMove ...");
+ }
+ return;
+ }
+
+ // Note: currently, all Document lifecycle transitions on
+ // the relevant doctype(s) are handled by this event handler,
+ // not just transitions between 'soft deleted' and active states.
+ //
+ // We are assuming that we'll want to re-compute current locations
+ // for related CollectionObjects on all such transitions, as the
+ // semantics of such transitions are opaque to this event handler,
+ // because arbitrary workflows can be bound to those doctype(s).
+ //
+ // If we need to filter out some of those lifecycle transitions,
+ // such as excluding transitions to the 'locked' workflow state; or,
+ // alternately, if we want to restrict this event handler's
+ // scope to handle only transitions into the 'soft deleted' state,
+ // we can add additional checks for doing so at this point in the code.
+
+ if (logger.isTraceEnabled()) {
+ logger.trace("Movement CSID=" + movementCsid);
+ }
+
+ // Find CollectionObject records that are related to this Movement record:
+ //
+ // Via an NXQL query, get a list of active relation records where:
+ // * This movement record's CSID is the subject CSID of the relation,
+ // and its object document type is a CollectionObject doctype;
+ // or
+ // * This movement record's CSID is the object CSID of the relation,
+ // and its subject document type is a CollectionObject doctype.
+ CoreSession coreSession = docEventContext.getCoreSession();
+ // Some values below are hard-coded for readability, rather than
+ // being obtained from constants.
+ String query = String.format(
+ "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
+ + "("
+ + " (%2$s:subjectCsid = '%3$s' "
+ + " AND %2$s:objectDocumentType = '%4$s') "
+ + " OR "
+ + " (%2$s:objectCsid = '%3$s' "
+ + " AND %2$s:subjectDocumentType = '%4$s') "
+ + ")"
+ + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
+ RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, movementCsid, COLLECTIONOBJECT_DOCTYPE);
+ DocumentModelList relationDocModels = coreSession.query(query);
+ if (relationDocModels == null || relationDocModels.isEmpty()) {
+ // Encountering a Movement record that is not related to any
+ // CollectionObject is potentially a normal occurrence, so no
+ // error messages are logged here when we stop handling this event.
+ return;
+ }
+
+ // Iterate through the list of Relation records found and build
+ // a list of CollectionObject CSIDs, by extracting the relevant CSIDs
+ // from those Relation records.
+
+ // FIXME: The following code might be refactored into a generic 'get
+ // values of a single property from a list of document models' method,
+ // if this doesn't already exist.
+ String csid = "";
+ Set<String> collectionObjectCsids = new HashSet<String>(); // Prevents/removes duplicates on add
+ for (DocumentModel relationDocModel : relationDocModels) {
+ csid = getCsidForDesiredDocType(relationDocModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
+ if (Tools.notBlank(csid)) {
+ collectionObjectCsids.add(csid);
+ }
+ }
+ if (collectionObjectCsids == null || collectionObjectCsids.isEmpty()) {
+ logger.warn("Could not obtain any CSIDs of related CollectionObject records.");
+ logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
+ return;
+ } else {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Found " + collectionObjectCsids.size() + " CSIDs of related CollectionObject records.");
+ }
+ }
+
+ // Iterate through the list of CollectionObject CSIDs found
+ // and update their location value(s).
+ for (String collectionObjectCsid : collectionObjectCsids) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("CollectionObject CSID=" + collectionObjectCsid);
+ }
+ updateAllLocationValues(coreSession, collectionObjectCsid);
+ }
+ }
+
+ // FIXME: Generic methods like many of those below might be split off,
+ // into an event utilities class, base classes, or otherwise. - ADR 2012-12-05
+ //
+ // FIXME: Identify whether the equivalent of the documentMatchesType utility
+ // method is already implemented and substitute a call to the latter if so.
+ // This may well already exist.
+ /**
+ * Identifies whether a document matches a supplied document type.
+ *
+ * @param docModel a document model.
+ * @param docType a document type string.
+ * @return true if the document matches the supplied document type; false if
+ * it does not.
+ */
+ protected static boolean documentMatchesType(DocumentModel docModel, String docType) {
+ if (docModel == null || Tools.isBlank(docType)) {
+ return false;
+ }
+ if (docModel.getType().startsWith(docType)) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Identifies whether a document is an active document; that is, if it is
+ * not a versioned record; not a proxy (symbolic link to an actual record);
+ * and not in the 'deleted' workflow state.
+ *
+ * (A note relating the latter: Nuxeo appears to send 'documentModified'
+ * events even on workflow transitions, such when records are 'soft deleted'
+ * by being transitioned to the 'deleted' workflow state.)
+ *
+ * @param docModel
+ * @return true if the document is an active document; false if it is not.
+ */
+ protected static boolean isActiveDocument(DocumentModel docModel) {
+ if (docModel == null) {
+ return false;
+ }
+ boolean isActiveDocument = false;
+ try {
+ if (!docModel.isVersion()
+ && !docModel.isProxy()
+ && !docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
+ isActiveDocument = true;
+ }
+ } catch (ClientException ce) {
+ logger.warn("Error while identifying whether document is an active document: ", ce);
+ }
+ return isActiveDocument;
+ }
+
+ /**
+ * Returns a document model for a record identified by a CSID.
+ *
+ * @param session a repository session.
+ * @param collectionObjectCsid a CollectionObject identifier (CSID)
+ * @return a document model for the record identified by the supplied CSID.
+ */
+ protected static DocumentModel getDocModelFromCsid(CoreSession session, String collectionObjectCsid) {
+ DocumentModelList collectionObjectDocModels = null;
+ try {
+ final String query = "SELECT * FROM "
+ + NuxeoUtils.BASE_DOCUMENT_TYPE
+ + " WHERE "
+ + NuxeoUtils.getByNameWhereClause(collectionObjectCsid);
+ collectionObjectDocModels = session.query(query);
+ } catch (Exception e) {
+ logger.warn("Exception in query to get document model for CollectionObject: ", e);
+ }
+ if (collectionObjectDocModels == null || collectionObjectDocModels.isEmpty()) {
+ logger.warn("Could not get document models for CollectionObject(s).");
+ return null;
+ } else if (collectionObjectDocModels.size() != 1) {
+ logger.debug("Found more than 1 document with CSID=" + collectionObjectCsid);
+ return null;
+ }
+ return collectionObjectDocModels.get(0);
+ }
+
+ // FIXME: A quick first pass, using an only partly query-based technique for
+ // getting the current location, augmented by procedural code.
+ //
+ // Should be replaced by a more performant method, based entirely, or nearly so,
+ // on a query.
+ //
+ // E.g. the following is a sample CMIS query for retrieving Movement records
+ // related to a CollectionObject, which might serve as the basis for that query.
+ /*
+ "SELECT DOC.nuxeo:pathSegment, DOC.dc:title, REL.dc:title,"
+ + "REL.relations_common:objectCsid, REL.relations_common:subjectCsid FROM Movement DOC "
+ + "JOIN Relation REL ON REL.relations_common:objectCsid = DOC.nuxeo:pathSegment "
+ + "WHERE REL.relations_common:subjectCsid = '5b4c617e-53a0-484b-804e' "
+ + "AND DOC.nuxeo:isVersion = false "
+ + "ORDER BY DOC.collectionspace_core:updatedAt DESC";
+ */
+ /**
+ * Returns the computed current location for a CollectionObject.
+ *
+ * @param session a repository session.
+ * @param collectionObjectCsid a CollectionObject identifier (CSID)
+ * @throws ClientException
+ * @return the computed current location for the CollectionObject identified
+ * by the supplied CSID.
+ */
+ protected static String computeCurrentLocation(CoreSession session, String collectionObjectCsid)
+ throws ClientException {
+ String computedCurrentLocation = "";
+ // Get Relation records for Movements related to this CollectionObject.
+ //
+ // Some values below are hard-coded for readability, rather than
+ // being obtained from constants.
+ String query = String.format(
+ "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
+ + "("
+ + " (%2$s:subjectCsid = '%3$s' "
+ + " AND %2$s:objectDocumentType = '%4$s') "
+ + " OR "
+ + " (%2$s:objectCsid = '%3$s' "
+ + " AND %2$s:subjectDocumentType = '%4$s') "
+ + ")"
+ + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
+ RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, collectionObjectCsid, MOVEMENT_DOCTYPE);
+ if (logger.isTraceEnabled()) {
+ logger.trace("query=" + query);
+ }
+ DocumentModelList relationDocModels = session.query(query);
+ if (relationDocModels == null || relationDocModels.isEmpty()) {
+ logger.warn("Unexpectedly found no relations to Movement records to/from to this CollectionObject record.");
+ return computedCurrentLocation;
+ } else {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Found " + relationDocModels.size() + " relations to Movement records to/from this CollectionObject record.");
+ }
+ }
+ // Iterate through related movement records, to get the CollectionObject's
+ // computed current location from the related Movement record with the
+ // most recent location date.
+ GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
+ DocumentModel movementDocModel = null;
+ Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
+ String relMovementCsid = "";
+ String location = "";
+ for (DocumentModel relationDocModel : relationDocModels) {
+ // Due to the 'OR' operator in the query above, related Movement
+ // record CSIDs may reside in either the subject or object CSID fields
+ // of the relation record. Whichever CSID value doesn't match the
+ // CollectionObject's CSID is inferred to be the related Movement record's CSID.
+ relMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
+ if (relMovementCsid.equals(collectionObjectCsid)) {
+ relMovementCsid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
+ }
+ if (Tools.isBlank(relMovementCsid)) {
+ continue;
+ }
+ // Because of the OR clause used in the query above, there may be
+ // two or more Relation records returned in the query results that
+ // reference the same Movement record, as either the subject
+ // or object of a relation to the same CollectionObject record;
+ // we need to filter out those duplicates.
+ if (alreadyProcessedMovementCsids.contains(relMovementCsid)) {
+ continue;
+ } else {
+ alreadyProcessedMovementCsids.add(relMovementCsid);
+ }
+ if (logger.isTraceEnabled()) {
+ logger.trace("Movement CSID=" + relMovementCsid);
+ }
+ movementDocModel = getDocModelFromCsid(session, relMovementCsid);
+ if (movementDocModel == null) {
+ continue;
+ }
+
+ // Verify that the Movement record is active. This will also exclude
+ // versioned Movement records from the computation of the current
+ // location, for tenants that are versioning such records.
+ if (!isActiveDocument(movementDocModel)) {
+ if (logger.isTraceEnabled()) {
+ logger.trace("Skipping this inactive Movement record ...");
+ }
+ continue;
+ }
+ GregorianCalendar locationDate =
+ (GregorianCalendar) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
+ if (locationDate == null) {
+ continue;
+ }
+ if (locationDate.after(mostRecentLocationDate)) {
+ mostRecentLocationDate = locationDate;
+ location = (String) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, CURRENT_LOCATION_PROPERTY);
+ if (Tools.notBlank(location)) {
+ computedCurrentLocation = location;
+ }
+ }
+ }
+ return computedCurrentLocation;
+ }
+
+ /**
+ * Returns the CSID for a desired document type from a Relation record,
+ * where the relationship involves two specified, different document types.
+ *
+ * @param relationDocModel a document model for a Relation record.
+ * @param desiredDocType a desired document type.
+ * @param relatedDocType a related document type.
+ * @throws ClientException
+ * @return the CSID from the desired document type in the relation. Returns
+ * an empty string if the Relation record does not involve both the desired
+ * and related document types, or if the desired document type is at both
+ * ends of the relation.
+ */
+ protected static String getCsidForDesiredDocType(DocumentModel relationDocModel,
+ String desiredDocType, String relatedDocType) throws ClientException {
+ String csid = "";
+ String subjectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
+ String objectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
+ if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(desiredDocType)) {
+ return csid;
+ }
+ if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(relatedDocType)) {
+ csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
+ } else if (subjectDocType.startsWith(relatedDocType) && objectDocType.startsWith(desiredDocType)) {
+ csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
+ }
+ return csid;
+ }
+
+ // Can be extended by sub-classes to update different/multiple values;
+ // e.g. values for moveable locations ("crates").
+ protected abstract void updateAllLocationValues(CoreSession coreSession, String collectionObjectCsid)
+ throws ClientException;
+}
\ No newline at end of file
package org.collectionspace.services.listener;
-import java.util.ArrayList;
-import java.util.GregorianCalendar;
import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
import java.util.Map;
-import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
-import org.collectionspace.services.client.workflow.WorkflowClient;
import org.collectionspace.services.common.api.RefNameUtils;
import org.collectionspace.services.common.api.Tools;
-import org.collectionspace.services.movement.nuxeo.MovementConstants;
-import org.collectionspace.services.nuxeo.util.NuxeoUtils;
import org.nuxeo.ecm.core.api.ClientException;
import org.nuxeo.ecm.core.api.CoreSession;
import org.nuxeo.ecm.core.api.DocumentModel;
-import org.nuxeo.ecm.core.api.DocumentModelList;
-import org.nuxeo.ecm.core.event.Event;
-import org.nuxeo.ecm.core.event.EventContext;
-import org.nuxeo.ecm.core.event.EventListener;
-import org.nuxeo.ecm.core.event.impl.DocumentEventContext;
-public class UpdateObjectLocationOnMove implements EventListener {
+public class UpdateObjectLocationOnMove extends AbstractUpdateObjectLocationValues {
// FIXME: We might experiment here with using log4j instead of Apache Commons Logging;
// am using the latter to follow Ray's pattern for now
private final Log logger = LogFactory.getLog(UpdateObjectLocationOnMove.class);
- // FIXME: Make the following message, or its equivalent, a constant usable by all event listeners
- private final String NO_FURTHER_PROCESSING_MESSAGE =
- "This event listener will not continue processing this event ...";
- private final List<String> relevantDocTypesList = new ArrayList<String>();
- GregorianCalendar EARLIEST_COMPARISON_DATE = new GregorianCalendar(1600, 1, 1);
- private final static String RELATIONS_COMMON_SCHEMA = "relations_common"; // FIXME: Get from external constant
- private final static String RELATION_DOCTYPE = "Relation"; // FIXME: Get from external constant
- private final static String SUBJECT_CSID_PROPERTY = "subjectCsid"; // FIXME: Get from external constant
- private final static String OBJECT_CSID_PROPERTY = "objectCsid"; // FIXME: Get from external constant
- private final static String SUBJECT_DOCTYPE_PROPERTY = "subjectDocumentType"; // FIXME: Get from external constant
- private final static String OBJECT_DOCTYPE_PROPERTY = "objectDocumentType"; // FIXME: Get from external constant
- private final static String COLLECTIONOBJECTS_COMMON_SCHEMA = "collectionobjects_common"; // FIXME: Get from external constant
- private final static String COLLECTIONOBJECT_DOCTYPE = "CollectionObject"; // FIXME: Get from external constant
- private final static String COMPUTED_CURRENT_LOCATION_PROPERTY = "computedCurrentLocation"; // FIXME: Create and then get from external constant
- private final static String MOVEMENTS_COMMON_SCHEMA = "movements_common"; // FIXME: Get from external constant
- private final static String MOVEMENT_DOCTYPE = MovementConstants.NUXEO_DOCTYPE;
- private final static String LOCATION_DATE_PROPERTY = "locationDate"; // FIXME: Get from external constant
- private final static String CURRENT_LOCATION_PROPERTY = "currentLocation"; // FIXME: Get from external constant
- private final static String ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT =
- "AND (ecm:currentLifeCycleState <> 'deleted') "
- + "AND ecm:isProxy = 0 "
- + "AND ecm:isCheckedInVersion = 0";
-
+
@Override
- public void handleEvent(Event event) throws ClientException {
-
- logger.trace("In handleEvent in UpdateObjectLocationOnMove ...");
-
- EventContext eventContext = event.getContext();
- if (eventContext == null) {
- return;
- }
+ protected void updateAllLocationValues(CoreSession coreSession, String collectionObjectCsid)
+ throws ClientException {
+ updateCurrentLocationValue(coreSession, collectionObjectCsid);
+ }
- if (!(eventContext instanceof DocumentEventContext)) {
+ private void updateCurrentLocationValue(CoreSession coreSession, String collectionObjectCsid)
+ throws ClientException {
+ DocumentModel collectionObjectDocModel;
+ String computedCurrentLocationRefName;
+ collectionObjectDocModel = getDocModelFromCsid(coreSession, collectionObjectCsid);
+ if (collectionObjectDocModel == null) {
return;
}
- DocumentEventContext docEventContext = (DocumentEventContext) eventContext;
- DocumentModel docModel = docEventContext.getSourceDocument();
-
- // If this document event involves a Relation record, does this pertain to
- // a relationship between a Movement record and a CollectionObject record?
- //
- // If not, we're not interested in processing this document event
- // in this event handler, as it will have no bearing on updating a
- // computed current location for a CollectionObject.
-
- //
- // (The rest of the code flow below is then identical to that which
- // is followed when this document event involves a Movement record.)
- String movementCsid = "";
- if (documentMatchesType(docModel, RELATION_DOCTYPE)) {
- if (logger.isTraceEnabled()) {
- logger.trace("An event involving a Relation document was received by UpdateObjectLocationOnMove ...");
- }
- // Get a Movement CSID from the Relation record. (If we can't
- // get it, then we don't have a pertinent relation record.)
- movementCsid = getCsidForDesiredDocType(docModel, MOVEMENT_DOCTYPE, COLLECTIONOBJECT_DOCTYPE);
- if (Tools.isBlank(movementCsid)) {
- logger.warn("Could not obtain CSID for Movement record from document event.");
- logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
- return;
- }
- } else if (documentMatchesType(docModel, MOVEMENT_DOCTYPE)) {
- if (logger.isTraceEnabled()) {
- logger.trace("An event involving a Movement document was received by UpdateObjectLocationOnMove ...");
- }
- // Otherwise, get a Movement CSID directly from the Movement record.
- movementCsid = NuxeoUtils.getCsid(docModel);
- if (Tools.isBlank(movementCsid)) {
- logger.warn("Could not obtain CSID for Movement record from document event.");
- logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
- return;
- }
- } else {
- if (logger.isTraceEnabled()) {
- logger.trace("This event did not involve a document relevant to UpdateObjectLocationOnMove ...");
- }
+ // Verify that the CollectionObject record is active.
+ if (!isActiveDocument(collectionObjectDocModel)) {
return;
}
-
- // Note: currently, all Document lifecycle transitions on
- // the relevant doctype(s) are handled by this event handler,
- // not just transitions between 'soft deleted' and active states.
- //
- // We are assuming that we'll want to re-compute current locations
- // for related CollectionObjects on all such transitions, as the
- // semantics of such transitions are opaque to this event handler,
- // because arbitrary workflows can be bound to those doctype(s).
- //
- // If we need to filter out some of those lifecycle transitions,
- // such as excluding transitions to the 'locked' workflow state; or,
- // alternately, if we want to restrict this event handler's
- // scope to handle only transitions into the 'soft deleted' state,
- // we can add additional checks for doing so at this point in the code.
-
+ // Obtain the computed current location of that CollectionObject.
+ computedCurrentLocationRefName = computeCurrentLocation(coreSession, collectionObjectCsid);
if (logger.isTraceEnabled()) {
- logger.trace("Movement CSID=" + movementCsid);
- }
-
- // Find CollectionObject records that are related to this Movement record:
- //
- // Via an NXQL query, get a list of active relation records where:
- // * This movement record's CSID is the subject CSID of the relation,
- // and its object document type is a CollectionObject doctype;
- // or
- // * This movement record's CSID is the object CSID of the relation,
- // and its subject document type is a CollectionObject doctype.
- CoreSession coreSession = docEventContext.getCoreSession();
- // Some values below are hard-coded for readability, rather than
- // being obtained from constants.
- String query = String.format(
- "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
- + "("
- + " (%2$s:subjectCsid = '%3$s' "
- + " AND %2$s:objectDocumentType = '%4$s') "
- + " OR "
- + " (%2$s:objectCsid = '%3$s' "
- + " AND %2$s:subjectDocumentType = '%4$s') "
- + ")"
- + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
- RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, movementCsid, COLLECTIONOBJECT_DOCTYPE);
- DocumentModelList relationDocModels = coreSession.query(query);
- if (relationDocModels == null || relationDocModels.isEmpty()) {
- // Encountering a Movement record that is not related to any
- // CollectionObject is potentially a normal occurrence, so no
- // error messages are logged here when we stop handling this event.
- return;
- }
-
- // Iterate through the list of Relation records found and build
- // a list of CollectionObject CSIDs, by extracting the relevant CSIDs
- // from those Relation records.
-
- // FIXME: The following code might be refactored into a generic 'get
- // values of a single property from a list of document models' method,
- // if this doesn't already exist.
- String csid = "";
- Set<String> collectionObjectCsids = new HashSet<String>(); // Prevents/removes duplicates on add
- for (DocumentModel relationDocModel : relationDocModels) {
- csid = getCsidForDesiredDocType(relationDocModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
- if (Tools.notBlank(csid)) {
- collectionObjectCsids.add(csid);
- }
- }
- if (collectionObjectCsids == null || collectionObjectCsids.isEmpty()) {
- logger.warn("Could not obtain any CSIDs of related CollectionObject records.");
- logger.warn(NO_FURTHER_PROCESSING_MESSAGE);
+ logger.trace("computedCurrentLocation refName=" + computedCurrentLocationRefName);
+ }
+
+ // Check that the value returned, which is expected to be a
+ // reference (refName) to a storage location authority term,
+ // is, at a minimum:
+ // * Non-null and non-blank. (We need to verify this assumption; can a
+ // CollectionObject's computed current location value ever meaningfully
+ // be 'un-set' by returning it to a null value?)
+ // * Capable of being successfully parsed by an authority item parser;
+ // that is, returning a non-null parse result.
+ if ((Tools.isBlank(computedCurrentLocationRefName)
+ || (RefNameUtils.parseAuthorityTermInfo(computedCurrentLocationRefName) == null))) {
+ logger.warn("Could not parse computed current location refName '" + computedCurrentLocationRefName + "'");
return;
} else {
if (logger.isTraceEnabled()) {
- logger.trace("Found " + collectionObjectCsids.size() + " CSIDs of related CollectionObject records.");
+ logger.trace("computed current location refName passes basic validation tests.");
}
}
- // Iterate through the list of CollectionObject CSIDs found.
- DocumentModel collectionObjectDocModel = null;
- String computedCurrentLocationRefName = "";
- Map<DocumentModel, String> docModelsToUpdate = new HashMap<DocumentModel, String>();
- for (String collectionObjectCsid : collectionObjectCsids) {
-
- if (logger.isTraceEnabled()) {
- logger.trace("CollectionObject CSID=" + collectionObjectCsids);
- }
- collectionObjectDocModel = getDocModelFromCsid(coreSession, collectionObjectCsid);
- if (collectionObjectDocModel == null) {
- continue;
- }
- // Verify that the CollectionObject record is active.
- if (!isActiveDocument(collectionObjectDocModel)) {
- continue;
- }
- // Obtain the computed current location of that CollectionObject.
- computedCurrentLocationRefName = computeCurrentLocation(coreSession, collectionObjectCsid);
+ // If the value returned from the function passes validation,
+ // compare it to the value in the computedCurrentLocation
+ // field of that CollectionObject.
+ String existingComputedCurrentLocationRefName =
+ (String) collectionObjectDocModel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY);
+ if (logger.isTraceEnabled()) {
+ logger.trace("Existing computedCurrentLocation refName=" + existingComputedCurrentLocationRefName);
+ }
+ // If the CollectionObject lacks a computed current location value,
+ // or if the new computed value differs from its existing value ...
+ if (Tools.isBlank(existingComputedCurrentLocationRefName)
+ || (!computedCurrentLocationRefName.equals(existingComputedCurrentLocationRefName))) {
if (logger.isTraceEnabled()) {
- logger.trace("computedCurrentLocation refName=" + computedCurrentLocationRefName);
- }
-
- // Check that the value returned, which is expected to be a
- // reference (refName) to a storage location authority term,
- // is, at a minimum:
- // * Non-null and non-blank. (We need to verify this assumption; can a
- // CollectionObject's computed current location meaningfully be 'un-set'?)
- // * Capable of being successfully parsed by an authority item parser;
- // that is, returning a non-null parse result.
- if ((Tools.isBlank(computedCurrentLocationRefName)
- || (RefNameUtils.parseAuthorityTermInfo(computedCurrentLocationRefName) == null))) {
- logger.warn("Could not parse computed current location refName '" + computedCurrentLocationRefName + "'");
- continue;
- } else {
- if (logger.isTraceEnabled()) {
- logger.trace("refName passes basic validation tests.");
- }
-
- // If the value returned from the function passes validation,
- // compare it to the value in the computedCurrentLocation
- // field of that CollectionObject.
- String existingComputedCurrentLocationRefName =
- (String) collectionObjectDocModel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY);
- if (logger.isTraceEnabled()) {
- logger.trace("Existing computedCurrentLocation refName=" + existingComputedCurrentLocationRefName);
- }
- // If the CollectionObject lacks a computed current location value,
- // or if the new computed value differs from its existing value ...
- if (Tools.isBlank(existingComputedCurrentLocationRefName)
- || (!computedCurrentLocationRefName.equals(existingComputedCurrentLocationRefName))) {
- if (logger.isTraceEnabled()) {
- logger.trace("computedCurrentLocation refName requires updating.");
- }
- // ... set aside this CollectionObject's docModel and its new
- // computed current location value for subsequent updating
- docModelsToUpdate.put(collectionObjectDocModel, computedCurrentLocationRefName);
- } else {
- if (logger.isTraceEnabled()) {
- logger.trace("computedCurrentLocation refName does NOT require updating.");
- }
- }
+ logger.trace("computedCurrentLocation refName requires updating.");
}
- }
-
- // For each CollectionObject docModel that has been set aside for updating,
- // update the value of its computedCurrentLocation field with its new,
- // computed current location.
- int collectionObjectsUpdated = 0;
- for (Map.Entry<DocumentModel, String> entry : docModelsToUpdate.entrySet()) {
- DocumentModel dmodel = entry.getKey();
- String newCurrentLocationValue = entry.getValue();
- dmodel.setProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY, newCurrentLocationValue);
- coreSession.saveDocument(dmodel);
- collectionObjectsUpdated++;
+ // ... update that value and then save the updated CollectionObject.
+ collectionObjectDocModel.setProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY, computedCurrentLocationRefName);
+ coreSession.saveDocument(collectionObjectDocModel);
if (logger.isTraceEnabled()) {
String afterUpdateComputedCurrentLocationRefName =
- (String) dmodel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY);
+ (String) collectionObjectDocModel.getProperty(COLLECTIONOBJECTS_COMMON_SCHEMA, COMPUTED_CURRENT_LOCATION_PROPERTY);
logger.trace("Following update, new computedCurrentLocation refName value=" + afterUpdateComputedCurrentLocationRefName);
}
- }
- logger.info("Updated " + collectionObjectsUpdated + " CollectionObject record(s) with new computed current location(s).");
- }
-
- // FIXME: Generic methods like many of those below might be split off,
- // into an event utilities class, base classes, or otherwise. - ADR 2012-12-05
- //
- // FIXME: Identify whether the equivalent of the documentMatchesType utility
- // method is already implemented and substitute a call to the latter if so.
- // This may well already exist.
- /**
- * Identifies whether a document matches a supplied document type.
- *
- * @param docModel a document model.
- * @param docType a document type string.
- * @return true if the document matches the supplied document type; false if
- * it does not.
- */
- private boolean documentMatchesType(DocumentModel docModel, String docType) {
- if (docModel == null || Tools.isBlank(docType)) {
- return false;
- }
- if (docModel.getType().startsWith(docType)) {
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Identifies whether a document is an active document; that is, if it is
- * not a versioned record; not a proxy (symbolic link to an actual record);
- * and not in the 'deleted' workflow state.
- *
- * (A note relating the latter: Nuxeo appears to send 'documentModified'
- * events even on workflow transitions, such when records are 'soft deleted'
- * by being transitioned to the 'deleted' workflow state.)
- *
- * @param docModel
- * @return true if the document is an active document; false if it is not.
- */
- private boolean isActiveDocument(DocumentModel docModel) {
- if (docModel == null) {
- return false;
- }
- boolean isActiveDocument = false;
- try {
- if (!docModel.isVersion()
- && !docModel.isProxy()
- && !docModel.getCurrentLifeCycleState().equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
- isActiveDocument = true;
- }
- } catch (ClientException ce) {
- logger.warn("Error while identifying whether document is an active document: ", ce);
- }
- return isActiveDocument;
- }
-
- /**
- * Returns a document model for a record identified by a CSID.
- *
- * @param session a repository session.
- * @param collectionObjectCsid a CollectionObject identifier (CSID)
- * @return a document model for the record identified by the supplied CSID.
- */
- private DocumentModel getDocModelFromCsid(CoreSession session, String collectionObjectCsid) {
- DocumentModelList collectionObjectDocModels = null;
- try {
- final String query = "SELECT * FROM "
- + NuxeoUtils.BASE_DOCUMENT_TYPE
- + " WHERE "
- + NuxeoUtils.getByNameWhereClause(collectionObjectCsid);
- collectionObjectDocModels = session.query(query);
- } catch (Exception e) {
- logger.warn("Exception in query to get document model for CollectionObject: ", e);
- }
- if (collectionObjectDocModels == null || collectionObjectDocModels.isEmpty()) {
- logger.warn("Could not get document models for CollectionObject(s).");
- return null;
- } else if (collectionObjectDocModels.size() != 1) {
- logger.debug("Found more than 1 document with CSID=" + collectionObjectCsid);
- return null;
- }
- return collectionObjectDocModels.get(0);
- }
-
- // FIXME: A quick first pass, using an only partly query-based technique for
- // getting the current location, augmented by procedural code.
- //
- // Should be replaced by a more performant method, based entirely, or nearly so,
- // on a query.
- //
- // E.g. the following is a sample CMIS query for retrieving Movement records
- // related to a CollectionObject, which might serve as the basis for that query.
- /*
- "SELECT DOC.nuxeo:pathSegment, DOC.dc:title, REL.dc:title,"
- + "REL.relations_common:objectCsid, REL.relations_common:subjectCsid FROM Movement DOC "
- + "JOIN Relation REL ON REL.relations_common:objectCsid = DOC.nuxeo:pathSegment "
- + "WHERE REL.relations_common:subjectCsid = '5b4c617e-53a0-484b-804e' "
- + "AND DOC.nuxeo:isVersion = false "
- + "ORDER BY DOC.collectionspace_core:updatedAt DESC";
- */
- /**
- * Returns the computed current location for a CollectionObject.
- *
- * @param session a repository session.
- * @param collectionObjectCsid a CollectionObject identifier (CSID)
- * @throws ClientException
- * @return the computed current location for the CollectionObject identified
- * by the supplied CSID.
- */
- private String computeCurrentLocation(CoreSession session, String collectionObjectCsid)
- throws ClientException {
- String computedCurrentLocation = "";
- // Get Relation records for Movements related to this CollectionObject.
- //
- // Some values below are hard-coded for readability, rather than
- // being obtained from constants.
- String query = String.format(
- "SELECT * FROM %1$s WHERE " // collectionspace_core:tenantId = 1 "
- + "("
- + " (%2$s:subjectCsid = '%3$s' "
- + " AND %2$s:objectDocumentType = '%4$s') "
- + " OR "
- + " (%2$s:objectCsid = '%3$s' "
- + " AND %2$s:subjectDocumentType = '%4$s') "
- + ")"
- + ACTIVE_DOCUMENT_WHERE_CLAUSE_FRAGMENT,
- RELATION_DOCTYPE, RELATIONS_COMMON_SCHEMA, collectionObjectCsid, MOVEMENT_DOCTYPE);
- if (logger.isTraceEnabled()) {
- logger.trace("query=" + query);
- }
- DocumentModelList relatedDocModels = session.query(query);
- if (relatedDocModels == null || relatedDocModels.isEmpty()) {
- logger.warn("Unexpectedly found no Movement records related to this CollectionObject record.");
- return computedCurrentLocation;
} else {
if (logger.isTraceEnabled()) {
- logger.trace("Found " + relatedDocModels.size() + " Movement record(s) related to this CollectionObject record.");
- }
- }
- // Iterate through related movement records, to get the CollectionObject's
- // computed current location from the related Movement record with the
- // most recent location date.
- GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
- DocumentModel movementDocModel = null;
- String csid = "";
- String location = "";
- for (DocumentModel relatedDocModel : relatedDocModels) {
- // Due to the 'OR' operator in the query above, related Movement
- // record CSIDs may reside in either the subject or object CSID fields
- // of the relation record. Whichever CSID value doesn't match the
- // CollectionObject's CSID is inferred to be the related Movement record's CSID.
- csid = (String) relatedDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
- if (csid.equals(collectionObjectCsid)) {
- csid = (String) relatedDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
+ logger.trace("computedCurrentLocation refName does NOT require updating.");
}
- movementDocModel = getDocModelFromCsid(session, csid);
- if (movementDocModel == null) {
- continue;
- }
- if (logger.isTraceEnabled()) {
- logger.trace("Movement CSID=" + NuxeoUtils.getCsid(movementDocModel));
- }
- // Verify that the Movement record is active. This will also exclude
- // versioned Movement records from the computation of the current
- // location, for tenants that are versioning such records.
- if (!isActiveDocument(movementDocModel)) {
- if (logger.isTraceEnabled()) {
- logger.trace("Skipping this inactive Movement record ...");
- }
- continue;
- }
- GregorianCalendar locationDate =
- (GregorianCalendar) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, LOCATION_DATE_PROPERTY);
- if (locationDate == null) {
- continue;
- }
- if (locationDate.after(mostRecentLocationDate)) {
- mostRecentLocationDate = locationDate;
- location = (String) movementDocModel.getProperty(MOVEMENTS_COMMON_SCHEMA, CURRENT_LOCATION_PROPERTY);
- }
- if (Tools.notBlank(location)) {
- computedCurrentLocation = location;
- }
- }
- return computedCurrentLocation;
- }
-
- /**
- * Returns the CSID for a desired document type from a Relation record,
- * where the relationship involves two specified, different document types.
- *
- * @param relationDocModel a document model for a Relation record.
- * @param desiredDocType a desired document type.
- * @param relatedDocType a related document type.
- * @throws ClientException
- * @return the CSID from the desired document type in the relation. Returns
- * an empty string if the Relation record does not involve both the desired
- * and related document types, or if the desired document type is at both
- * ends of the relation.
- */
- private String getCsidForDesiredDocType(DocumentModel relationDocModel,
- String desiredDocType, String relatedDocType) throws ClientException {
- String csid = "";
- String subjectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_DOCTYPE_PROPERTY);
- String objectDocType = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_DOCTYPE_PROPERTY);
- if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(desiredDocType)) {
- return csid;
- }
- if (subjectDocType.startsWith(desiredDocType) && objectDocType.startsWith(relatedDocType)) {
- csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
- } else if (subjectDocType.startsWith(relatedDocType) && objectDocType.startsWith(desiredDocType)) {
- csid = (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
}
- return csid;
}
}
\ No newline at end of file