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.Response;
14 import javax.ws.rs.core.UriInfo;
15 import org.collectionspace.services.batch.AbstractBatchInvocable;
16 import org.collectionspace.services.client.AbstractCommonListUtils;
17 import org.collectionspace.services.client.CollectionObjectClient;
18 import org.collectionspace.services.client.MovementClient;
19 import org.collectionspace.services.client.PoxPayloadOut;
20 import org.collectionspace.services.client.workflow.WorkflowClient;
21 import org.collectionspace.services.common.ResourceBase;
22 import org.collectionspace.services.common.ResourceMap;
23 import org.collectionspace.services.common.api.Tools;
24 import org.collectionspace.services.common.invocable.InvocationResults;
25 import org.collectionspace.services.jaxb.AbstractCommonList;
26 import org.dom4j.DocumentException;
27 import org.jboss.resteasy.specimpl.UriInfoImpl;
28 import org.jdom.Document;
29 import org.jdom.Element;
30 import org.jdom.Namespace;
31 import org.jdom.input.SAXBuilder;
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
35 public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
37 // FIXME: Where appropriate, get from existing constants rather than local declarations
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 static String NONDELETED_QUERY_COMPONENT =
60 "&" + WorkflowClient.WORKFLOW_QUERY_NONDELETED + "=false";
61 private PoxPayloadOut payloadOut;
62 private final String CLASSNAME = this.getClass().getSimpleName();
63 private final Logger logger = LoggerFactory.getLogger(this.getClass());
65 // Initialization tasks
66 public UpdateObjectLocationBatchJob() {
67 setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE, INVOCATION_MODE_LIST));
71 * The main work logic of the batch job. Will be called after setContext.
76 setCompletionStatus(STATUS_MIN_PROGRESS);
80 List<String> csids = new ArrayList<String>();
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 // Currently not supported
96 // FIXME: Get individual CSIDs, if any, from the group
97 // String groupCsid = getInvocationContext().getGroupCSID();
99 } else if (requestIsForInvocationModeNoContext()) {
100 // Currently not supported
101 // FIXME: Add code to invoke batch job on every active CollectionObject
104 // Update the computed current location field for each CollectionObject
105 setResults(updateComputedCurrentLocations(csids));
106 setCompletionStatus(STATUS_COMPLETE);
108 } catch (Exception e) {
109 String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage();
110 setErrorResult(errMsg);
115 // #################################################################
116 // Ray's convenience methods from his AbstractBatchJob class for the
117 // UC Berkeley Botanical Garden v2.4 implementation.
118 // #################################################################
119 protected PoxPayloadOut findByCsid(String serviceName, String csid) throws URISyntaxException, DocumentException {
120 ResourceBase resource = getResourceMap().get(serviceName);
121 return findByCsid(resource, csid);
124 protected PoxPayloadOut findByCsid(ResourceBase resource, String csid) throws URISyntaxException, DocumentException {
125 byte[] response = resource.get(null, createUriInfo(), csid);
126 PoxPayloadOut payload = new PoxPayloadOut(response);
130 protected UriInfo createUriInfo() throws URISyntaxException {
131 return createUriInfo("");
134 protected UriInfo createUriInfo(String queryString) throws URISyntaxException {
135 URI absolutePath = new URI("");
136 URI baseUri = new URI("");
137 return new UriInfoImpl(absolutePath, baseUri, "", queryString, Collections.<PathSegment>emptyList());
140 // #################################################################
141 // Other convenience methods
142 // #################################################################
143 protected UriInfo createRelatedRecordsUriInfo(String query) throws URISyntaxException {
144 URI uri = new URI(null, null, null, query, null);
145 return createUriInfo(uri.getRawQuery());
148 protected String getFieldElementValue(PoxPayloadOut payload, String partLabel, Namespace partNamespace, String fieldPath) {
150 SAXBuilder builder = new SAXBuilder();
152 Document document = builder.build(new StringReader(payload.toXML()));
153 Element root = document.getRootElement();
154 // The part element is always expected to have an explicit namespace.
155 Element part = root.getChild(partLabel, partNamespace);
156 // Try getting the field element both with and without a namespace.
157 // Even though a field element that lacks a namespace prefix
158 // may yet inherit its namespace from a parent, JDOM may require that
159 // the getChild() call be made without a namespace.
160 Element field = part.getChild(fieldPath, partNamespace);
162 field = part.getChild(fieldPath);
165 value = field.getText();
167 } catch (Exception e) {
168 logger.error("Error getting value from field path " + fieldPath
169 + " in schema part " + partLabel);
175 private boolean isRecordDeleted(ResourceBase resource, String collectionObjectCsid) throws URISyntaxException, DocumentException {
176 boolean isDeleted = false;
177 byte[] workflowResponse = resource.getWorkflow(createUriInfo(), collectionObjectCsid);
178 if (workflowResponse != null) {
179 payloadOut = new PoxPayloadOut(workflowResponse);
180 String workflowState =
181 getFieldElementValue(payloadOut, WORKFLOW_COMMON_SCHEMA_NAME,
182 WORKFLOW_COMMON_NAMESPACE, LIFECYCLE_STATE_ELEMENT_NAME);
183 if (Tools.notBlank(workflowState) && workflowState.contentEquals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
190 private InvocationResults updateComputedCurrentLocations(List<String> csids) {
192 ResourceMap resourcemap = getResourceMap();
193 ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
194 ResourceBase movementResource = resourcemap.get(MovementClient.SERVICE_NAME);
195 PoxPayloadOut collectionObjectPayload;
196 String computedCurrentLocation;
199 PoxPayloadOut payloadOut;
205 // For each CollectionObject record
206 for (String collectionObjectCsid : csids) {
208 // Skip over soft-deleted CollectionObject records
209 if (isRecordDeleted(collectionObjectResource, collectionObjectCsid)) {
210 if (logger.isTraceEnabled()) {
211 logger.trace("Skipping soft-deleted CollectionObject record with CSID " + collectionObjectCsid);
216 // Get the movement records related to this record
218 // Get movement records related to this record where the CollectionObject
219 // record is the subject of the relation
220 // FIXME: Get query string(s) from constant(s), where appropriate
221 queryString = "rtObj=" + collectionObjectCsid + NONDELETED_QUERY_COMPONENT;
222 UriInfo uriInfo = createRelatedRecordsUriInfo(queryString);
224 AbstractCommonList relatedMovements = movementResource.getList(uriInfo);
225 if (logger.isTraceEnabled()) {
226 logger.trace("Identified " + relatedMovements.getTotalItems()
227 + " Movement record(s) related to the object CollectionObject record " + collectionObjectCsid);
230 // Get movement records related to this record where the CollectionObject
231 // record is the object of the relation
232 // FIXME: Get query string(s) from constant(s), where appropriate
233 queryString = "rtSbj=" + collectionObjectCsid + NONDELETED_QUERY_COMPONENT;
234 uriInfo = createRelatedRecordsUriInfo(queryString);
236 AbstractCommonList reverseRelatedMovements = movementResource.getList(uriInfo);
237 if (logger.isTraceEnabled()) {
238 logger.trace("Identified " + reverseRelatedMovements.getTotalItems()
239 + " Movement record(s) related to the subject CollectionObject record " + collectionObjectCsid);
242 if ((relatedMovements.getTotalItems() == 0) && reverseRelatedMovements.getTotalItems() == 0) {
246 // Merge the two lists of related movement records
247 relatedMovements.getListItem().addAll(reverseRelatedMovements.getListItem());
249 if (logger.isTraceEnabled()) {
250 logger.trace("Identified a total of " + relatedMovements.getListItem().size()
251 + " Movement record(s) related to the subject CollectionObject record " + collectionObjectCsid);
254 // Get the latest movement record from among those, and extract
255 // its current location value
256 Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
257 computedCurrentLocation = "";
258 String currentLocation;
260 String mostRecentLocationDate = "";
261 for (AbstractCommonList.ListItem movementRecord : relatedMovements.getListItem()) {
262 movementCsid = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CSID_ELEMENT_NAME);
263 if (Tools.isBlank(movementCsid)) {
266 // Avoid processing any related Movement record more than once,
267 // regardless of the directionality of its relation(s) to this
268 // CollectionObject record.
269 if (alreadyProcessedMovementCsids.contains(movementCsid)) {
272 alreadyProcessedMovementCsids.add(movementCsid);
274 locationDate = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, LOCATION_DATE_ELEMENT_NAME);
275 if (Tools.isBlank(locationDate)) {
278 currentLocation = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CURRENT_LOCATION_ELEMENT_NAME);
279 if (Tools.isBlank(currentLocation)) {
282 if (logger.isTraceEnabled()) {
283 logger.trace("Location date value = " + locationDate);
284 logger.trace("Current location value = " + currentLocation);
286 // Assumes that all values for this element/field will be consistent ISO 8601
287 // date/time representations, each of which can be ordered via string comparison.
289 // If this is *not* the case, we can instead parse and convert these values
290 // to date/time objects.
291 if (locationDate.compareTo(mostRecentLocationDate) > 0) {
292 mostRecentLocationDate = locationDate;
293 // FIXME: Add optional validation here that the currentLocation value
294 // parses successfully as an item refName
295 computedCurrentLocation = currentLocation;
300 // Update the computed current location value in the CollectionObject record
301 collectionObjectPayload = findByCsid(collectionObjectResource, collectionObjectCsid);
302 if (Tools.notBlank(collectionObjectPayload.toXML())) {
303 if (logger.isTraceEnabled()) {
304 logger.trace("Payload: " + "\n" + collectionObjectPayload);
306 objectNumber = getFieldElementValue(collectionObjectPayload,
307 COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
308 OBJECT_NUMBER_ELEMENT_NAME);
309 if (logger.isTraceEnabled()) {
310 logger.trace("Object number: " + objectNumber);
312 if (Tools.notBlank(objectNumber)) {
313 String collectionObjectUpdatePayload =
314 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
315 + "<document name=\"collectionobject\">"
316 + " <ns2:collectionobjects_common "
317 + " xmlns:ns2=\"http://collectionspace.org/services/collectionobject\">"
318 + " <objectNumber>" + objectNumber + "</objectNumber>"
319 + " <computedCurrentLocation>" + computedCurrentLocation + "</computedCurrentLocation>"
320 + " </ns2:collectionobjects_common>"
322 if (logger.isTraceEnabled()) {
323 logger.trace("Update payload: " + "\n" + collectionObjectUpdatePayload);
325 byte[] response = collectionObjectResource.update(resourcemap, null, collectionObjectCsid,
326 collectionObjectUpdatePayload);
328 if (logger.isTraceEnabled()) {
329 logger.trace("Computed current location value for CollectionObject " + collectionObjectCsid
330 + " was set to " + computedCurrentLocation);
336 } catch (Exception e) {
337 String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage() + " ";
338 errMsg = errMsg + "Successfully updated " + numAffected + " CollectionObject record(s) prior to error.";
339 logger.error(errMsg);
340 setErrorResult(errMsg);
341 getResults().setNumAffected(numAffected);
345 logger.info("Updated computedCurrentLocation values in " + numAffected + " CollectionObject record(s).");
346 getResults().setNumAffected(numAffected);