1 package org.collectionspace.services.batch.nuxeo;
3 import java.io.StringReader;
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;
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;
34 public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
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());
62 // Initialization tasks
63 public UpdateObjectLocationBatchJob() {
64 setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE, INVOCATION_MODE_LIST, INVOCATION_MODE_NO_CONTEXT));
68 * The main work logic of the batch job. Will be called after setContext.
73 setCompletionStatus(STATUS_MIN_PROGRESS);
77 List<String> csids = new ArrayList<String>();
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);
86 csids.add(singleCsid);
88 } else if (requestIsForInvocationModeList()) {
89 List<String> listCsids = getListCsids();
90 if (listCsids.isEmpty()) {
91 throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
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);
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);
108 csids.addAll(noContextCsids);
111 // Update the value of the computed current location field for each CollectionObject
112 setResults(updateComputedCurrentLocations(csids));
113 setCompletionStatus(STATUS_COMPLETE);
115 } catch (Exception e) {
116 String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage();
117 setErrorResult(errMsg);
122 private InvocationResults updateComputedCurrentLocations(List<String> csids) {
124 ResourceMap resourcemap = getResourceMap();
125 ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
126 ResourceBase movementResource = resourcemap.get(MovementClient.SERVICE_NAME);
127 String computedCurrentLocation;
132 // For each CollectionObject record
133 for (String collectionObjectCsid : csids) {
135 // Skip over soft-deleted CollectionObject records
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);
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()) {
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
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)) {
165 // Update the value of the computed current location field
166 // in the CollectionObject record
167 numUpdated = updateComputedCurrentLocationValue(collectionObjectResource,
168 collectionObjectCsid, computedCurrentLocation, resourcemap, numUpdated);
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);
180 logger.info("Updated computedCurrentLocation values in " + numUpdated + " CollectionObject record(s).");
181 getResults().setNumAffected(numUpdated);
185 private String computeCurrentLocation(AbstractCommonList relatedMovements) {
186 String computedCurrentLocation;
188 Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
189 computedCurrentLocation = "";
190 String currentLocation;
192 String mostRecentLocationDate = "";
193 for (AbstractCommonList.ListItem movementRecord : relatedMovements.getListItem()) {
194 movementCsid = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CSID_ELEMENT_NAME);
195 if (Tools.isBlank(movementCsid)) {
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)) {
204 alreadyProcessedMovementCsids.add(movementCsid);
206 locationDate = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, LOCATION_DATE_ELEMENT_NAME);
207 if (Tools.isBlank(locationDate)) {
210 currentLocation = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CURRENT_LOCATION_ELEMENT_NAME);
211 if (Tools.isBlank(currentLocation)) {
214 if (logger.isTraceEnabled()) {
215 logger.trace("Location date value = " + locationDate);
216 logger.trace("Current location value = " + currentLocation);
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.
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.
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;
237 return computedCurrentLocation;
240 private int updateComputedCurrentLocationValue(ResourceBase collectionObjectResource,
241 String collectionObjectCsid, String computedCurrentLocation, ResourceMap resourcemap, int numUpdated)
242 throws DocumentException, URISyntaxException {
243 PoxPayloadOut collectionObjectPayload;
245 String previousComputedCurrentLocation;
247 collectionObjectPayload = findByCsid(collectionObjectResource, collectionObjectCsid);
248 if (Tools.isBlank(collectionObjectPayload.toXML())) {
251 if (logger.isTraceEnabled()) {
252 logger.trace("Payload: " + "\n" + collectionObjectPayload);
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)) {
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.
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);
275 if (Tools.isBlank(objectNumber)) {
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>"
288 if (logger.isTraceEnabled()) {
289 logger.trace("Update payload: " + "\n" + collectionObjectUpdatePayload);
291 byte[] response = collectionObjectResource.update(resourcemap, null, collectionObjectCsid,
292 collectionObjectUpdatePayload);
294 if (logger.isTraceEnabled()) {
295 logger.trace("Computed current location value for CollectionObject " + collectionObjectCsid
296 + " was set to " + computedCurrentLocation);
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);
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);
317 protected UriInfo createUriInfo() throws URISyntaxException {
318 return createUriInfo("");
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());
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());
335 protected String getFieldElementValue(PoxPayloadOut payload, String partLabel, Namespace partNamespace, String fieldPath) {
337 SAXBuilder builder = new SAXBuilder();
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);
349 field = part.getChild(fieldPath);
352 value = field.getText();
354 } catch (Exception e) {
355 logger.error("Error getting value from field path " + fieldPath
356 + " in schema part " + partLabel);
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)) {
378 private AbstractCommonList getRelatedRecords(ResourceBase resource, String csid, boolean excludeDeletedRecords)
379 throws URISyntaxException, DocumentException {
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");
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);
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");
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);
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());
416 if (logger.isTraceEnabled()) {
417 logger.trace("Identified a total of " + relatedRecords.getListItem().size()
418 + " record(s) related to the record with CSID " + csid);
421 return relatedRecords;
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();
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));
440 if (logger.isInfoEnabled()) {
441 logger.info("Identified " + noContextCsids.size()
442 + " total active CollectionObjects to process in the 'no context' invocation mode.");
444 return noContextCsids;