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