\r
</testGroup>\r
\r
- <testGroup ID="invocationModeModeList" autoDeletePOSTS="true">\r
+ <testGroup ID="invocationModeList" autoDeletePOSTS="true">\r
\r
<test ID="createBatchRecord">\r
<method>POST</method>\r
\r
</testGroup>\r
\r
+ <testGroup ID="invocationModeGroup" autoDeletePOSTS="true">\r
+ \r
+ <test ID="createBatchRecord">\r
+ <method>POST</method>\r
+ <uri>/cspace-services/batch</uri>\r
+ <filename>batch/batch-create-updateobjloc.xml</filename>\r
+ <expectedCodes>201</expectedCodes>\r
+ </test>\r
+ \r
+ <test ID="createCollectionObject1">\r
+ <method>POST</method>\r
+ <uri>/cspace-services/collectionobjects</uri>\r
+ <filename>batch/collObj1.xml</filename>\r
+ <expectedCodes>201</expectedCodes>\r
+ </test>\r
+ \r
+ <test ID="createCollectionObject2">\r
+ <method>POST</method>\r
+ <uri>/cspace-services/collectionobjects</uri>\r
+ <filename>batch/collObj1.xml</filename>\r
+ <expectedCodes>201</expectedCodes>\r
+ </test>\r
+ \r
+ <test ID="createMovement1">\r
+ <method>POST</method>\r
+ <uri>/cspace-services/movements</uri>\r
+ <filename>batch/movement.xml</filename>\r
+ <vars>\r
+ <var ID="currentLocation">location-1</var>\r
+ <var ID="locationDate">1900-01-01</var>\r
+ </vars>\r
+ <expectedCodes>201</expectedCodes>\r
+ </test>\r
+ \r
+ <test ID="createMovement2">\r
+ <method>POST</method>\r
+ <uri>/cspace-services/movements</uri>\r
+ <filename>batch/movement.xml</filename>\r
+ <vars>\r
+ <var ID="currentLocation">location-2</var>\r
+ <var ID="locationDate">2000-01-01</var>\r
+ </vars>\r
+ <expectedCodes>201</expectedCodes>\r
+ </test>\r
+ \r
+ <test ID="createMovement3">\r
+ <method>POST</method>\r
+ <uri>/cspace-services/movements</uri>\r
+ <filename>batch/movement.xml</filename>\r
+ <vars>\r
+ <var ID="currentLocation">location-3</var>\r
+ <var ID="locationDate">1800-01-01</var>\r
+ </vars>\r
+ <expectedCodes>201</expectedCodes>\r
+ </test>\r
+ \r
+ <test ID="relateCollectionObject1ToMovement1">\r
+ <method>POST</method>\r
+ <uri>/cspace-services/relations</uri>\r
+ <filename>batch/relation.xml</filename>\r
+ <vars>\r
+ <var ID="subjectCsid">${createCollectionObject1.CSID}</var>\r
+ <var ID="subjectDocumentType">CollectionObject</var>\r
+ <var ID="objectCsid">${createMovement1.CSID}</var>\r
+ <var ID="objectDocumentType">Movement</var>\r
+ </vars>\r
+ <expectedCodes>201</expectedCodes>\r
+ </test>\r
+ \r
+ <test ID="relateMovement2toCollectionObject1">\r
+ <method>POST</method>\r
+ <uri>/cspace-services/relations</uri>\r
+ <filename>batch/relation.xml</filename>\r
+ <vars>\r
+ <var ID="subjectCsid">${createMovement2.CSID}</var>\r
+ <var ID="subjectDocumentType">Movement</var>\r
+ <var ID="objectCsid">${createCollectionObject1.CSID}</var>\r
+ <var ID="objectDocumentType">CollectionObject</var>\r
+ </vars>\r
+ <expectedCodes>201</expectedCodes>\r
+ </test>\r
+ \r
+ <test ID="relateCollectionObject2ToMovement3">\r
+ <method>POST</method>\r
+ <uri>/cspace-services/relations</uri>\r
+ <filename>batch/relation.xml</filename>\r
+ <vars>\r
+ <var ID="subjectCsid">${createCollectionObject2.CSID}</var>\r
+ <var ID="subjectDocumentType">CollectionObject</var>\r
+ <var ID="objectCsid">${createMovement3.CSID}</var>\r
+ <var ID="objectDocumentType">Movement</var>\r
+ </vars>\r
+ <expectedCodes>201</expectedCodes>\r
+ </test>\r
+ \r
+ <test ID="createGroup">\r
+ <method>POST</method>\r
+ <uri>/cspace-services/groups</uri>\r
+ <filename>batch/group.xml</filename>\r
+ <expectedCodes>201</expectedCodes>\r
+ </test>\r
+ \r
+ <test ID="relateCollectionObject1ToGroup">\r
+ <method>POST</method>\r
+ <uri>/cspace-services/relations</uri>\r
+ <filename>batch/relation.xml</filename>\r
+ <vars>\r
+ <var ID="subjectCsid">${createCollectionObject1.CSID}</var>\r
+ <var ID="subjectDocumentType">CollectionObject</var>\r
+ <var ID="objectCsid">${createGroup.CSID}</var>\r
+ <var ID="objectDocumentType">Group</var>\r
+ </vars>\r
+ <expectedCodes>201</expectedCodes>\r
+ </test>\r
+ \r
+ <test ID="relateCollectionObject2ToGroup">\r
+ <method>POST</method>\r
+ <uri>/cspace-services/relations</uri>\r
+ <filename>batch/relation.xml</filename>\r
+ <vars>\r
+ <var ID="subjectCsid">${createCollectionObject2.CSID}</var>\r
+ <var ID="subjectDocumentType">CollectionObject</var>\r
+ <var ID="objectCsid">${createGroup.CSID}</var>\r
+ <var ID="objectDocumentType">Group</var>\r
+ </vars>\r
+ <expectedCodes>201</expectedCodes>\r
+ </test>\r
+ \r
+ <test ID="invokeBatchWithGroup" auth="test" autoDeletePOSTS="false">\r
+ <method>POST</method>\r
+ <uri>/cspace-services/batch/${createBatchRecord.CSID}</uri>\r
+ <filename>batch/batch-invoke-updateobjloc-group.xml</filename>\r
+ <vars>\r
+ <var ID="groupCSID">${createGroup.CSID}</var>\r
+ </vars>\r
+ <expectedCodes>200</expectedCodes>\r
+ </test>\r
+ \r
+ <!-- This CollectionObject record's computedCurrentLocation field should -->\r
+ <!-- have been updated with the currentLocation value from Movement record 2 -->\r
+ <test ID="readUpdatedCollectionObjectRecord1">\r
+ <method>GET</method>\r
+ <uri>/cspace-services/collectionobjects/${createCollectionObject1.CSID}</uri>\r
+ <filename>batch/updateobjloc.xml</filename>\r
+ <response>\r
+ <expected level="ADDOK" />\r
+ <filename>batch/res/collectionobject.res.xml</filename>\r
+ <vars>\r
+ <var ID="computedCurrentLocationValue">${createMovement2.currentLocation}</var>\r
+ </vars>\r
+ </response>\r
+ <expectedCodes>200</expectedCodes>\r
+ </test>\r
+ \r
+ <!-- This CollectionObject record's computedCurrentLocation field should -->\r
+ <!-- have been updated with the currentLocation value from Movement record 3 -->\r
+ <test ID="readUpdatedCollectionObjectRecord2">\r
+ <method>GET</method>\r
+ <uri>/cspace-services/collectionobjects/${createCollectionObject2.CSID}</uri>\r
+ <filename>batch/updateobjloc.xml</filename>\r
+ <response>\r
+ <expected level="ADDOK" />\r
+ <filename>batch/res/collectionobject.res.xml</filename>\r
+ <vars>\r
+ <var ID="computedCurrentLocationValue">${createMovement3.currentLocation}</var>\r
+ </vars>\r
+ </response>\r
+ <expectedCodes>200</expectedCodes>\r
+ </test>\r
+ \r
+ </testGroup>\r
+ \r
<!--\r
WARNING: DANGER, WILL ROBINSON! This test group will process every active\r
CollectionObject record available to the batch job.\r
only at your peril. Use ONLY during development. - ADR 2013-01-10\r
-->\r
\r
- <!--\r
- <testGroup ID="invocationModeModeNoContext" autoDeletePOSTS="true">\r
+ <testGroup ID="invocationModeNoContext" autoDeletePOSTS="true">\r
\r
<test ID="createBatchRecord">\r
<method>POST</method>\r
</test>\r
\r
</testGroup>\r
- -->\r
\r
</xmlReplay>\r
\r
Namespace.getNamespace(
COLLECTIONOBJECTS_COMMON_NAMESPACE_PREFIX,
COLLECTIONOBJECTS_COMMON_NAMESPACE_URI);
+ private final boolean EXCLUDE_DELETED = true;
private final String CLASSNAME = this.getClass().getSimpleName();
private final Logger logger = LoggerFactory.getLogger(this.getClass());
// Initialization tasks
public UpdateObjectLocationBatchJob() {
- setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE, INVOCATION_MODE_LIST, INVOCATION_MODE_NO_CONTEXT));
+ setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE, INVOCATION_MODE_LIST,
+ INVOCATION_MODE_GROUP, INVOCATION_MODE_NO_CONTEXT));
}
/**
}
csids.addAll(listCsids);
} else if (requestIsForInvocationModeGroup()) {
- // This invocation mode is currently not yet supported.
- // FIXME: Add code to getMemberCsidsFromGroup() to support this mode.
String groupCsid = getInvocationContext().getGroupCSID();
- List<String> groupMemberCsids = getMemberCsidsFromGroup(groupCsid);
+ if (Tools.isBlank(groupCsid)) {
+ throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
+ }
+ List<String> groupMemberCsids = getMemberCsidsFromGroup(CollectionObjectClient.SERVICE_NAME, groupCsid);
if (groupMemberCsids.isEmpty()) {
throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
}
}
csids.addAll(noContextCsids);
}
+ if (logger.isInfoEnabled()) {
+ logger.info("Identified " + csids.size() + " total CollectionObject(s) to be processed via the " + CLASSNAME + " batch job");
+ }
// Update the value of the computed current location field for each CollectionObject
setResults(updateComputedCurrentLocations(csids));
}
private InvocationResults updateComputedCurrentLocations(List<String> csids) {
-
ResourceMap resourcemap = getResourceMap();
ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
ResourceBase movementResource = resourcemap.get(MovementClient.SERVICE_NAME);
// For each CollectionObject record
for (String collectionObjectCsid : csids) {
+ // FIXME: Optionally set competition status here to
+ // indicate what percentage of records have been processed.
+
// Skip over soft-deleted CollectionObject records
//
- // (No context invocations already have filtered out those records)
+ // (Invocations using the 'no context' mode have already
+ // filtered out soft-deleted records.)
if (!requestIsForInvocationModeNoContext()) {
if (isRecordDeleted(collectionObjectResource, collectionObjectCsid)) {
if (logger.isTraceEnabled()) {
}
// Get the Movement records related to this CollectionObject record
AbstractCommonList relatedMovements =
- getRelatedRecords(movementResource, collectionObjectCsid, true /* exclude deleted records */);
+ getRelatedRecords(movementResource, collectionObjectCsid, EXCLUDE_DELETED);
// Skip over CollectionObject records that have no related Movement records
if (relatedMovements.getListItem().isEmpty()) {
continue;
// based on data in its related Movement records
computedCurrentLocation = computeCurrentLocation(relatedMovements);
// Skip over CollectionObject records where no current location
- // value can be computed from related Movement records
+ // value can be computed
//
// FIXME: Clarify: it ever necessary to 'unset' a computed
// current location value, by setting it to a null or empty value,
}
private String computeCurrentLocation(AbstractCommonList relatedMovements) {
+ Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
String computedCurrentLocation;
String movementCsid;
- Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
computedCurrentLocation = "";
String currentLocation;
String locationDate;
if (Tools.isBlank(movementCsid)) {
continue;
}
- // Avoid processing any related Movement record more than once,
- // regardless of the directionality of its relation(s) to this
- // CollectionObject record.
+ // Skip over any duplicates in the list, such as records that might
+ // appear as the subject of one relation record and the object of
+ // its reciprocal relation record
if (alreadyProcessedMovementCsids.contains(movementCsid)) {
continue;
} else {
// Movement records processed so far, set the computed current location
// to its current location value.
//
- // Assumes that all values for this element/field will be consistent ISO 8601
- // date/time representations, each of which can be ordered via string comparison.
+ // The following comparison assumes that all values for this element/field
+ // will be consistent ISO 8601 date/time representations, each of which can
+ // be ordered via string comparison.
//
- // If this is *not* the case, we can instead parse and convert these values
+ // If this is *not* the case, we should instead parse and convert these values
// to date/time objects.
if (locationDate.compareTo(mostRecentLocationDate) > 0) {
mostRecentLocationDate = locationDate;
}
}
// Perform the update only if the computed current location value will change
+ // as a result of the update
previousComputedCurrentLocation = getFieldElementValue(collectionObjectPayload,
COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
COMPUTED_CURRENT_LOCATION_ELEMENT_NAME);
&& computedCurrentLocation.equals(previousComputedCurrentLocation)) {
return numUpdated;
}
+ // Perform the update only if there is a non-blank object number available.
+ //
// In the default CollectionObject validation handler, the object number
// is a required field and its (non-blank) value must be present in update
// payloads to successfully perform an update.
- //
- // FIXME: Consider making this check for an object number dependent on the
- // value of a parameter passed in during batch job invocation.
objectNumber = getFieldElementValue(collectionObjectPayload,
COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
OBJECT_NUMBER_ELEMENT_NAME);
if (logger.isTraceEnabled()) {
logger.trace("Object number: " + objectNumber);
}
+ // FIXME: Consider making the requirement that a non-blank object number
+ // be present dependent on the value of a parameter passed in during
+ // batch job invocation, as some implementations may have turned off that
+ // validation requirement.
if (Tools.isBlank(objectNumber)) {
return numUpdated;
}
return isDeleted;
}
- private AbstractCommonList getRelatedRecords(ResourceBase resource, String csid, boolean excludeDeletedRecords)
- throws URISyntaxException, DocumentException {
+ private UriInfo addFilterToExcludeSoftDeletedRecords(UriInfo uriInfo) throws URISyntaxException {
+ if (uriInfo == null) {
+ uriInfo = createUriInfo();
+ }
+ uriInfo.getQueryParameters().add(WorkflowClient.WORKFLOW_QUERY_NONDELETED, Boolean.FALSE.toString());
+ return uriInfo;
+ }
- // Get records related to a record, specified by its CSID,
- // where the record is the object of the relation
+ private AbstractCommonList getRecordsRelatedToCsid(ResourceBase resource, String csid,
+ String relationshipDirection, boolean excludeDeletedRecords) throws URISyntaxException {
UriInfo uriInfo = createUriInfo();
- // FIXME: Get this from constant(s), where appropriate
- uriInfo.getQueryParameters().add("rtObj", csid);
+ uriInfo.getQueryParameters().add(relationshipDirection, csid);
if (excludeDeletedRecords) {
- uriInfo.getQueryParameters().add(WorkflowClient.WORKFLOW_QUERY_NONDELETED, "false");
+ uriInfo = addFilterToExcludeSoftDeletedRecords(uriInfo);
}
-
+ // The 'resource' type used here identifies the record type of the
+ // related records to be retrieved
AbstractCommonList relatedRecords = resource.getList(uriInfo);
if (logger.isTraceEnabled()) {
logger.trace("Identified " + relatedRecords.getTotalItems()
- + " record(s) related to the object record with CSID " + csid);
+ + " record(s) related to the object record via direction " + relationshipDirection + " with CSID " + csid);
}
+ return relatedRecords;
+ }
- // Get records related to a record, specified by its CSID,
- // where the record is the subject of the relation
- // FIXME: Get query string(s) from constant(s), where appropriate
- uriInfo = createUriInfo();
- uriInfo.getQueryParameters().add("rtSbj", csid);
- if (excludeDeletedRecords) {
- uriInfo.getQueryParameters().add(WorkflowClient.WORKFLOW_QUERY_NONDELETED, "false");
- }
- AbstractCommonList reverseRelatedRecords = resource.getList(uriInfo);
- if (logger.isTraceEnabled()) {
- logger.trace("Identified " + reverseRelatedRecords.getTotalItems()
- + " record(s) related to the subject record with CSID " + csid);
- }
+ /**
+ * Returns the records of a specified type that are related to a specified
+ * record, where that record is the object of the relation.
+ *
+ * @param resource a resource. The type of this resource determines the type
+ * of related records that are returned.
+ * @param csid a CSID identifying a record
+ * @param excludeDeletedRecords true if 'soft-deleted' records should be
+ * excluded from results; false if those records should be included
+ * @return a list of records of a specified type, related to a specified
+ * record
+ * @throws URISyntaxException
+ */
+ private AbstractCommonList getRecordsRelatedToObjectCsid(ResourceBase resource, String csid, boolean excludeDeletedRecords) throws URISyntaxException {
+ return getRecordsRelatedToCsid(resource, csid, "rtObj", excludeDeletedRecords);
+ }
- // If the second list contains any related records,
- // merge it into the first list
- if (reverseRelatedRecords.getListItem().size() > 0) {
- relatedRecords.getListItem().addAll(reverseRelatedRecords.getListItem());
- }
+ /**
+ * Returns the records of a specified type that are related to a specified
+ * record, where that record is the subject of the relation.
+ *
+ * @param resource a resource. The type of this resource determines the type
+ * of related records that are returned.
+ * @param csid a CSID identifying a record
+ * @param excludeDeletedRecords true if 'soft-deleted' records should be
+ * excluded from results; false if those records should be included
+ * @return a list of records of a specified type, related to a specified
+ * record
+ * @throws URISyntaxException
+ */
+ private AbstractCommonList getRecordsRelatedToSubjectCsid(ResourceBase resource, String csid, boolean excludeDeletedRecords) throws URISyntaxException {
+ return getRecordsRelatedToCsid(resource, csid, "rtSbj", excludeDeletedRecords);
+ }
+ private AbstractCommonList getRelatedRecords(ResourceBase resource, String csid, boolean excludeDeletedRecords)
+ throws URISyntaxException, DocumentException {
+ AbstractCommonList relatedRecords = new AbstractCommonList();
+ AbstractCommonList recordsRelatedToObjectCSID = getRecordsRelatedToObjectCsid(resource, csid, excludeDeletedRecords);
+ AbstractCommonList recordsRelatedToSubjectCSID = getRecordsRelatedToSubjectCsid(resource, csid, excludeDeletedRecords);
+ // If either list contains any related records, merge in its items
+ if (recordsRelatedToObjectCSID.getListItem().size() > 0) {
+ relatedRecords.getListItem().addAll(recordsRelatedToObjectCSID.getListItem());
+ }
+ if (recordsRelatedToSubjectCSID.getListItem().size() > 0) {
+ relatedRecords.getListItem().addAll(recordsRelatedToSubjectCSID.getListItem());
+ }
if (logger.isTraceEnabled()) {
logger.trace("Identified a total of " + relatedRecords.getListItem().size()
+ " record(s) related to the record with CSID " + csid);
}
-
return relatedRecords;
}
- // Stub method, as this invocation mode is not currently supported
- private List<String> getMemberCsidsFromGroup(String groupCsid) throws URISyntaxException {
- List<String> memberCsids = Collections.emptyList();
+ private List<String> getCsidsList(AbstractCommonList list) {
+ List<String> csids = new ArrayList<String>();
+ for (AbstractCommonList.ListItem listitem : list.getListItem()) {
+ csids.add(AbstractCommonListUtils.ListItemGetCSID(listitem));
+ }
+ return csids;
+ }
+
+ private List<String> getMemberCsidsFromGroup(String serviceName, String groupCsid) throws URISyntaxException, DocumentException {
+ ResourceMap resourcemap = getResourceMap();
+ ResourceBase resource = resourcemap.get(serviceName);
+ return getMemberCsidsFromGroup(resource, groupCsid);
+ }
+
+ private List<String> getMemberCsidsFromGroup(ResourceBase resource, String groupCsid) throws URISyntaxException, DocumentException {
+ // The 'resource' type used here identifies the record type of the
+ // related records to be retrieved
+ AbstractCommonList relatedRecords =
+ getRelatedRecords(resource, groupCsid, EXCLUDE_DELETED);
+ List<String> memberCsids = getCsidsList(relatedRecords);
return memberCsids;
}
private List<String> getNoContextCsids() throws URISyntaxException {
- List<String> noContextCsids = new ArrayList<String>();
ResourceMap resourcemap = getResourceMap();
ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
UriInfo uriInfo = createUriInfo();
- uriInfo.getQueryParameters().add(WorkflowClient.WORKFLOW_QUERY_NONDELETED, "false");
+ uriInfo = addFilterToExcludeSoftDeletedRecords(uriInfo);
AbstractCommonList collectionObjects = collectionObjectResource.getList(uriInfo);
- for (AbstractCommonList.ListItem collectionObjectRecord : collectionObjects.getListItem()) {
- noContextCsids.add(AbstractCommonListUtils.ListItemGetCSID(collectionObjectRecord));
- }
- if (logger.isInfoEnabled()) {
- logger.info("Identified " + noContextCsids.size()
- + " total active CollectionObjects to process in the 'no context' invocation mode.");
- }
+ List<String> noContextCsids = getCsidsList(collectionObjects);
return noContextCsids;
}
}