From 6bd87711af6066566eb2e82baebd39af4c252710 Mon Sep 17 00:00:00 2001 From: Aron Roberts Date: Wed, 12 Dec 2012 17:40:38 -0800 Subject: [PATCH] CSPACE-5727: Refactored into abstract and implementation methods for easier extensibility; e.g. for updating moveable locations (crates or equivalent) for some implementers. --- .../AbstractUpdateObjectLocationValues.java | 422 +++++++++++++++ .../listener/UpdateObjectLocationOnMove.java | 490 ++---------------- 2 files changed, 471 insertions(+), 441 deletions(-) create mode 100644 3rdparty/nuxeo/nuxeo-platform-listener/updateobjectlocationonmove/src/main/java/org/collectionspace/services/listener/AbstractUpdateObjectLocationValues.java diff --git a/3rdparty/nuxeo/nuxeo-platform-listener/updateobjectlocationonmove/src/main/java/org/collectionspace/services/listener/AbstractUpdateObjectLocationValues.java b/3rdparty/nuxeo/nuxeo-platform-listener/updateobjectlocationonmove/src/main/java/org/collectionspace/services/listener/AbstractUpdateObjectLocationValues.java new file mode 100644 index 000000000..1df44ebcd --- /dev/null +++ b/3rdparty/nuxeo/nuxeo-platform-listener/updateobjectlocationonmove/src/main/java/org/collectionspace/services/listener/AbstractUpdateObjectLocationValues.java @@ -0,0 +1,422 @@ +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 collectionObjectCsids = new HashSet(); // 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 alreadyProcessedMovementCsids = new HashSet(); + 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 diff --git a/3rdparty/nuxeo/nuxeo-platform-listener/updateobjectlocationonmove/src/main/java/org/collectionspace/services/listener/UpdateObjectLocationOnMove.java b/3rdparty/nuxeo/nuxeo-platform-listener/updateobjectlocationonmove/src/main/java/org/collectionspace/services/listener/UpdateObjectLocationOnMove.java index 6b8ca9a78..ba3330089 100644 --- a/3rdparty/nuxeo/nuxeo-platform-listener/updateobjectlocationonmove/src/main/java/org/collectionspace/services/listener/UpdateObjectLocationOnMove.java +++ b/3rdparty/nuxeo/nuxeo-platform-listener/updateobjectlocationonmove/src/main/java/org/collectionspace/services/listener/UpdateObjectLocationOnMove.java @@ -1,483 +1,91 @@ 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 relevantDocTypesList = new ArrayList(); - 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 collectionObjectCsids = new HashSet(); // 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 docModelsToUpdate = new HashMap(); - 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 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 -- 2.47.3