]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
f29f498384a97be6d0e3d53c1658772a1310cfe3
[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 import javax.ws.rs.core.PathSegment;
13 import javax.ws.rs.core.UriInfo;
14 import org.collectionspace.services.batch.AbstractBatchInvocable;
15 import org.collectionspace.services.client.AbstractCommonListUtils;
16 import org.collectionspace.services.client.CollectionObjectClient;
17 import org.collectionspace.services.client.MovementClient;
18 import org.collectionspace.services.client.PoxPayloadOut;
19 import org.collectionspace.services.client.workflow.WorkflowClient;
20 import org.collectionspace.services.common.ResourceBase;
21 import org.collectionspace.services.common.ResourceMap;
22 import org.collectionspace.services.common.api.Tools;
23 import org.collectionspace.services.common.invocable.InvocationResults;
24 import org.collectionspace.services.jaxb.AbstractCommonList;
25 import org.dom4j.DocumentException;
26 import org.jboss.resteasy.specimpl.UriInfoImpl;
27 import org.jdom.Document;
28 import org.jdom.Element;
29 import org.jdom.Namespace;
30 import org.jdom.input.SAXBuilder;
31 import org.slf4j.Logger;
32 import org.slf4j.LoggerFactory;
33
34 public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
35
36     // FIXME: Where appropriate, get from existing constants rather than local declarations
37     private final static String COMPUTED_CURRENT_LOCATION_ELEMENT_NAME = "computedCurrentLocation";
38     private final static String CSID_ELEMENT_NAME = "csid";
39     private final static String CURRENT_LOCATION_ELEMENT_NAME = "currentLocation";
40     private final static String LIFECYCLE_STATE_ELEMENT_NAME = "currentLifeCycleState";
41     private final static String LOCATION_DATE_ELEMENT_NAME = "locationDate";
42     private final static String OBJECT_NUMBER_ELEMENT_NAME = "objectNumber";
43     private final static String WORKFLOW_COMMON_SCHEMA_NAME = "workflow_common";
44     private final static String WORKFLOW_COMMON_NAMESPACE_PREFIX = "ns2";
45     private final static String WORKFLOW_COMMON_NAMESPACE_URI =
46             "http://collectionspace.org/services/workflow";
47     private final static Namespace WORKFLOW_COMMON_NAMESPACE =
48             Namespace.getNamespace(
49             WORKFLOW_COMMON_NAMESPACE_PREFIX,
50             WORKFLOW_COMMON_NAMESPACE_URI);
51     private final static String COLLECTIONOBJECTS_COMMON_SCHEMA_NAME = "collectionobjects_common";
52     private final static String COLLECTIONOBJECTS_COMMON_NAMESPACE_PREFIX = "ns2";
53     private final static String COLLECTIONOBJECTS_COMMON_NAMESPACE_URI =
54             "http://collectionspace.org/services/collectionobject";
55     private final static Namespace COLLECTIONOBJECTS_COMMON_NAMESPACE =
56             Namespace.getNamespace(
57             COLLECTIONOBJECTS_COMMON_NAMESPACE_PREFIX,
58             COLLECTIONOBJECTS_COMMON_NAMESPACE_URI);
59     private final String CLASSNAME = this.getClass().getSimpleName();
60     private final Logger logger = LoggerFactory.getLogger(this.getClass());
61
62     // Initialization tasks
63     public UpdateObjectLocationBatchJob() {
64         setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE, INVOCATION_MODE_LIST, INVOCATION_MODE_NO_CONTEXT));
65     }
66
67     /**
68      * The main work logic of the batch job. Will be called after setContext.
69      */
70     @Override
71     public void run() {
72
73         setCompletionStatus(STATUS_MIN_PROGRESS);
74
75         try {
76
77             List<String> csids = new ArrayList<String>();
78
79             // Build a list of CollectionObject records to process via this
80             // batch job, depending on the invocation mode requested.
81             if (requestIsForInvocationModeSingle()) {
82                 String singleCsid = getInvocationContext().getSingleCSID();
83                 if (Tools.isBlank(singleCsid)) {
84                     throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
85                 } else {
86                     csids.add(singleCsid);
87                 }
88             } else if (requestIsForInvocationModeList()) {
89                 List<String> listCsids = getListCsids();
90                 if (listCsids.isEmpty()) {
91                     throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
92                 }
93                 csids.addAll(listCsids);
94             } else if (requestIsForInvocationModeGroup()) {
95                 // This invocation mode is currently not yet supported.
96                 // FIXME: Add code to getMemberCsidsFromGroup() to support this mode.
97                 String groupCsid = getInvocationContext().getGroupCSID();
98                 List<String> groupMemberCsids = getMemberCsidsFromGroup(groupCsid);
99                 if (groupMemberCsids.isEmpty()) {
100                     throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
101                 }
102                 csids.addAll(groupMemberCsids);
103             } else if (requestIsForInvocationModeNoContext()) {
104                 List<String> noContextCsids = getNoContextCsids();
105                 if (noContextCsids.isEmpty()) {
106                     throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
107                 }
108                 csids.addAll(noContextCsids);
109             }
110
111             // Update the value of the computed current location field for each CollectionObject
112             setResults(updateComputedCurrentLocations(csids));
113             setCompletionStatus(STATUS_COMPLETE);
114
115         } catch (Exception e) {
116             String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage();
117             setErrorResult(errMsg);
118         }
119
120     }
121
122     private InvocationResults updateComputedCurrentLocations(List<String> csids) {
123
124         ResourceMap resourcemap = getResourceMap();
125         ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
126         ResourceBase movementResource = resourcemap.get(MovementClient.SERVICE_NAME);
127         String computedCurrentLocation;
128         int numUpdated = 0;
129
130         try {
131
132             // For each CollectionObject record
133             for (String collectionObjectCsid : csids) {
134
135                 // Skip over soft-deleted CollectionObject records
136                 //
137                 // (No context invocations already have filtered out those records)
138                 if (!requestIsForInvocationModeNoContext()) {
139                     if (isRecordDeleted(collectionObjectResource, collectionObjectCsid)) {
140                         if (logger.isTraceEnabled()) {
141                             logger.trace("Skipping soft-deleted CollectionObject record with CSID " + collectionObjectCsid);
142                         }
143                         continue;
144                     }
145                 }
146                 // Get the Movement records related to this CollectionObject record
147                 AbstractCommonList relatedMovements =
148                         getRelatedRecords(movementResource, collectionObjectCsid, true /* exclude deleted records */);
149                 // Skip over CollectionObject records that have no related Movement records
150                 if (relatedMovements.getListItem().isEmpty()) {
151                     continue;
152                 }
153                 // Compute the current location of this CollectionObject,
154                 // based on data in its related Movement records
155                 computedCurrentLocation = computeCurrentLocation(relatedMovements);
156                 // Skip over CollectionObject records where no current location
157                 // value can be computed from related Movement records
158                 //
159                 // FIXME: Clarify: it ever necessary to 'unset' a computed
160                 // current location value, by setting it to a null or empty value,
161                 // if that value is no longer obtainable from related Movement records?
162                 if (Tools.isBlank(computedCurrentLocation)) {
163                     continue;
164                 }
165                 // Update the value of the computed current location field
166                 // in the CollectionObject record
167                 numUpdated = updateComputedCurrentLocationValue(collectionObjectResource,
168                         collectionObjectCsid, computedCurrentLocation, resourcemap, numUpdated);
169             }
170
171         } catch (Exception e) {
172             String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage() + " ";
173             errMsg = errMsg + "Successfully updated " + numUpdated + " CollectionObject record(s) prior to error.";
174             logger.error(errMsg);
175             setErrorResult(errMsg);
176             getResults().setNumAffected(numUpdated);
177             return getResults();
178         }
179
180         logger.info("Updated computedCurrentLocation values in " + numUpdated + " CollectionObject record(s).");
181         getResults().setNumAffected(numUpdated);
182         return getResults();
183     }
184
185     private String computeCurrentLocation(AbstractCommonList relatedMovements) {
186         String computedCurrentLocation;
187         String movementCsid;
188         Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
189         computedCurrentLocation = "";
190         String currentLocation;
191         String locationDate;
192         String mostRecentLocationDate = "";
193         for (AbstractCommonList.ListItem movementRecord : relatedMovements.getListItem()) {
194             movementCsid = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CSID_ELEMENT_NAME);
195             if (Tools.isBlank(movementCsid)) {
196                 continue;
197             }
198             // Avoid processing any related Movement record more than once,
199             // regardless of the directionality of its relation(s) to this
200             // CollectionObject record.
201             if (alreadyProcessedMovementCsids.contains(movementCsid)) {
202                 continue;
203             } else {
204                 alreadyProcessedMovementCsids.add(movementCsid);
205             }
206             locationDate = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, LOCATION_DATE_ELEMENT_NAME);
207             if (Tools.isBlank(locationDate)) {
208                 continue;
209             }
210             currentLocation = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CURRENT_LOCATION_ELEMENT_NAME);
211             if (Tools.isBlank(currentLocation)) {
212                 continue;
213             }
214             if (logger.isTraceEnabled()) {
215                 logger.trace("Location date value = " + locationDate);
216                 logger.trace("Current location value = " + currentLocation);
217             }
218             // If this record's location date value is more recent than that of other
219             // Movement records processed so far, set the computed current location
220             // to its current location value.
221             //
222             // Assumes that all values for this element/field will be consistent ISO 8601
223             // date/time representations, each of which can be ordered via string comparison.
224             //
225             // If this is *not* the case, we can instead parse and convert these values
226             // to date/time objects.
227             if (locationDate.compareTo(mostRecentLocationDate) > 0) {
228                 mostRecentLocationDate = locationDate;
229                 // FIXME: Add optional validation here that the currentLocation value
230                 // parses successfully as an item refName.
231                 // Consider making this optional validation, in turn dependent on the
232                 // value of a parameter passed in during batch job invocation.
233                 computedCurrentLocation = currentLocation;
234             }
235
236         }
237         return computedCurrentLocation;
238     }
239
240     private int updateComputedCurrentLocationValue(ResourceBase collectionObjectResource,
241             String collectionObjectCsid, String computedCurrentLocation, ResourceMap resourcemap, int numUpdated)
242             throws DocumentException, URISyntaxException {
243         PoxPayloadOut collectionObjectPayload;
244         String objectNumber;
245         String previousComputedCurrentLocation;
246
247         collectionObjectPayload = findByCsid(collectionObjectResource, collectionObjectCsid);
248         if (Tools.isBlank(collectionObjectPayload.toXML())) {
249             return numUpdated;
250         } else {
251             if (logger.isTraceEnabled()) {
252                 logger.trace("Payload: " + "\n" + collectionObjectPayload);
253             }
254         }
255         // Perform the update only if the computed current location value will change
256         previousComputedCurrentLocation = getFieldElementValue(collectionObjectPayload,
257                 COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
258                 COMPUTED_CURRENT_LOCATION_ELEMENT_NAME);
259         if (Tools.notBlank(previousComputedCurrentLocation)
260                 && computedCurrentLocation.equals(previousComputedCurrentLocation)) {
261             return numUpdated;
262         }
263         // In the default CollectionObject validation handler, the object number
264         // is a required field and its (non-blank) value must be present in update
265         // payloads to successfully perform an update.
266         //
267         // FIXME: Consider making this check for an object number dependent on the
268         // value of a parameter passed in during batch job invocation.
269         objectNumber = getFieldElementValue(collectionObjectPayload,
270                 COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
271                 OBJECT_NUMBER_ELEMENT_NAME);
272         if (logger.isTraceEnabled()) {
273             logger.trace("Object number: " + objectNumber);
274         }
275         if (Tools.isBlank(objectNumber)) {
276             return numUpdated;
277         }
278
279         String collectionObjectUpdatePayload =
280                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
281                 + "<document name=\"collectionobject\">"
282                 + "  <ns2:collectionobjects_common "
283                 + "      xmlns:ns2=\"http://collectionspace.org/services/collectionobject\">"
284                 + "    <objectNumber>" + objectNumber + "</objectNumber>"
285                 + "    <computedCurrentLocation>" + computedCurrentLocation + "</computedCurrentLocation>"
286                 + "  </ns2:collectionobjects_common>"
287                 + "</document>";
288         if (logger.isTraceEnabled()) {
289             logger.trace("Update payload: " + "\n" + collectionObjectUpdatePayload);
290         }
291         byte[] response = collectionObjectResource.update(resourcemap, null, collectionObjectCsid,
292                 collectionObjectUpdatePayload);
293         numUpdated++;
294         if (logger.isTraceEnabled()) {
295             logger.trace("Computed current location value for CollectionObject " + collectionObjectCsid
296                     + " was set to " + computedCurrentLocation);
297
298         }
299         return numUpdated;
300     }
301
302     // #################################################################
303     // Ray Lee's convenience methods from his AbstractBatchJob class for the
304     // UC Berkeley Botanical Garden v2.4 implementation.
305     // #################################################################
306     protected PoxPayloadOut findByCsid(String serviceName, String csid) throws URISyntaxException, DocumentException {
307         ResourceBase resource = getResourceMap().get(serviceName);
308         return findByCsid(resource, csid);
309     }
310
311     protected PoxPayloadOut findByCsid(ResourceBase resource, String csid) throws URISyntaxException, DocumentException {
312         byte[] response = resource.get(null, createUriInfo(), csid);
313         PoxPayloadOut payload = new PoxPayloadOut(response);
314         return payload;
315     }
316
317     protected UriInfo createUriInfo() throws URISyntaxException {
318         return createUriInfo("");
319     }
320
321     protected UriInfo createUriInfo(String queryString) throws URISyntaxException {
322         URI absolutePath = new URI("");
323         URI baseUri = new URI("");
324         return new UriInfoImpl(absolutePath, baseUri, "", queryString, Collections.<PathSegment>emptyList());
325     }
326
327     // #################################################################
328     // Other convenience methods
329     // #################################################################
330     protected UriInfo createRelatedRecordsUriInfo(String queryString) throws URISyntaxException {
331         URI uri = new URI(null, null, null, queryString, null);
332         return createUriInfo(uri.getRawQuery());
333     }
334
335     protected String getFieldElementValue(PoxPayloadOut payload, String partLabel, Namespace partNamespace, String fieldPath) {
336         String value = null;
337         SAXBuilder builder = new SAXBuilder();
338         try {
339             Document document = builder.build(new StringReader(payload.toXML()));
340             Element root = document.getRootElement();
341             // The part element is always expected to have an explicit namespace.
342             Element part = root.getChild(partLabel, partNamespace);
343             // Try getting the field element both with and without a namespace.
344             // Even though a field element that lacks a namespace prefix
345             // may yet inherit its namespace from a parent, JDOM may require that
346             // the getChild() call be made without a namespace.
347             Element field = part.getChild(fieldPath, partNamespace);
348             if (field == null) {
349                 field = part.getChild(fieldPath);
350             }
351             if (field != null) {
352                 value = field.getText();
353             }
354         } catch (Exception e) {
355             logger.error("Error getting value from field path " + fieldPath
356                     + " in schema part " + partLabel);
357             return null;
358         }
359         return value;
360     }
361
362     private boolean isRecordDeleted(ResourceBase resource, String collectionObjectCsid)
363             throws URISyntaxException, DocumentException {
364         boolean isDeleted = false;
365         byte[] workflowResponse = resource.getWorkflow(createUriInfo(), collectionObjectCsid);
366         if (workflowResponse != null) {
367             PoxPayloadOut payloadOut = new PoxPayloadOut(workflowResponse);
368             String workflowState =
369                     getFieldElementValue(payloadOut, WORKFLOW_COMMON_SCHEMA_NAME,
370                     WORKFLOW_COMMON_NAMESPACE, LIFECYCLE_STATE_ELEMENT_NAME);
371             if (Tools.notBlank(workflowState) && workflowState.equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
372                 isDeleted = true;
373             }
374         }
375         return isDeleted;
376     }
377
378     private AbstractCommonList getRelatedRecords(ResourceBase resource, String csid, boolean excludeDeletedRecords)
379             throws URISyntaxException, DocumentException {
380
381         // Get records related to a record, specified by its CSID,
382         // where the record is the object of the relation
383         UriInfo uriInfo = createUriInfo();
384         // FIXME: Get this from constant(s), where appropriate
385         uriInfo.getQueryParameters().add("rtObj", csid);
386         if (excludeDeletedRecords) {
387             uriInfo.getQueryParameters().add(WorkflowClient.WORKFLOW_QUERY_NONDELETED, "false");
388         }
389
390         AbstractCommonList relatedRecords = resource.getList(uriInfo);
391         if (logger.isTraceEnabled()) {
392             logger.trace("Identified " + relatedRecords.getTotalItems()
393                     + " record(s) related to the object record with CSID " + csid);
394         }
395
396         // Get records related to a record, specified by its CSID,
397         // where the record is the subject of the relation
398         // FIXME: Get query string(s) from constant(s), where appropriate
399         uriInfo = createUriInfo();
400         uriInfo.getQueryParameters().add("rtSbj", csid);
401         if (excludeDeletedRecords) {
402             uriInfo.getQueryParameters().add(WorkflowClient.WORKFLOW_QUERY_NONDELETED, "false");
403         }
404         AbstractCommonList reverseRelatedRecords = resource.getList(uriInfo);
405         if (logger.isTraceEnabled()) {
406             logger.trace("Identified " + reverseRelatedRecords.getTotalItems()
407                     + " record(s) related to the subject record with CSID " + csid);
408         }
409
410         // If the second list contains any related records,
411         // merge it into the first list
412         if (reverseRelatedRecords.getListItem().size() > 0) {
413             relatedRecords.getListItem().addAll(reverseRelatedRecords.getListItem());
414         }
415
416         if (logger.isTraceEnabled()) {
417             logger.trace("Identified a total of " + relatedRecords.getListItem().size()
418                     + " record(s) related to the record with CSID " + csid);
419         }
420
421         return relatedRecords;
422     }
423
424     // Stub method, as this invocation mode is not currently supported
425     private List<String> getMemberCsidsFromGroup(String groupCsid) throws URISyntaxException {
426         List<String> memberCsids = Collections.emptyList();
427         return memberCsids;
428     }
429
430     private List<String> getNoContextCsids() throws URISyntaxException {
431         List<String> noContextCsids = new ArrayList<String>();
432         ResourceMap resourcemap = getResourceMap();
433         ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
434         UriInfo uriInfo = createUriInfo();
435         uriInfo.getQueryParameters().add(WorkflowClient.WORKFLOW_QUERY_NONDELETED, "false");
436         AbstractCommonList collectionObjects = collectionObjectResource.getList(uriInfo);
437         for (AbstractCommonList.ListItem collectionObjectRecord : collectionObjects.getListItem()) {
438             noContextCsids.add(AbstractCommonListUtils.ListItemGetCSID(collectionObjectRecord));
439         }
440         if (logger.isInfoEnabled()) {
441             logger.info("Identified " + noContextCsids.size()
442                     + " total active CollectionObjects to process in the 'no context' invocation mode.");
443         }
444         return noContextCsids;
445     }
446 }