]> git.aero2k.de Git - tmp/jakarta-migration.git/commitdiff
CSPACE-5728: Batch job to update computed current location values now also supports...
authorAron Roberts <aron@socrates.berkeley.edu>
Fri, 11 Jan 2013 23:42:31 +0000 (15:42 -0800)
committerAron Roberts <aron@socrates.berkeley.edu>
Fri, 11 Jan 2013 23:42:31 +0000 (15:42 -0800)
services/IntegrationTests/src/test/resources/test-data/xmlreplay/batch/batch-create-updateobjloc-nocontext.xml
services/IntegrationTests/src/test/resources/test-data/xmlreplay/batch/batch-create-updateobjloc.xml
services/IntegrationTests/src/test/resources/test-data/xmlreplay/batch/batch-invoke-updateobjloc-group.xml [new file with mode: 0644]
services/IntegrationTests/src/test/resources/test-data/xmlreplay/batch/batch-update-object-loc.xml
services/IntegrationTests/src/test/resources/test-data/xmlreplay/batch/group.xml [new file with mode: 0644]
services/IntegrationTests/src/test/resources/test-data/xmlreplay/xml-replay-master.xml
services/batch/service/src/main/java/org/collectionspace/services/batch/nuxeo/UpdateObjectLocationBatchJob.java

index 91198eb9321fbdd41d6ea6054abfccc66b79e82a..c6253a472d4ca3f34858cda1003b98433cd2d4ff 100644 (file)
@@ -9,7 +9,7 @@
         </forDocTypes>\r
         <supportsSingleDoc>true</supportsSingleDoc>\r
         <supportsDocList>true</supportsDocList>\r
-        <supportsGroup>false</supportsGroup>\r
+        <supportsGroup>true</supportsGroup>\r
         <supportsNoContext>true</supportsNoContext>\r
         <createsNewFocus>false</createsNewFocus>\r
         <className>org.collectionspace.services.batch.nuxeo.UpdateObjectLocationBatchJob</className>\r
index afe6d20e3b29469e6677dbf0706f81ada9b4da56..7e47ad68883ec6850613559217246104d1f032f8 100644 (file)
@@ -9,7 +9,7 @@
         </forDocTypes>\r
         <supportsSingleDoc>true</supportsSingleDoc>\r
         <supportsDocList>true</supportsDocList>\r
-        <supportsGroup>false</supportsGroup>\r
+        <supportsGroup>true</supportsGroup>\r
         <supportsNoContext>false</supportsNoContext>\r
         <createsNewFocus>false</createsNewFocus>\r
         <className>org.collectionspace.services.batch.nuxeo.UpdateObjectLocationBatchJob</className>\r
diff --git a/services/IntegrationTests/src/test/resources/test-data/xmlreplay/batch/batch-invoke-updateobjloc-group.xml b/services/IntegrationTests/src/test/resources/test-data/xmlreplay/batch/batch-invoke-updateobjloc-group.xml
new file mode 100644 (file)
index 0000000..9d060f2
--- /dev/null
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\r
+<ns2:invocationContext\r
+    xmlns:ns2="http://collectionspace.org/services/common/invocable"\r
+    xmlns:ns3="http://collectionspace.org/services/jaxb">\r
+    <mode>group</mode>\r
+    <docType>CollectionObject</docType>\r
+    <singleCSID></singleCSID>\r
+    <groupCSID>${groupCSID}</groupCSID>\r
+    <listCSIDs></listCSIDs>\r
+    <params></params>\r
+</ns2:invocationContext>\r
+\r
+\r
index e96a8b613a00bdfa05295cb254506c4d1ecada0d..8bc0bce3e43ba293b2912af8650c93ca3bd95255 100644 (file)
         \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
diff --git a/services/IntegrationTests/src/test/resources/test-data/xmlreplay/batch/group.xml b/services/IntegrationTests/src/test/resources/test-data/xmlreplay/batch/group.xml
new file mode 100644 (file)
index 0000000..d238fcc
--- /dev/null
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<document name="groups">
+
+    <ns2:groups_common xmlns:ns2="http://collectionspace.org/group">
+        <title>A group title</title>
+    </ns2:groups_common>
+
+</document>
+
+
index 46895cdeb8d9f0657ef6fff7a59dd8a9d256a444..761e4feaeee12a205c04490fe986591f8b7a6e94 100644 (file)
@@ -50,6 +50,7 @@
     <!--\r
     <run controlFile="batch/batch-update-object-loc.xml" testGroup="invocationModeSingle" />\r
     <run controlFile="batch/batch-update-object-loc.xml" testGroup="invocationModeList" />\r
+    <run controlFile="batch/batch-update-object-loc.xml" testGroup="invocationModeGroup" />\r
     -->\r
     \r
     <!-- Broken tests -->\r
index f29f498384a97be6d0e3d53c1658772a1310cfe3..b161d076a3e6d1fa33c81d4de5a998b215e82229 100644 (file)
@@ -56,12 +56,14 @@ public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
             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));
     }
 
     /**
@@ -92,10 +94,11 @@ public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
                 }
                 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);
                 }
@@ -107,6 +110,9 @@ public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
                 }
                 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));
@@ -120,7 +126,6 @@ public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
     }
 
     private InvocationResults updateComputedCurrentLocations(List<String> csids) {
-
         ResourceMap resourcemap = getResourceMap();
         ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
         ResourceBase movementResource = resourcemap.get(MovementClient.SERVICE_NAME);
@@ -132,9 +137,13 @@ public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
             // 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()) {
@@ -145,7 +154,7 @@ public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
                 }
                 // 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;
@@ -154,7 +163,7 @@ public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
                 // 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,
@@ -183,9 +192,9 @@ public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
     }
 
     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;
@@ -195,9 +204,9 @@ public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
             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 {
@@ -219,10 +228,11 @@ public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
             // 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;
@@ -253,6 +263,7 @@ public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
             }
         }
         // 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);
@@ -260,18 +271,21 @@ public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
                 && 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;
         }
@@ -375,72 +389,114 @@ public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
         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;
     }
 }