]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
e5b97d128be6559606f11e6fd57613879d56e840
[tmp/jakarta-migration.git] /
1 package org.collectionspace.services.batch.nuxeo;
2
3 import java.io.StringReader;
4 import java.net.URI;
5 import java.net.URISyntaxException;
6 import java.util.ArrayList;
7 import java.util.Arrays;
8 import java.util.Collections;
9 import java.util.HashSet;
10 import java.util.List;
11 import java.util.Set;
12
13 import javax.ws.rs.core.PathSegment;
14 import javax.ws.rs.core.UriInfo;
15
16 import org.collectionspace.services.batch.AbstractBatchInvocable;
17 import org.collectionspace.services.batch.BatchCommon;
18 import org.collectionspace.services.client.AbstractCommonListUtils;
19 import org.collectionspace.services.client.CollectionObjectClient;
20 import org.collectionspace.services.client.IClientQueryParams;
21 import org.collectionspace.services.client.IQueryManager;
22 import org.collectionspace.services.client.MovementClient;
23 import org.collectionspace.services.client.PoxPayloadOut;
24 import org.collectionspace.services.client.workflow.WorkflowClient;
25 import org.collectionspace.services.common.NuxeoBasedResource;
26 import org.collectionspace.services.common.ResourceMap;
27 import org.collectionspace.services.common.api.RefNameUtils;
28 import org.collectionspace.services.common.api.Tools;
29 import org.collectionspace.services.common.invocable.InvocationResults;
30 import org.collectionspace.services.common.query.UriInfoImpl;
31 import org.collectionspace.services.jaxb.AbstractCommonList;
32 import org.dom4j.DocumentException;
33 //import org.jboss.resteasy.specimpl.UriInfoImpl;
34 import org.jdom.Document;
35 import org.jdom.Element;
36 import org.jdom.Namespace;
37 import org.jdom.input.SAXBuilder;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
42
43     // FIXME: Where appropriate, get from existing constants rather than local declarations
44     private final static String COMPUTED_CURRENT_LOCATION_ELEMENT_NAME = "computedCurrentLocation";
45     private final static String CSID_ELEMENT_NAME = "csid";
46     private final static String CURRENT_LOCATION_ELEMENT_NAME = "currentLocation";
47     private final static String LIFECYCLE_STATE_ELEMENT_NAME = "currentLifeCycleState";
48     private final static String LOCATION_DATE_ELEMENT_NAME = "locationDate";
49     private final static String OBJECT_NUMBER_ELEMENT_NAME = "objectNumber";
50     private final static String UPDATE_DATE_ELEMENT_NAME = "updatedAt";
51     private final static String WORKFLOW_COMMON_SCHEMA_NAME = "workflow_common";
52     private final static String WORKFLOW_COMMON_NAMESPACE_PREFIX = "ns2";
53     private final static String WORKFLOW_COMMON_NAMESPACE_URI =
54             "http://collectionspace.org/services/workflow";
55     private final static Namespace WORKFLOW_COMMON_NAMESPACE =
56             Namespace.getNamespace(
57             WORKFLOW_COMMON_NAMESPACE_PREFIX,
58             WORKFLOW_COMMON_NAMESPACE_URI);
59     private final static String COLLECTIONOBJECTS_COMMON_SCHEMA_NAME = "collectionobjects_common";
60     private final static String COLLECTIONOBJECTS_COMMON_NAMESPACE_PREFIX = "ns2";
61     private final static String COLLECTIONOBJECTS_COMMON_NAMESPACE_URI =
62             "http://collectionspace.org/services/collectionobject";
63     private final static Namespace COLLECTIONOBJECTS_COMMON_NAMESPACE =
64             Namespace.getNamespace(
65             COLLECTIONOBJECTS_COMMON_NAMESPACE_PREFIX,
66             COLLECTIONOBJECTS_COMMON_NAMESPACE_URI);
67         private static final int DEFAULT_PAGE_SIZE = 1000;
68     private final boolean EXCLUDE_DELETED = true;
69     private final String CLASSNAME = this.getClass().getSimpleName();
70     private final Logger logger = LoggerFactory.getLogger(this.getClass());
71
72     // Initialization tasks
73     public UpdateObjectLocationBatchJob() {
74         setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE, INVOCATION_MODE_LIST,
75                 INVOCATION_MODE_GROUP, INVOCATION_MODE_NO_CONTEXT));
76     }
77
78         @Override
79         public void run(BatchCommon batchCommon) {
80                 String errMsg = String.format("%s class does not support run(BatchCommon batchCommon) method.", getClass().getName());
81                 throw new java.lang.UnsupportedOperationException(errMsg);
82         }
83
84     /**
85      * The main work logic of the batch job. Will be called after setContext.
86      */
87     @Override
88     public void run() {
89
90         setCompletionStatus(STATUS_MIN_PROGRESS);
91
92         try {
93
94             List<String> csids = new ArrayList<String>();
95
96             // Build a list of CollectionObject records to process via this
97             // batch job, depending on the invocation mode requested.
98             if (requestIsForInvocationModeSingle()) {
99                 String singleCsid = getInvocationContext().getSingleCSID();
100                 if (Tools.isBlank(singleCsid)) {
101                     throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
102                 } else {
103                     csids.add(singleCsid);
104                 }
105             } else if (requestIsForInvocationModeList()) {
106                 List<String> listCsids = getListCsids();
107                 if (listCsids.isEmpty()) {
108                     throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
109                 }
110                 csids.addAll(listCsids);
111             } else if (requestIsForInvocationModeGroup()) {
112                 String groupCsid = getInvocationContext().getGroupCSID();
113                 if (Tools.isBlank(groupCsid)) {
114                     throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
115                 }
116                 List<String> groupMemberCsids = getMemberCsidsFromGroup(CollectionObjectClient.SERVICE_NAME, groupCsid);
117                 if (groupMemberCsids.isEmpty()) {
118                     throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
119                 }
120                 csids.addAll(groupMemberCsids);
121             } else if (requestIsForInvocationModeNoContext()) {
122                 List<String> noContextCsids = getNoContextCsids();
123                 if (noContextCsids.isEmpty()) {
124                     throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
125                 }
126                 csids.addAll(noContextCsids);
127             }
128             if (logger.isInfoEnabled()) {
129                 logger.info("Identified " + csids.size() + " total CollectionObject(s) to be processed via the " + CLASSNAME + " batch job");
130             }
131
132             // Update the value of the computed current location field for each CollectionObject
133             setResults(updateComputedCurrentLocations(csids));
134             setCompletionStatus(STATUS_COMPLETE);
135
136         } catch (Exception e) {
137             String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage();
138             setErrorResult(errMsg);
139         }
140
141     }
142
143     private InvocationResults updateComputedCurrentLocations(List<String> csids) {
144         ResourceMap resourcemap = getResourceMap();
145         NuxeoBasedResource collectionObjectResource = (NuxeoBasedResource) resourcemap.get(CollectionObjectClient.SERVICE_NAME);
146         NuxeoBasedResource movementResource = (NuxeoBasedResource) resourcemap.get(MovementClient.SERVICE_NAME);
147         long numUpdated = 0;
148         long processed = 0;
149
150         long recordsToProcess = csids.size();
151         long logInterval = recordsToProcess / 10 + 2;
152         try {
153
154             // For each CollectionObject record
155             for (String collectionObjectCsid : csids) {
156                 
157                 // Log progress at INFO level
158                 if (processed % logInterval == 0) {
159                         logger.info(String.format("Recalculated computed location for %d of %d cataloging records.",
160                                         processed, recordsToProcess));
161                 }
162                 processed++;
163
164                 // Skip over soft-deleted CollectionObject records
165                 //
166                 // (Invocations using the 'no context' mode have already
167                 // filtered out soft-deleted records.)
168                 if (!requestIsForInvocationModeNoContext()) {
169                     if (isRecordDeleted(collectionObjectResource, collectionObjectCsid)) {
170                         if (logger.isTraceEnabled()) {
171                             logger.trace("Skipping soft-deleted CollectionObject record with CSID " + collectionObjectCsid);
172                         }
173                         continue;
174                     }
175                 }
176                 // Get the Movement records related to this CollectionObject record
177                 AbstractCommonList relatedMovements =
178                         getRelatedRecords(movementResource, collectionObjectCsid, EXCLUDE_DELETED);
179                 // Skip over CollectionObject records that have no related Movement records
180                 if (relatedMovements.getListItem().isEmpty()) {
181                     continue;
182                 }
183                 // Get the most recent 'suitable' Movement record, one which
184                 // contains both a location date and a current location value
185                 AbstractCommonList.ListItem mostRecentMovement = getMostRecentMovement(relatedMovements);
186                 // Skip over CollectionObject records where no suitable
187                 // most recent Movement record can be identified.
188                 //
189                 // FIXME: Clarify: it ever necessary to 'unset' a computed
190                 // current location value, by setting it to a null or empty value,
191                 // if that value is no longer obtainable from related Movement
192                 // records, if any?
193                 if (mostRecentMovement == null) {
194                     continue;
195                 }
196                 // Update the value of the computed current location field
197                 // (and, via subclasses, this and/or other relevant fields)
198                 // in the CollectionObject record
199                 numUpdated = updateCollectionObjectValues(collectionObjectResource,
200                         collectionObjectCsid, mostRecentMovement, resourcemap, numUpdated);
201             }
202
203         } catch (Exception e) {
204             String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage() + " ";
205             errMsg = errMsg + "Successfully updated " + numUpdated + " CollectionObject record(s) prior to error.";
206             logger.error(errMsg);
207             setErrorResult(errMsg);
208             getResults().setNumAffected(numUpdated);
209             return getResults();
210         }
211
212         logger.info("Updated computedCurrentLocation values in " + numUpdated + " CollectionObject record(s).");
213         getResults().setNumAffected(numUpdated);
214         return getResults();
215     }
216     
217     //
218     // Returns the number of distinct/unique CSID values in the list
219     //
220     private int getNumberOfDistinceRecords(AbstractCommonList abstractCommonList) {
221         Set<String> resultSet = new HashSet<String>();
222         
223         for (AbstractCommonList.ListItem listItem : abstractCommonList.getListItem()) {
224                 String csid = AbstractCommonListUtils.ListItemGetElementValue(listItem, CSID_ELEMENT_NAME);
225                 if (!Tools.isBlank(csid)) {
226                     resultSet.add(csid);
227                 }
228         }
229         
230         return resultSet.size();
231     }
232
233     private AbstractCommonList.ListItem getMostRecentMovement(AbstractCommonList relatedMovements) {
234         Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
235         AbstractCommonList.ListItem mostRecentMovement = null;
236         String movementCsid;
237         String currentLocation;
238         String locationDate;
239         String updateDate;
240         String mostRecentLocationDate = "";
241         String comparisonUpdateDate = "";
242         
243         //
244         // If there is only one related movement record, then return it as the most recent
245         // movement record -if it's current location element is not empty.
246         //
247         if (getNumberOfDistinceRecords(relatedMovements) == 1) {
248                 mostRecentMovement = relatedMovements.getListItem().get(0);
249             currentLocation = AbstractCommonListUtils.ListItemGetElementValue(mostRecentMovement, CURRENT_LOCATION_ELEMENT_NAME);
250             if (Tools.isBlank(currentLocation)) {
251                 mostRecentMovement = null;
252             }
253             return mostRecentMovement;
254         }
255         
256         for (AbstractCommonList.ListItem movementListItem : relatedMovements.getListItem()) {
257             movementCsid = AbstractCommonListUtils.ListItemGetElementValue(movementListItem, CSID_ELEMENT_NAME);
258             if (Tools.isBlank(movementCsid)) {
259                 continue;
260             }
261             // Skip over any duplicates in the list, such as records that might
262             // appear as the subject of one relation record and the object of
263             // its reciprocal relation record
264             if (alreadyProcessedMovementCsids.contains(movementCsid)) {
265                 continue;
266             } else {
267                 alreadyProcessedMovementCsids.add(movementCsid);
268             }
269             locationDate = AbstractCommonListUtils.ListItemGetElementValue(movementListItem, LOCATION_DATE_ELEMENT_NAME);
270             if (Tools.isBlank(locationDate)) {
271                 continue;
272             }
273             updateDate = AbstractCommonListUtils.ListItemGetElementValue(movementListItem, UPDATE_DATE_ELEMENT_NAME);
274             if (Tools.isBlank(updateDate)) {
275                 continue;
276             }
277             currentLocation = AbstractCommonListUtils.ListItemGetElementValue(movementListItem, CURRENT_LOCATION_ELEMENT_NAME);
278             if (Tools.isBlank(currentLocation)) {
279                 continue;
280             }
281             // Validate that this Movement record's currentLocation value parses
282             // successfully as an item refName, before identifying that record
283             // as the most recent Movement.
284             //
285             // TODO: Consider making this optional validation, in turn dependent on the
286             // value of a parameter passed in during batch job invocation.
287             if (RefNameUtils.parseAuthorityTermInfo(currentLocation) == null) {
288                 logger.warn(String.format("Could not parse current location refName '%s' in Movement record",
289                     currentLocation));
290                  continue;
291             }
292            
293             if (logger.isTraceEnabled()) {
294                 logger.trace("Location date value = " + locationDate);
295                 logger.trace("Update date value = " + updateDate);
296                 logger.trace("Current location value = " + currentLocation);
297             }
298             
299             // If this record's location date value is more recent than that of other
300             // Movement records processed so far, set the current Movement record
301             // as the most recent Movement.
302             //
303             // The following comparison assumes that all values for this element/field
304             // will be consistent ISO 8601 date/time representations, each of which can
305             // be ordered via string comparison.
306             //
307             // If this is *not* the case, we should instead parse and convert these values
308             // to date/time objects.
309             if (locationDate.compareTo(mostRecentLocationDate) > 0) {
310                 mostRecentLocationDate = locationDate;
311                 mostRecentMovement = movementListItem;
312                 comparisonUpdateDate = updateDate;
313             } else if (locationDate.compareTo(mostRecentLocationDate) == 0) {
314                 // If the two location dates match, then use a tiebreaker
315                 if (updateDate.compareTo(comparisonUpdateDate) > 0) {
316                     // The most recent location date value doesn't need to be
317                     // updated here, as the two records' values are identical
318                     mostRecentMovement = movementListItem;
319                     comparisonUpdateDate = updateDate;
320                 }
321             }
322
323         }
324         
325         return mostRecentMovement;
326     }
327
328     // This method can be overridden and extended to update a custom set of
329     // values in the CollectionObject record by pulling in values from its
330     // most recent related Movement record.
331     //
332     // Note: any such values must first be exposed in Movement list items,
333     // in turn via configuration in Services tenant bindings ("listResultsField").
334     protected long updateCollectionObjectValues(NuxeoBasedResource collectionObjectResource,
335             String collectionObjectCsid,
336             AbstractCommonList.ListItem mostRecentMovement,
337             ResourceMap resourcemap, long numUpdated)
338             throws DocumentException, URISyntaxException {
339         PoxPayloadOut collectionObjectPayload;
340         String computedCurrentLocation;
341         String objectNumber;
342         String previousComputedCurrentLocation;
343
344         collectionObjectPayload = findByCsid(collectionObjectResource, collectionObjectCsid);
345         if (Tools.isBlank(collectionObjectPayload.toXML())) {
346             return numUpdated;
347         } else {
348             if (logger.isTraceEnabled()) {
349                 logger.trace("Payload: " + "\n" + collectionObjectPayload);
350             }
351         }
352         // Perform the update only if the computed current location value will change
353         // as a result of the update
354         computedCurrentLocation =
355                 AbstractCommonListUtils.ListItemGetElementValue(mostRecentMovement, CURRENT_LOCATION_ELEMENT_NAME);
356         previousComputedCurrentLocation = getFieldElementValue(collectionObjectPayload,
357                 COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
358                 COMPUTED_CURRENT_LOCATION_ELEMENT_NAME);
359         if (!shouldUpdateLocation(previousComputedCurrentLocation, computedCurrentLocation)) {
360             return numUpdated;
361         }
362     
363         // Perform the update only if there is a non-blank object number available.
364         //
365         // In the default CollectionObject validation handler, the object number
366         // is a required field and its (non-blank) value must be present in update
367         // payloads to successfully perform an update.
368         objectNumber = getFieldElementValue(collectionObjectPayload,
369                 COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
370                 OBJECT_NUMBER_ELEMENT_NAME);
371         if (logger.isTraceEnabled()) {
372             logger.trace("Object number: " + objectNumber);
373         }
374         // FIXME: Consider making the requirement that a non-blank object number
375         // be present dependent on the value of a parameter passed in during
376         // batch job invocation, as some implementations may have turned off that
377         // validation requirement.
378         if (Tools.isBlank(objectNumber)) {
379             return numUpdated;
380         }
381
382         // At this point in the code, the most recent related Movement record
383         // should not have a null current location, as such records are
384         // excluded from consideration altogether in getMostRecentMovement().
385         // This is a redundant fallback check, in case that code somehow fails
386         // or is modified or deleted.
387         if (computedCurrentLocation == null) {
388             return numUpdated;
389         }
390
391         // Update the location.
392         String collectionObjectUpdatePayload =
393                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
394                 + "<document name=\"collectionobject\">"
395                 + "  <ns2:collectionobjects_common "
396                 + "      xmlns:ns2=\"http://collectionspace.org/services/collectionobject\">"
397                 + "    <objectNumber>" + objectNumber + "</objectNumber>"
398                 + "    <computedCurrentLocation>" + computedCurrentLocation + "</computedCurrentLocation>"
399                 + "  </ns2:collectionobjects_common>"
400                 + "</document>";
401         if (logger.isTraceEnabled()) {
402             logger.trace("Update payload: " + "\n" + collectionObjectUpdatePayload);
403         }
404         
405         //
406         // Update the record and save the response for debugging message
407         //
408         UriInfo uriInfo = this.setupQueryParamForUpdateRecords(); // Determines if we'll updated the updateAt and updatedBy core values
409         byte[] responseBytes = collectionObjectResource.update(getServiceContext(), resourcemap, uriInfo, collectionObjectCsid,
410                 collectionObjectUpdatePayload);
411         numUpdated++;
412
413         if (logger.isDebugEnabled()) {
414                 logger.debug(String.format("Batch resource: Resonse from collectionobject (cataloging record) update: %s", new String(responseBytes)));
415         }
416         
417         if (logger.isTraceEnabled()) {
418             logger.trace("Computed current location value for CollectionObject " + collectionObjectCsid
419                     + " was set to " + computedCurrentLocation);
420         }
421
422         return numUpdated;
423     }
424     
425     protected boolean shouldUpdateLocation(String previousLocation, String currentLocation) {
426         boolean shouldUpdate = true;
427         if (Tools.isBlank(previousLocation) && Tools.isBlank(currentLocation)) {
428             shouldUpdate = false;
429         } else if (Tools.notBlank(previousLocation) && previousLocation.equals(currentLocation)) {
430             shouldUpdate = false;
431         }
432         return shouldUpdate;
433     }
434
435     // #################################################################
436     // Ray Lee's convenience methods from his AbstractBatchJob class for the
437     // UC Berkeley Botanical Garden v2.4 implementation.
438     // #################################################################
439     protected PoxPayloadOut findByCsid(String serviceName, String csid) throws URISyntaxException, DocumentException {
440         NuxeoBasedResource resource = (NuxeoBasedResource) getResourceMap().get(serviceName);
441         return findByCsid(resource, csid);
442     }
443
444     protected PoxPayloadOut findByCsid(NuxeoBasedResource resource, String csid) throws URISyntaxException, DocumentException {
445         PoxPayloadOut result = null;
446         
447         try {
448                         result = resource.getWithParentCtx(getServiceContext(), csid);
449                 } catch (Exception e) {
450                         String msg = String.format("UpdateObjectLocation batch job could find/get resource CSID='%s' of type '%s'",
451                                         csid, resource.getServiceName());
452                         if (logger.isDebugEnabled()) {
453                                 logger.debug(msg, e);
454                         } else {
455                                 logger.error(msg);
456                         }
457                 }
458         
459         return result;
460     }
461
462     protected UriInfo createUriInfo() throws URISyntaxException {
463         return createUriInfo("");
464     }
465
466     private UriInfo createUriInfo(String queryString) throws URISyntaxException {
467         URI absolutePath = new URI("");
468         URI baseUri = new URI("");
469         return new UriInfoImpl(absolutePath, baseUri, "", queryString, Collections.<PathSegment>emptyList());
470     }
471
472     // #################################################################
473     // Other convenience methods
474     // #################################################################
475     protected UriInfo createRelatedRecordsUriInfo(String queryString) throws URISyntaxException {
476         URI uri = new URI(null, null, null, queryString, null);
477         return createUriInfo(uri.getRawQuery());
478     }
479     
480     protected UriInfo setupQueryParamForUpdateRecords() throws URISyntaxException {
481         UriInfo result = null;
482         
483         //
484         // Check first to see if we've got a query param.  It will override any invocation context value
485         //
486         String updateCoreValues = (String) getServiceContext().getQueryParams().getFirst(IClientQueryParams.UPDATE_CORE_VALUES);
487         if (Tools.isBlank(updateCoreValues)) {
488                 //
489                 // Since there is no query param, let's check the invocation context
490                 //
491                 updateCoreValues = getInvocationContext().getUpdateCoreValues();                
492         }
493         
494         //
495         // If we found a value, then use it to create a query parameter
496         //
497         if (Tools.notBlank(updateCoreValues)) {
498                 result = createUriInfo(IClientQueryParams.UPDATE_CORE_VALUES + "=" + updateCoreValues);
499         }
500         
501         return result;
502     }
503
504     protected String getFieldElementValue(PoxPayloadOut payload, String partLabel, Namespace partNamespace, String fieldPath) {
505         String value = null;
506         SAXBuilder builder = new SAXBuilder();
507         try {
508             Document document = builder.build(new StringReader(payload.toXML()));
509             Element root = document.getRootElement();
510             // The part element is always expected to have an explicit namespace.
511             Element part = root.getChild(partLabel, partNamespace);
512             // Try getting the field element both with and without a namespace.
513             // Even though a field element that lacks a namespace prefix
514             // may yet inherit its namespace from a parent, JDOM may require that
515             // the getChild() call be made without a namespace.
516             Element field = part.getChild(fieldPath, partNamespace);
517             if (field == null) {
518                 field = part.getChild(fieldPath);
519             }
520             if (field != null) {
521                 value = field.getText();
522             }
523         } catch (Exception e) {
524             logger.error("Error getting value from field path " + fieldPath
525                     + " in schema part " + partLabel);
526             return null;
527         }
528         return value;
529     }
530
531     private boolean isRecordDeleted(NuxeoBasedResource resource, String collectionObjectCsid)
532             throws URISyntaxException, DocumentException {
533         boolean isDeleted = false;
534         
535         byte[] workflowResponse = resource.getWorkflowWithExistingContext(getServiceContext(), createUriInfo(), collectionObjectCsid);
536         if (workflowResponse != null) {
537             PoxPayloadOut payloadOut = new PoxPayloadOut(workflowResponse);
538             String workflowState =
539                     getFieldElementValue(payloadOut, WORKFLOW_COMMON_SCHEMA_NAME,
540                     WORKFLOW_COMMON_NAMESPACE, LIFECYCLE_STATE_ELEMENT_NAME);
541             if (Tools.notBlank(workflowState) && workflowState.contains(WorkflowClient.WORKFLOWSTATE_DELETED)) {
542                 isDeleted = true;
543             }
544         }
545         
546         return isDeleted;
547     }
548
549     private UriInfo addFilterToExcludeSoftDeletedRecords(UriInfo uriInfo) throws URISyntaxException {
550         if (uriInfo == null) {
551             uriInfo = createUriInfo();
552         }
553         uriInfo.getQueryParameters().add(WorkflowClient.WORKFLOW_QUERY_DELETED_QP, Boolean.FALSE.toString());
554         return uriInfo;
555     }
556     
557     private UriInfo addFilterForPageSize(UriInfo uriInfo, long startPage, long pageSize) throws URISyntaxException {
558         if (uriInfo == null) {
559             uriInfo = createUriInfo();
560         }
561         uriInfo.getQueryParameters().addFirst(IClientQueryParams.START_PAGE_PARAM, Long.toString(startPage));
562         uriInfo.getQueryParameters().addFirst(IClientQueryParams.PAGE_SIZE_PARAM, Long.toString(pageSize));
563
564         return uriInfo;
565     }
566
567     private AbstractCommonList getRecordsRelatedToCsid(NuxeoBasedResource resource, String csid,
568             String relationshipDirection, boolean excludeDeletedRecords) throws URISyntaxException {
569         UriInfo uriInfo = createUriInfo();
570         uriInfo.getQueryParameters().add(relationshipDirection, csid);
571         if (excludeDeletedRecords) {
572             uriInfo = addFilterToExcludeSoftDeletedRecords(uriInfo);
573         }
574         // The 'resource' type used here identifies the record type of the
575         // related records to be retrieved
576         AbstractCommonList relatedRecords = resource.getList(getServiceContext(), uriInfo);
577         if (logger.isTraceEnabled()) {
578             logger.trace("Identified " + relatedRecords.getTotalItems()
579                     + " record(s) related to the object record via direction " + relationshipDirection + " with CSID " + csid);
580         }
581         return relatedRecords;
582     }
583
584     /**
585      * Returns the records of a specified type that are related to a specified
586      * record, where that record is the object of the relation.
587      *
588      * @param resource a resource. The type of this resource determines the type
589      * of related records that are returned.
590      * @param csid a CSID identifying a record
591      * @param excludeDeletedRecords true if 'soft-deleted' records should be
592      * excluded from results; false if those records should be included
593      * @return a list of records of a specified type, related to a specified
594      * record
595      * @throws URISyntaxException
596      */
597     private AbstractCommonList getRecordsRelatedToObjectCsid(NuxeoBasedResource resource, String csid, boolean excludeDeletedRecords) throws URISyntaxException {
598         return getRecordsRelatedToCsid(resource, csid, IQueryManager.SEARCH_RELATED_TO_CSID_AS_OBJECT, excludeDeletedRecords);
599     }
600
601     /**
602      * Returns the records of a specified type that are related to a specified
603      * record, where that record is the subject of the relation.
604      *
605      * @param resource a resource. The type of this resource determines the type
606      * of related records that are returned.
607      * @param csid a CSID identifying a record
608      * @param excludeDeletedRecords true if 'soft-deleted' records should be
609      * excluded from results; false if those records should be included
610      * @return a list of records of a specified type, related to a specified
611      * record
612      * @throws URISyntaxException
613      */
614     private AbstractCommonList getRecordsRelatedToSubjectCsid(NuxeoBasedResource resource, String csid, boolean excludeDeletedRecords) throws URISyntaxException {
615         return getRecordsRelatedToCsid(resource, csid, IQueryManager.SEARCH_RELATED_TO_CSID_AS_SUBJECT, excludeDeletedRecords);
616     }
617
618     private AbstractCommonList getRelatedRecords(NuxeoBasedResource resource, String csid, boolean excludeDeletedRecords)
619             throws URISyntaxException, DocumentException {
620         AbstractCommonList relatedRecords = new AbstractCommonList();
621         AbstractCommonList recordsRelatedToObjectCSID = getRecordsRelatedToObjectCsid(resource, csid, excludeDeletedRecords);
622         AbstractCommonList recordsRelatedToSubjectCSID = getRecordsRelatedToSubjectCsid(resource, csid, excludeDeletedRecords);
623         // If either list contains any related records, merge in its items
624         if (recordsRelatedToObjectCSID.getListItem().size() > 0) {
625             relatedRecords.getListItem().addAll(recordsRelatedToObjectCSID.getListItem());
626         }
627         if (recordsRelatedToSubjectCSID.getListItem().size() > 0) {
628             relatedRecords.getListItem().addAll(recordsRelatedToSubjectCSID.getListItem());
629         }
630         if (logger.isTraceEnabled()) {
631             logger.trace("Identified a total of " + relatedRecords.getListItem().size()
632                     + " record(s) related to the record with CSID " + csid);
633         }
634         return relatedRecords;
635     }
636
637     private List<String> getCsidsList(AbstractCommonList list) {
638         List<String> csids = new ArrayList<String>();
639         for (AbstractCommonList.ListItem listitem : list.getListItem()) {
640             csids.add(AbstractCommonListUtils.ListItemGetCSID(listitem));
641         }
642         return csids;
643     }
644     
645     private void appendItemsToCsidsList(List<String> existingList, AbstractCommonList abstractCommonList) {
646         for (AbstractCommonList.ListItem listitem : abstractCommonList.getListItem()) {
647                 existingList.add(AbstractCommonListUtils.ListItemGetCSID(listitem));
648         }
649     }
650     
651     private List<String> getMemberCsidsFromGroup(String serviceName, String groupCsid) throws URISyntaxException, DocumentException {
652         ResourceMap resourcemap = getResourceMap();
653         NuxeoBasedResource resource = (NuxeoBasedResource) resourcemap.get(serviceName);
654         return getMemberCsidsFromGroup(resource, groupCsid);
655     }
656
657     private List<String> getMemberCsidsFromGroup(NuxeoBasedResource resource, String groupCsid) throws URISyntaxException, DocumentException {
658         // The 'resource' type used here identifies the record type of the
659         // related records to be retrieved
660         AbstractCommonList relatedRecords =
661                 getRelatedRecords(resource, groupCsid, EXCLUDE_DELETED);
662         List<String> memberCsids = getCsidsList(relatedRecords);
663         return memberCsids;
664     }
665
666     private List<String> getNoContextCsids() throws URISyntaxException {
667         ResourceMap resourcemap = getResourceMap();
668         NuxeoBasedResource collectionObjectResource = (NuxeoBasedResource) resourcemap.get(CollectionObjectClient.SERVICE_NAME);
669         UriInfo uriInfo = createUriInfo();
670         uriInfo = addFilterToExcludeSoftDeletedRecords(uriInfo);
671
672         boolean morePages = true;
673         long currentPage = 0;
674         long pageSize = DEFAULT_PAGE_SIZE;
675         List<String> noContextCsids = new ArrayList<String>();
676         
677         while (morePages == true) {
678                 uriInfo = addFilterForPageSize(uriInfo, currentPage, pageSize);
679                 AbstractCommonList collectionObjects = collectionObjectResource.getList(getServiceContext(), uriInfo);
680                 appendItemsToCsidsList(noContextCsids, collectionObjects);
681                 
682                 if (collectionObjects.getItemsInPage() == pageSize) { // We know we're at the last page when the number of items returned in the last request is less than the page size.
683                         currentPage++;
684                 } else {
685                         morePages = false;                      
686                 }
687         }
688         
689         return noContextCsids;
690     }
691 }