/**
   * Returns the CSIDs of CollectionObject records that are related to a Movement record.
   *
   * @param movementCsid the CSID of a Movement record.
   * @param coreSession a repository session.
   * @throws ClientException
   * @return the CSIDs of the CollectionObject records, if any, which are related to the Movement
   *     record.
   */
  private Set<String> getCollectionObjectCsidsRelatedToMovement(
      String movementCsid, CoreSession coreSession) throws ClientException {

    Set<String> csids = new HashSet<>();

    // 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.
    //
    // 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 csids;
    }
    // Iterate through the list of Relation records found and build
    // a list of CollectionObject CSIDs, by extracting the relevant CSIDs
    // from those Relation records.
    String csid;
    for (DocumentModel relationDocModel : relationDocModels) {
      csid =
          getCsidForDesiredDocTypeFromRelation(
              relationDocModel, COLLECTIONOBJECT_DOCTYPE, MOVEMENT_DOCTYPE);
      if (Tools.notBlank(csid)) {
        csids.add(csid);
      }
    }
    return csids;
  }
 /**
  * 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);
 }
  /**
   * Returns the most recent Movement record related to a CollectionObject.
   *
   * <p>This method currently returns the related Movement record with the latest (i.e. most recent
   * in time) Location Date field value.
   *
   * @param session a repository session.
   * @param collectionObjectCsid a CollectionObject identifier (CSID)
   * @throws ClientException
   * @return the most recent Movement record related to the CollectionObject identified by the
   *     supplied CSID.
   */
  protected static DocumentModel getMostRecentMovement(
      CoreSession session, String collectionObjectCsid) throws ClientException {
    DocumentModel mostRecentMovementDocModel = null;
    // 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 mostRecentMovementDocModel;
    } else {
      if (logger.isTraceEnabled()) {
        logger.trace(
            "Found "
                + relationDocModels.size()
                + " relations to Movement records to/from this CollectionObject record.");
      }
    }
    // Iterate through related movement records, to find the related
    // Movement record with the most recent location date.
    GregorianCalendar mostRecentLocationDate = EARLIEST_COMPARISON_DATE;
    // Note: the following value is used to compare any two records, rather
    // than to identify the most recent value so far encountered. Thus, it may
    // occasionally be set backward or forward in time, within the loop below.
    GregorianCalendar comparisonCreationDate = EARLIEST_COMPARISON_DATE;
    DocumentModel movementDocModel;
    Set<String> alreadyProcessedMovementCsids = new HashSet<>();
    String relatedMovementCsid;
    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.
      relatedMovementCsid =
          (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, SUBJECT_CSID_PROPERTY);
      if (relatedMovementCsid.equals(collectionObjectCsid)) {
        relatedMovementCsid =
            (String) relationDocModel.getProperty(RELATIONS_COMMON_SCHEMA, OBJECT_CSID_PROPERTY);
      }
      if (Tools.isBlank(relatedMovementCsid)) {
        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(relatedMovementCsid)) {
        continue;
      } else {
        alreadyProcessedMovementCsids.add(relatedMovementCsid);
      }
      if (logger.isTraceEnabled()) {
        logger.trace("Related movement CSID=" + relatedMovementCsid);
      }
      movementDocModel = getDocModelFromCsid(session, relatedMovementCsid);
      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 the current Movement record lacks a location date, it cannot
      // be established as the most recent Movement record; skip over it.
      if (locationDate == null) {
        continue;
      }
      GregorianCalendar creationDate =
          (GregorianCalendar)
              movementDocModel.getProperty(COLLECTIONSPACE_CORE_SCHEMA, CREATED_AT_PROPERTY);
      if (locationDate.after(mostRecentLocationDate)) {
        mostRecentLocationDate = locationDate;
        if (creationDate != null) {
          comparisonCreationDate = creationDate;
        }
        mostRecentMovementDocModel = movementDocModel;
        // If the current Movement record's location date is identical
        // to that of the (at this time) most recent Movement record, then
        // instead compare the two records using their creation date values
      } else if (locationDate.compareTo(mostRecentLocationDate) == 0) {
        if (creationDate != null && creationDate.after(comparisonCreationDate)) {
          // The most recent location date value doesn't need to be
          // updated here, as the two records' values are identical
          comparisonCreationDate = creationDate;
          mostRecentMovementDocModel = movementDocModel;
        }
      }
    }
    return mostRecentMovementDocModel;
  }