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