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.common.ResourceBase;
20 import org.collectionspace.services.common.ResourceMap;
21 import org.collectionspace.services.common.api.Tools;
22 import org.collectionspace.services.common.invocable.InvocationResults;
23 import org.collectionspace.services.jaxb.AbstractCommonList;
24 import org.dom4j.DocumentException;
25 import org.jboss.resteasy.specimpl.UriInfoImpl;
26 import org.jdom.Document;
27 import org.jdom.Element;
28 import org.jdom.Namespace;
29 import org.jdom.input.SAXBuilder;
30 import org.slf4j.Logger;
31 import org.slf4j.LoggerFactory;
33 public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
35 // FIXME: Where appropriate, get from existing constants rather than local declarations
36 private final static String COLLECTIONOBJECTS_COMMON_SCHEMA_NAME = "collectionobjects_common";
37 private final static String CSID_ELEMENT_NAME = "csid";
38 private final static String CURRENT_LOCATION_ELEMENT_NAME = "currentLocation";
39 private final static String LOCATION_DATE_ELEMENT_NAME = "locationDate";
40 private final static String OBJECT_NUMBER_ELEMENT_NAME = "objectNumber";
41 private final static String COLLECTIONOBJECTS_COMMON_NAMESPACE_PREFIX = "ns2";
42 private final static String COLLECTIONOBJECTS_COMMON_NAMESPACE_URI =
43 "http://collectionspace.org/services/collectionobject";
44 private final static Namespace COLLECTIONOBJECTS_COMMON_NAMESPACE =
45 Namespace.getNamespace(
46 COLLECTIONOBJECTS_COMMON_NAMESPACE_PREFIX,
47 COLLECTIONOBJECTS_COMMON_NAMESPACE_URI);
48 private final String CLASSNAME = this.getClass().getSimpleName();
49 private final Logger logger = LoggerFactory.getLogger(this.getClass());
51 // Initialization tasks
52 public UpdateObjectLocationBatchJob() {
53 setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE, INVOCATION_MODE_LIST));
57 * The main work logic of the batch job. Will be called after setContext.
62 setCompletionStatus(STATUS_MIN_PROGRESS);
65 if (logger.isTraceEnabled()) {
66 logger.trace("Invoking " + CLASSNAME + " ...");
67 logger.trace("Invocation context is: " + getInvocationContext().getMode());
70 List<String> csids = new ArrayList<String>();
71 if (requestIsForInvocationModeSingle()) {
72 String singleCsid = getInvocationContext().getSingleCSID();
73 if (Tools.notBlank(singleCsid)) {
74 csids.add(singleCsid);
76 } else if (requestIsForInvocationModeList()) {
77 List<String> listCsids = (getInvocationContext().getListCSIDs().getCsid());
78 if (listCsids == null || listCsids.isEmpty()) {
79 throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
81 csids.addAll(listCsids);
82 } else if (requestIsForInvocationModeGroup()) {
83 String groupCsid = getInvocationContext().getGroupCSID();
84 // FIXME: Get individual CSIDs from the group
85 // and add them to the list
88 if (csids.isEmpty()) {
89 throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
92 // Update the computed current location field for each CollectionObject
93 setResults(updateComputedCurrentLocations(csids));
94 setCompletionStatus(STATUS_COMPLETE);
96 } catch (Exception e) {
97 String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage();
98 setErrorResult(errMsg);
103 // Ray's convenience methods from his AbstractBatchJob class for the UC Berkeley Botanical Garden v2.4 implementation.
104 protected PoxPayloadOut findByCsid(String serviceName, String csid) throws URISyntaxException, DocumentException {
105 ResourceBase resource = getResourceMap().get(serviceName);
106 return findByCsid(resource, csid);
109 protected PoxPayloadOut findByCsid(ResourceBase resource, String csid) throws URISyntaxException, DocumentException {
110 byte[] response = resource.get(null, createUriInfo(), csid);
111 PoxPayloadOut payload = new PoxPayloadOut(response);
115 protected UriInfo createUriInfo() throws URISyntaxException {
116 return createUriInfo("");
119 protected UriInfo createUriInfo(String queryString) throws URISyntaxException {
120 URI absolutePath = new URI("");
121 URI baseUri = new URI("");
122 return new UriInfoImpl(absolutePath, baseUri, "", queryString, Collections.<PathSegment>emptyList());
125 protected UriInfo createRelatedRecordsUriInfo(String query) throws URISyntaxException {
126 URI uri = new URI(null, null, null, query, null);
127 return createUriInfo(uri.getRawQuery());
130 protected String getFieldElementValue(PoxPayloadOut payload, String partLabel, Namespace partNamespace, String fieldPath) {
132 SAXBuilder builder = new SAXBuilder();
134 Document document = builder.build(new StringReader(payload.toXML()));
135 Element root = document.getRootElement();
136 // The part element is always expected to have an explicit namespace.
137 Element part = root.getChild(partLabel, partNamespace);
138 // Try getting the field element both with and without a namespace.
139 // Even though a field element that lacks a namespace prefix
140 // may yet inherit its namespace from a parent, JDOM may require that
141 // the getChild() call be made without a namespace.
142 Element field = part.getChild(fieldPath, partNamespace);
144 field = part.getChild(fieldPath);
147 value = field.getText();
149 } catch (Exception e) {
150 logger.error("Error getting value from field path " + fieldPath
151 + " in schema part " + partLabel);
157 private InvocationResults updateComputedCurrentLocations(List<String> csids) {
159 ResourceMap resourcemap = getResourceMap();
160 ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
161 ResourceBase movementResource = resourcemap.get(MovementClient.SERVICE_NAME);
162 PoxPayloadOut collectionObjectPayload;
163 String computedCurrentLocation;
170 // For each CollectionObject record
171 for (String collectionObjectCsid : csids) {
173 // Get the movement records related to this record
175 // Get movement records related to this record where the CollectionObject
176 // record is the subject of the relation
177 String queryString = "rtObj=" + collectionObjectCsid; // FIXME: Get from constant
178 UriInfo uriInfo = createRelatedRecordsUriInfo(queryString);
180 AbstractCommonList relatedMovements = movementResource.getList(uriInfo);
181 if (logger.isTraceEnabled()) {
182 logger.trace("Identified " + relatedMovements.getTotalItems()
183 + " Movement records related to the object CollectionObject record " + collectionObjectCsid);
186 // Get movement records related to this record where the CollectionObject
187 // record is the object of the relation
188 queryString = "rtSbj=" + collectionObjectCsid; // FIXME: Get from constant
189 uriInfo = createRelatedRecordsUriInfo(queryString);
191 AbstractCommonList reverseRelatedMovements = movementResource.getList(uriInfo);
192 if (logger.isTraceEnabled()) {
193 logger.trace("Identified " + reverseRelatedMovements.getTotalItems()
194 + " Movement records related to the subject CollectionObject record " + collectionObjectCsid);
197 if ((relatedMovements.getTotalItems() == 0) && reverseRelatedMovements.getTotalItems() == 0) {
201 // Merge the two lists of related movement records
202 relatedMovements.getListItem().addAll(reverseRelatedMovements.getListItem());
204 // Get the latest movement record from among those, and extract
205 // its current location value
206 Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
207 computedCurrentLocation = "";
208 String currentLocation;
210 String mostRecentLocationDate = "";
211 for (AbstractCommonList.ListItem movementRecord : relatedMovements.getListItem()) {
212 movementCsid = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CSID_ELEMENT_NAME);
213 if (Tools.isBlank(movementCsid)) {
216 // Avoid processing any related Movement record more than once,
217 // regardless of the directionality of its relation(s) to this
218 // CollectionObject record.
219 if (alreadyProcessedMovementCsids.contains(movementCsid)) {
222 alreadyProcessedMovementCsids.add(movementCsid);
224 locationDate = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, LOCATION_DATE_ELEMENT_NAME);
225 if (Tools.isBlank(locationDate)) {
228 currentLocation = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CURRENT_LOCATION_ELEMENT_NAME);
229 if (Tools.isBlank(currentLocation)) {
232 if (logger.isTraceEnabled()) {
233 logger.trace("Location date value = " + locationDate);
234 logger.trace("Current location value = " + currentLocation);
236 // Assumes that all values for this element/field will be consistent ISO 8601
237 // date/time representations, each of which can be ordered via string comparison.
239 // If this is *not* the case, we can instead parse and convert these values
240 // to date/time objects.
241 if (locationDate.compareTo(mostRecentLocationDate) > 0) {
242 mostRecentLocationDate = locationDate;
243 // FIXME: Add optional validation here that the currentLocation value
244 // parses successfully as an item refName
245 computedCurrentLocation = currentLocation;
250 // Update the computed current location value in the CollectionObject record
251 collectionObjectPayload = findByCsid(collectionObjectResource, collectionObjectCsid);
252 if (Tools.notBlank(collectionObjectPayload.toXML())) {
253 if (logger.isTraceEnabled()) {
254 logger.trace("Payload: " + "\n" + collectionObjectPayload);
256 objectNumber = getFieldElementValue(collectionObjectPayload,
257 COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
258 OBJECT_NUMBER_ELEMENT_NAME);
259 if (logger.isTraceEnabled()) {
260 logger.trace("Object number: " + objectNumber);
262 if (Tools.notBlank(objectNumber)) {
263 String collectionObjectUpdatePayload =
264 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
265 + "<document name=\"collectionobject\">"
266 + " <ns2:collectionobjects_common "
267 + " xmlns:ns2=\"http://collectionspace.org/services/collectionobject\">"
268 + " <objectNumber>" + objectNumber + "</objectNumber>"
269 + " <computedCurrentLocation>" + computedCurrentLocation + "</computedCurrentLocation>"
270 + " </ns2:collectionobjects_common>"
272 if (logger.isTraceEnabled()) {
273 logger.trace("Update payload: " + "\n" + collectionObjectUpdatePayload);
275 byte[] response = collectionObjectResource.update(resourcemap, null, collectionObjectCsid,
276 collectionObjectUpdatePayload);
278 if (logger.isTraceEnabled()) {
279 logger.trace("Computed current location value for CollectionObject " + collectionObjectCsid
280 + " was set to " + computedCurrentLocation);
286 } catch (Exception e) {
287 String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage() + " ";
288 errMsg = errMsg + "Successfully updated " + numAffected + " CollectionObject record(s) prior to error.";
289 logger.error(errMsg);
290 setErrorResult(errMsg);
291 getResults().setNumAffected(numAffected);
295 logger.info("Updated computedCurrentLocation values in " + numAffected + " CollectionObject records.");
296 getResults().setNumAffected(numAffected);