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 CSID_ELEMENT_NAME = "csid";
38 private final static String CURRENT_LOCATION_ELEMENT_NAME = "currentLocation";
39 private final static String LIFECYCLE_STATE_ELEMENT_NAME = "currentLifeCycleState";
40 private final static String LOCATION_DATE_ELEMENT_NAME = "locationDate";
41 private final static String OBJECT_NUMBER_ELEMENT_NAME = "objectNumber";
42 private final static String WORKFLOW_COMMON_SCHEMA_NAME = "workflow_common";
43 private final static String WORKFLOW_COMMON_NAMESPACE_PREFIX = "ns2";
44 private final static String WORKFLOW_COMMON_NAMESPACE_URI =
45 "http://collectionspace.org/services/workflow";
46 private final static Namespace WORKFLOW_COMMON_NAMESPACE =
47 Namespace.getNamespace(
48 WORKFLOW_COMMON_NAMESPACE_PREFIX,
49 WORKFLOW_COMMON_NAMESPACE_URI);
50 private final static String COLLECTIONOBJECTS_COMMON_SCHEMA_NAME = "collectionobjects_common";
51 private final static String COLLECTIONOBJECTS_COMMON_NAMESPACE_PREFIX = "ns2";
52 private final static String COLLECTIONOBJECTS_COMMON_NAMESPACE_URI =
53 "http://collectionspace.org/services/collectionobject";
54 private final static Namespace COLLECTIONOBJECTS_COMMON_NAMESPACE =
55 Namespace.getNamespace(
56 COLLECTIONOBJECTS_COMMON_NAMESPACE_PREFIX,
57 COLLECTIONOBJECTS_COMMON_NAMESPACE_URI);
58 private final static String NONDELETED_QUERY_COMPONENT =
59 "&" + WorkflowClient.WORKFLOW_QUERY_NONDELETED + "=false";
60 private final String CLASSNAME = this.getClass().getSimpleName();
61 private final Logger logger = LoggerFactory.getLogger(this.getClass());
63 // Initialization tasks
64 public UpdateObjectLocationBatchJob() {
65 setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE, INVOCATION_MODE_LIST));
69 * The main work logic of the batch job. Will be called after setContext.
74 setCompletionStatus(STATUS_MIN_PROGRESS);
78 List<String> csids = new ArrayList<String>();
80 // Build a list of CollectionObject records to process via this
81 // batch job, depending on the invocation mode requested.
82 if (requestIsForInvocationModeSingle()) {
83 String singleCsid = getInvocationContext().getSingleCSID();
84 if (Tools.isBlank(singleCsid)) {
85 throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
87 csids.add(singleCsid);
89 } else if (requestIsForInvocationModeList()) {
90 List<String> listCsids = getListCsids();
91 if (listCsids.isEmpty()) {
92 throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
94 csids.addAll(listCsids);
95 } else if (requestIsForInvocationModeGroup()) {
96 // Currently not supported
97 // FIXME: Get individual CSIDs, if any, from the group
98 // String groupCsid = getInvocationContext().getGroupCSID();
100 } else if (requestIsForInvocationModeNoContext()) {
101 // Currently not supported
102 // FIXME: Add code to invoke batch job on every active CollectionObject
105 // Update the value of the computed current location field for each CollectionObject
106 setResults(updateComputedCurrentLocations(csids));
107 setCompletionStatus(STATUS_COMPLETE);
109 } catch (Exception e) {
110 String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage();
111 setErrorResult(errMsg);
116 private InvocationResults updateComputedCurrentLocations(List<String> csids) {
118 ResourceMap resourcemap = getResourceMap();
119 ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
120 ResourceBase movementResource = resourcemap.get(MovementClient.SERVICE_NAME);
121 String computedCurrentLocation;
122 PoxPayloadOut payloadOut;
127 // For each CollectionObject record
128 for (String collectionObjectCsid : csids) {
130 // Skip over soft-deleted CollectionObject records
131 if (isRecordDeleted(collectionObjectResource, collectionObjectCsid)) {
132 if (logger.isTraceEnabled()) {
133 logger.trace("Skipping soft-deleted CollectionObject record with CSID " + collectionObjectCsid);
138 // Get the Movement records related to this record
139 AbstractCommonList relatedMovements = getRelatedMovements(movementResource, collectionObjectCsid);
141 // Skip over CollectionObject records that have no related Movement records
142 if (relatedMovements.getListItem().isEmpty()) {
146 // Based on data in its related Movement records, compute the
147 // current location of this CollectionObject
148 computedCurrentLocation = computeCurrentLocation(relatedMovements);
150 // Skip over CollectionObject records where no computed current
151 // location value can be obtained from related Movement records.
153 // FIXME: Clarify: it ever necessary to 'unset' a computed
154 // current location value, by setting it to a null or empty value,
155 // if that value is no longer obtainable from related Movement records?
156 if (Tools.isBlank(computedCurrentLocation)) {
160 // Update the value of the computed current location field
161 // in the CollectionObject record
162 numAffected = updateComputedCurrentLocationValue(collectionObjectResource,
163 collectionObjectCsid, computedCurrentLocation, resourcemap, numAffected);
166 } catch (Exception e) {
167 String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage() + " ";
168 errMsg = errMsg + "Successfully updated " + numAffected + " CollectionObject record(s) prior to error.";
169 logger.error(errMsg);
170 setErrorResult(errMsg);
171 getResults().setNumAffected(numAffected);
175 logger.info("Updated computedCurrentLocation values in " + numAffected + " CollectionObject record(s).");
176 getResults().setNumAffected(numAffected);
180 private AbstractCommonList getRelatedMovements(ResourceBase movementResource, String csid)
181 throws URISyntaxException, DocumentException {
183 // Get movement records related to a record, specified by its CSID,
184 // where the record is the object of the relation
185 // FIXME: Get query string(s) from constant(s), where appropriate
186 String queryString = "rtObj=" + csid + NONDELETED_QUERY_COMPONENT;
187 UriInfo uriInfo = createRelatedRecordsUriInfo(queryString);
188 AbstractCommonList relatedMovements = movementResource.getList(uriInfo);
189 if (logger.isTraceEnabled()) {
190 logger.trace("Identified " + relatedMovements.getTotalItems()
191 + " Movement record(s) related to the object CollectionObject record " + csid);
194 // Get movement records related to a record, specified by its CSID,
195 // where the record is the subject of the relation
196 // FIXME: Get query string(s) from constant(s), where appropriate
197 queryString = "rtSbj=" + csid + NONDELETED_QUERY_COMPONENT;
198 uriInfo = createRelatedRecordsUriInfo(queryString);
199 AbstractCommonList reverseRelatedMovements = movementResource.getList(uriInfo);
200 if (logger.isTraceEnabled()) {
201 logger.trace("Identified " + reverseRelatedMovements.getTotalItems()
202 + " Movement record(s) related to the subject CollectionObject record " + csid);
205 // If the second list contains any related movement records,
206 // merge it into the first list
207 if (reverseRelatedMovements.getListItem().size() > 0) {
208 relatedMovements.getListItem().addAll(reverseRelatedMovements.getListItem());
211 if (logger.isTraceEnabled()) {
212 logger.trace("Identified a total of " + relatedMovements.getListItem().size()
213 + " Movement record(s) related to the subject CollectionObject record " + csid);
216 return relatedMovements;
219 private String computeCurrentLocation(AbstractCommonList relatedMovements) {
220 String computedCurrentLocation;
222 Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
223 computedCurrentLocation = "";
224 String currentLocation;
226 String mostRecentLocationDate = "";
227 for (AbstractCommonList.ListItem movementRecord : relatedMovements.getListItem()) {
228 movementCsid = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CSID_ELEMENT_NAME);
229 if (Tools.isBlank(movementCsid)) {
232 // Avoid processing any related Movement record more than once,
233 // regardless of the directionality of its relation(s) to this
234 // CollectionObject record.
235 if (alreadyProcessedMovementCsids.contains(movementCsid)) {
238 alreadyProcessedMovementCsids.add(movementCsid);
240 locationDate = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, LOCATION_DATE_ELEMENT_NAME);
241 if (Tools.isBlank(locationDate)) {
244 currentLocation = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CURRENT_LOCATION_ELEMENT_NAME);
245 if (Tools.isBlank(currentLocation)) {
248 if (logger.isTraceEnabled()) {
249 logger.trace("Location date value = " + locationDate);
250 logger.trace("Current location value = " + currentLocation);
252 // If this record's location date value is more recent than that of other
253 // Movement records processed so far, set the computed current location
254 // to its current location value.
256 // Assumes that all values for this element/field will be consistent ISO 8601
257 // date/time representations, each of which can be ordered via string comparison.
259 // If this is *not* the case, we can instead parse and convert these values
260 // to date/time objects.
261 if (locationDate.compareTo(mostRecentLocationDate) > 0) {
262 mostRecentLocationDate = locationDate;
263 // FIXME: Add optional validation here that the currentLocation value
264 // parses successfully as an item refName. (We might make that validation
265 // dependent on the value of a parameter passed in during batch job invocation.)
266 computedCurrentLocation = currentLocation;
270 return computedCurrentLocation;
273 private int updateComputedCurrentLocationValue(ResourceBase collectionObjectResource,
274 String collectionObjectCsid, String computedCurrentLocation, ResourceMap resourcemap, int numAffected)
275 throws DocumentException, URISyntaxException {
276 PoxPayloadOut collectionObjectPayload;
279 collectionObjectPayload = findByCsid(collectionObjectResource, collectionObjectCsid);
280 if (Tools.notBlank(collectionObjectPayload.toXML())) {
281 if (logger.isTraceEnabled()) {
282 logger.trace("Payload: " + "\n" + collectionObjectPayload);
284 objectNumber = getFieldElementValue(collectionObjectPayload,
285 COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
286 OBJECT_NUMBER_ELEMENT_NAME);
287 if (logger.isTraceEnabled()) {
288 logger.trace("Object number: " + objectNumber);
290 if (Tools.notBlank(objectNumber)) {
291 String collectionObjectUpdatePayload =
292 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
293 + "<document name=\"collectionobject\">"
294 + " <ns2:collectionobjects_common "
295 + " xmlns:ns2=\"http://collectionspace.org/services/collectionobject\">"
296 + " <objectNumber>" + objectNumber + "</objectNumber>"
297 + " <computedCurrentLocation>" + computedCurrentLocation + "</computedCurrentLocation>"
298 + " </ns2:collectionobjects_common>"
300 if (logger.isTraceEnabled()) {
301 logger.trace("Update payload: " + "\n" + collectionObjectUpdatePayload);
303 byte[] response = collectionObjectResource.update(resourcemap, null, collectionObjectCsid,
304 collectionObjectUpdatePayload);
306 if (logger.isTraceEnabled()) {
307 logger.trace("Computed current location value for CollectionObject " + collectionObjectCsid
308 + " was set to " + computedCurrentLocation);
316 // #################################################################
317 // Ray's convenience methods from his AbstractBatchJob class for the
318 // UC Berkeley Botanical Garden v2.4 implementation.
319 // #################################################################
320 protected PoxPayloadOut findByCsid(String serviceName, String csid) throws URISyntaxException, DocumentException {
321 ResourceBase resource = getResourceMap().get(serviceName);
322 return findByCsid(resource, csid);
325 protected PoxPayloadOut findByCsid(ResourceBase resource, String csid) throws URISyntaxException, DocumentException {
326 byte[] response = resource.get(null, createUriInfo(), csid);
327 PoxPayloadOut payload = new PoxPayloadOut(response);
331 protected UriInfo createUriInfo() throws URISyntaxException {
332 return createUriInfo("");
335 protected UriInfo createUriInfo(String queryString) throws URISyntaxException {
336 URI absolutePath = new URI("");
337 URI baseUri = new URI("");
338 return new UriInfoImpl(absolutePath, baseUri, "", queryString, Collections.<PathSegment>emptyList());
341 // #################################################################
342 // Other convenience methods
343 // #################################################################
344 protected UriInfo createRelatedRecordsUriInfo(String query) throws URISyntaxException {
345 URI uri = new URI(null, null, null, query, null);
346 return createUriInfo(uri.getRawQuery());
349 protected String getFieldElementValue(PoxPayloadOut payload, String partLabel, Namespace partNamespace, String fieldPath) {
351 SAXBuilder builder = new SAXBuilder();
353 Document document = builder.build(new StringReader(payload.toXML()));
354 Element root = document.getRootElement();
355 // The part element is always expected to have an explicit namespace.
356 Element part = root.getChild(partLabel, partNamespace);
357 // Try getting the field element both with and without a namespace.
358 // Even though a field element that lacks a namespace prefix
359 // may yet inherit its namespace from a parent, JDOM may require that
360 // the getChild() call be made without a namespace.
361 Element field = part.getChild(fieldPath, partNamespace);
363 field = part.getChild(fieldPath);
366 value = field.getText();
368 } catch (Exception e) {
369 logger.error("Error getting value from field path " + fieldPath
370 + " in schema part " + partLabel);
376 private boolean isRecordDeleted(ResourceBase resource, String collectionObjectCsid) throws URISyntaxException, DocumentException {
377 boolean isDeleted = false;
378 byte[] workflowResponse = resource.getWorkflow(createUriInfo(), collectionObjectCsid);
379 if (workflowResponse != null) {
380 PoxPayloadOut payloadOut = new PoxPayloadOut(workflowResponse);
381 String workflowState =
382 getFieldElementValue(payloadOut, WORKFLOW_COMMON_SCHEMA_NAME,
383 WORKFLOW_COMMON_NAMESPACE, LIFECYCLE_STATE_ELEMENT_NAME);
384 if (Tools.notBlank(workflowState) && workflowState.contentEquals(WorkflowClient.WORKFLOWSTATE_DELETED)) {