1 package org.collectionspace.services.batch.nuxeo;
4 import java.net.URISyntaxException;
5 import java.util.ArrayList;
6 import java.util.Arrays;
7 import java.util.Collections;
8 import java.util.HashMap;
11 import javax.ws.rs.core.PathSegment;
12 import javax.ws.rs.core.UriInfo;
13 import org.collectionspace.services.batch.AbstractBatchInvocable;
14 import org.collectionspace.services.client.AbstractCommonListUtils;
15 import org.collectionspace.services.client.CollectionObjectClient;
16 import org.collectionspace.services.client.MovementClient;
17 import org.collectionspace.services.client.PayloadOutputPart;
18 import org.collectionspace.services.client.PoxPayloadOut;
19 import org.collectionspace.services.client.RelationClient;
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.collectionspace.services.movement.nuxeo.MovementConstants;
26 import org.collectionspace.services.nuxeo.client.java.RepositoryJavaClientImpl;
27 import org.collectionspace.services.nuxeo.util.NuxeoUtils;
29 import org.dom4j.DocumentException;
30 import org.dom4j.DocumentHelper;
31 import org.dom4j.Element;
32 import org.dom4j.Node;
33 import org.dom4j.XPath;
34 import org.jboss.resteasy.specimpl.UriInfoImpl;
35 import org.nuxeo.ecm.core.api.DocumentModel;
36 import org.nuxeo.ecm.core.api.DocumentModelList;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
42 // FIXME: Where appropriate, get from existing constants rather than local declarations
43 private final static String COLLECTIONOBJECTS_COMMON_SCHEMA_NAME = "collectionobjects_common";
44 private final static String COLLECTIONOBJECTS_COMMON_SCHEMA = "collectionobjects_common";
45 private final static String MOVEMENTS_COMMON_SCHEMA = MovementConstants.NUXEO_SCHEMA_NAME;
46 private final static String COMPUTED_CURRENT_LOCATION_ELEMENT_NAME = "computedCurrentLocation";
47 private final static String CURRENT_LOCATION_ELEMENT_NAME = "currentLocation";
48 private final static String LOCATION_DATE_ELEMENT_NAME = "locationDate";
49 private final static String OBJECT_NUMBER_ELEMENT_NAME = "objectNumber";
50 private InvocationResults results = new InvocationResults();
51 private final String CLASSNAME = this.getClass().getSimpleName();
52 private final Logger logger = LoggerFactory.getLogger(UpdateObjectLocationBatchJob.class);
54 // Initialization tasks
55 public UpdateObjectLocationBatchJob() {
56 setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE, INVOCATION_MODE_LIST));
60 * The main work logic of the batch job. Will be called after setContext.
65 setCompletionStatus(STATUS_MIN_PROGRESS);
68 // FIXME: Placeholder during early development
69 if (logger.isInfoEnabled()) {
70 logger.info("Invoking " + CLASSNAME + " ...");
71 logger.info("Invocation context is: " + getInvocationContext().getMode());
74 if (!requestedInvocationModeIsSupported()) {
75 setInvocationModeNotSupportedResult();
78 List<String> csids = new ArrayList<String>();
79 if (requestIsForInvocationModeSingle()) {
80 String singleCsid = getInvocationContext().getSingleCSID();
81 if (Tools.notBlank(singleCsid)) {
82 csids.add(singleCsid);
84 } else if (requestIsForInvocationModeList()) {
85 List<String> listCsids = (getInvocationContext().getListCSIDs().getCsid());
86 if (listCsids == null || listCsids.isEmpty()) {
87 throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT_MESSAGE);
89 csids.addAll(listCsids);
90 } else if (requestIsForInvocationModeGroup()) {
91 String groupCsid = getInvocationContext().getGroupCSID();
92 // FIXME: Get individual CSIDs from the group
93 // and add them to the list
96 if (csids.isEmpty()) {
97 throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT_MESSAGE);
100 // Update the current computed location field for each CollectionObject
101 setResults(updateComputedCurrentLocations(csids));
102 setCompletionStatus(STATUS_COMPLETE);
104 } catch (Exception e) {
105 String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage();
106 setErrorResult(errMsg);
111 // Ray's convenience methods from his AbstractBatchJob class for the UC Berkeley Botanical Garden v2.4 implementation.
112 protected PoxPayloadOut findByCsid(String serviceName, String csid) throws URISyntaxException, DocumentException {
113 ResourceBase resource = getResourceMap().get(serviceName);
114 return findByCsid(resource, csid);
117 protected PoxPayloadOut findByCsid(ResourceBase resource, String csid) throws URISyntaxException, DocumentException {
118 byte[] response = resource.get(null, createUriInfo(), csid);
119 PoxPayloadOut payload = new PoxPayloadOut(response);
123 protected UriInfo createUriInfo() throws URISyntaxException {
124 return createUriInfo("");
127 protected UriInfo createUriInfo(String queryString) throws URISyntaxException {
128 URI absolutePath = new URI("");
129 URI baseUri = new URI("");
130 return new UriInfoImpl(absolutePath, baseUri, "", queryString, Collections.<PathSegment>emptyList());
133 protected UriInfo createRelationSearchUriInfo(String subjectCsid, String objType) throws URISyntaxException {
134 String queryString = "sbj=" + subjectCsid + "&objType=" + objType;
135 URI uri = new URI(null, null, null, queryString, null);
136 return createUriInfo(uri.getRawQuery());
140 * Get a field value from a PoxPayloadOut, given a part name and
141 * namespace-qualified xpath expression.
143 protected String getFieldValue(PoxPayloadOut payload, String partLabel, String namespacePrefix, String namespace, String fieldPath) {
145 PayloadOutputPart part = payload.getPart(partLabel);
148 Element element = part.asElement();
149 logger.info(partLabel + " part element =" + element.asXML());
151 Map<String, String> namespaceUris = new HashMap<String, String>();
152 namespaceUris.put(namespacePrefix, namespace);
154 XPath xPath = DocumentHelper.createXPath(fieldPath);
155 xPath.setNamespaceURIs(namespaceUris);
157 Node node = xPath.selectSingleNode(element);
158 // Node node = element.selectSingleNode(fieldPath);
161 value = node.getText();
168 protected List<String> getFieldValues(PoxPayloadOut payload, String partLabel, String fieldPath) {
169 List<String> values = new ArrayList<String>();
170 PayloadOutputPart part = payload.getPart(partLabel);
173 Element element = part.asElement();
174 List<Node> nodes = element.selectNodes(fieldPath);
177 for (Node node : nodes) {
178 values.add(node.getText());
186 private InvocationResults updateComputedCurrentLocations(List<String> csids) {
188 ResourceMap resourcemap = getResourceMap();
189 ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
190 ResourceBase movementResource = resourcemap.get(MovementClient.SERVICE_NAME);
191 PoxPayloadOut collectionObjectPayload;
193 String computedCurrentLocation;
195 // FIXME: Temporary during testing/development
196 final String COMPUTED_CURRENT_LOCATION = "FOO_COMPUTED_CURRENT_LOCATION";
200 // For each CollectionObject record:
201 for (String csid : csids) {
203 // Get the movement records related to this record
205 // FIXME: Create a convenience method for constructing queries like the following
206 String queryString = "rtObj=" + csid;
207 URI uri = new URI(null, null, null, queryString, null);
208 UriInfo uriInfo = createUriInfo(uri.getRawQuery());
210 AbstractCommonList relatedMovements = movementResource.getList(uriInfo);
211 if (logger.isInfoEnabled()) {
212 logger.info("Identified " + relatedMovements.getTotalItems()
213 + " movement records related to CollectionObject record " + csid);
216 // FIXME: Get relation records in the reverse direction as well,
217 // via rtSbj=, merge with records obtained above, and remove duplicates
219 queryString = "rtSbj=" + csid;
220 uri = new URI(null, null, null, queryString, null);
221 uriInfo = createUriInfo(uri.getRawQuery());
223 AbstractCommonList reverseRelatedMovements = movementResource.getList(uriInfo);
224 if (logger.isInfoEnabled()) {
225 logger.info("Identified " + reverseRelatedMovements.getTotalItems()
226 + " movement records related in the reverse to CollectionObject record " + csid);
229 if ((relatedMovements.getTotalItems() == 0) && reverseRelatedMovements.getTotalItems() == 0) {
233 // Merge the two lists
234 relatedMovements.getListItem().addAll(reverseRelatedMovements.getListItem());
236 // Get the latest movement record from among those, and extract
237 // its current location value
238 computedCurrentLocation = "";
239 String currentLocation;
241 String mostRecentLocationDate = "";
242 for (AbstractCommonList.ListItem movementRecord : relatedMovements.getListItem()) {
243 locationDate = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, LOCATION_DATE_ELEMENT_NAME);
244 if (Tools.notBlank(locationDate)) {
245 if (logger.isInfoEnabled()) {
246 logger.info("Location date value = " + locationDate);
249 currentLocation = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CURRENT_LOCATION_ELEMENT_NAME);
250 if (Tools.notBlank(currentLocation)) {
251 if (logger.isInfoEnabled()) {
252 logger.info("Current location value = " + currentLocation);
255 if (Tools.notBlank(locationDate) && Tools.notBlank(currentLocation)) {
256 // Assumes that all values for this element/field will be ISO 8601
257 // date representations, each of which can be ordered via string comparison.
258 if (locationDate.compareTo(mostRecentLocationDate) > 0) {
259 mostRecentLocationDate = locationDate;
260 computedCurrentLocation = currentLocation;
266 // Update the computed current location value in the CollectionObject record
267 collectionObjectPayload = findByCsid(collectionObjectResource, csid);
268 if (Tools.notBlank(collectionObjectPayload.toXML())) {
269 if (logger.isInfoEnabled()) {
270 logger.info("Payload: " + "\n" + collectionObjectPayload);
272 // Silently fails at various places in dom4j calls (selectSingleNode, selectNode,
273 // createXpath) in any of the methods tried above, without throwing an Exception
275 objectNumber = getFieldValue(collectionObjectPayload,
276 COLLECTIONOBJECTS_COMMON_SCHEMA_NAME,
277 "ns2", "http://collectionspace.org/services/collectionobject",
278 OBJECT_NUMBER_ELEMENT_NAME);
279 if (logger.isInfoEnabled()) {
280 logger.info("Object number: " + objectNumber);
283 objectNumber = "BAR"; // FIXME
284 if (Tools.notBlank(objectNumber)) {
285 String collectionObjectUpdatePayload =
286 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
287 + "<document name=\"collectionobject\">"
288 + "<ns2:collectionobjects_common xmlns:ns2=\"http://collectionspace.org/services/collectionobject\">"
289 + "<objectNumber>" + objectNumber + "</objectNumber>"
290 + "<computedCurrentLocation>" + computedCurrentLocation + "</computedCurrentLocation>"
291 + "</ns2:collectionobjects_common></document>";
292 if (logger.isInfoEnabled()) {
293 logger.info("Update payload: " + "\n" + collectionObjectUpdatePayload);
295 byte[] response = collectionObjectResource.update(resourcemap, null, csid, collectionObjectUpdatePayload);
297 if (logger.isTraceEnabled()) {
298 logger.trace("Computed current location value for CollectionObject " + csid + " set to " + computedCurrentLocation);
305 } catch (Exception e) {
306 String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage() + " ";
307 errMsg = errMsg + "Successfully updated " + numAffected + " CollectionObject record(s) prior to error.";
308 setErrorResult(errMsg);
309 getResults().setNumAffected(numAffected);
313 getResults().setNumAffected(numAffected);