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.jdom.Document;
30 import org.jdom.Element;
31 import org.jdom.input.SAXBuilder;
33 import java.io.StringReader;
35 import org.dom4j.DocumentException;
39 import org.dom4j.DocumentHelper;
40 import org.dom4j.Element;
41 import org.dom4j.Node;
42 import org.dom4j.XPath;
45 import org.jboss.resteasy.specimpl.UriInfoImpl;
46 import org.jdom.Namespace;
47 import org.nuxeo.ecm.core.api.DocumentModel;
48 import org.nuxeo.ecm.core.api.DocumentModelList;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
52 public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
54 // FIXME: Where appropriate, get from existing constants rather than local declarations
55 private final static String COLLECTIONOBJECTS_COMMON_SCHEMA_NAME = "collectionobjects_common";
56 private final static String COLLECTIONOBJECTS_COMMON_SCHEMA = "collectionobjects_common";
57 private final static String MOVEMENTS_COMMON_SCHEMA = MovementConstants.NUXEO_SCHEMA_NAME;
58 private final static String COMPUTED_CURRENT_LOCATION_ELEMENT_NAME = "computedCurrentLocation";
59 private final static String CURRENT_LOCATION_ELEMENT_NAME = "currentLocation";
60 private final static String LOCATION_DATE_ELEMENT_NAME = "locationDate";
61 private final static String OBJECT_NUMBER_ELEMENT_NAME = "objectNumber";
62 private final static Namespace COLLECTIONOBJECTS_COMMON_NAMESPACE =
63 Namespace.getNamespace("ns2", "http://collectionspace.org/services/collectionobject");
64 private InvocationResults results = new InvocationResults();
65 private final String CLASSNAME = this.getClass().getSimpleName();
66 private final Logger logger = LoggerFactory.getLogger(UpdateObjectLocationBatchJob.class);
68 // Initialization tasks
69 public UpdateObjectLocationBatchJob() {
70 setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE, INVOCATION_MODE_LIST));
74 * The main work logic of the batch job. Will be called after setContext.
79 setCompletionStatus(STATUS_MIN_PROGRESS);
82 // FIXME: Placeholder during early development
83 if (logger.isInfoEnabled()) {
84 logger.info("Invoking " + CLASSNAME + " ...");
85 logger.info("Invocation context is: " + getInvocationContext().getMode());
88 if (!requestedInvocationModeIsSupported()) {
89 setInvocationModeNotSupportedResult();
92 List<String> csids = new ArrayList<String>();
93 if (requestIsForInvocationModeSingle()) {
94 String singleCsid = getInvocationContext().getSingleCSID();
95 if (Tools.notBlank(singleCsid)) {
96 csids.add(singleCsid);
98 } else if (requestIsForInvocationModeList()) {
99 List<String> listCsids = (getInvocationContext().getListCSIDs().getCsid());
100 if (listCsids == null || listCsids.isEmpty()) {
101 throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT_MESSAGE);
103 csids.addAll(listCsids);
104 } else if (requestIsForInvocationModeGroup()) {
105 String groupCsid = getInvocationContext().getGroupCSID();
106 // FIXME: Get individual CSIDs from the group
107 // and add them to the list
110 if (csids.isEmpty()) {
111 throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT_MESSAGE);
114 // Update the current computed location field for each CollectionObject
115 setResults(updateComputedCurrentLocations(csids));
116 setCompletionStatus(STATUS_COMPLETE);
118 } catch (Exception e) {
119 String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage();
120 setErrorResult(errMsg);
125 // Ray's convenience methods from his AbstractBatchJob class for the UC Berkeley Botanical Garden v2.4 implementation.
126 protected PoxPayloadOut findByCsid(String serviceName, String csid) throws URISyntaxException, DocumentException {
127 ResourceBase resource = getResourceMap().get(serviceName);
128 return findByCsid(resource, csid);
131 protected PoxPayloadOut findByCsid(ResourceBase resource, String csid) throws URISyntaxException, DocumentException {
132 byte[] response = resource.get(null, createUriInfo(), csid);
133 PoxPayloadOut payload = new PoxPayloadOut(response);
137 protected UriInfo createUriInfo() throws URISyntaxException {
138 return createUriInfo("");
141 protected UriInfo createUriInfo(String queryString) throws URISyntaxException {
142 URI absolutePath = new URI("");
143 URI baseUri = new URI("");
144 return new UriInfoImpl(absolutePath, baseUri, "", queryString, Collections.<PathSegment>emptyList());
147 protected UriInfo createRelationSearchUriInfo(String subjectCsid, String objType) throws URISyntaxException {
148 String queryString = "sbj=" + subjectCsid + "&objType=" + objType;
149 URI uri = new URI(null, null, null, queryString, null);
150 return createUriInfo(uri.getRawQuery());
153 protected String getFieldValue(PoxPayloadOut payload, String partLabel, Namespace partNamespace, String fieldPath) {
155 SAXBuilder builder = new SAXBuilder();
157 Document document = builder.build(new StringReader(payload.toXML()));
158 Element root = document.getRootElement();
159 // The part element is always expected to have an explicit namespace.
160 Element part = root.getChild(partLabel, partNamespace);
161 // Try getting the field element both with and without a namespace.
162 // Even though a field element that lacks a namespace prefix
163 // may yet inherit its namespace from a parent, JDOM may require that
164 // the getChild() call be made without a namespace.
165 Element field = part.getChild(fieldPath, partNamespace);
167 field = part.getChild(fieldPath);
170 value = field.getText();
172 } catch (Exception e) {
173 logger.error("Error getting value from field path " + fieldPath
174 + " in schema part " + partLabel);
181 * Get a field value from a PoxPayloadOut, given a part name and xpath
185 protected String getFieldValue(PoxPayloadOut payload, String partLabel, String fieldPath) {
187 PayloadOutputPart part = payload.getPart(partLabel);
190 Element element = part.asElement();
191 Node node = element.selectSingleNode(fieldPath);
194 value = node.getText();
202 * Get a field value from a PoxPayloadOut, given a part name and
203 * namespace-qualified xpath expression.
206 protected String getFieldValue(PoxPayloadOut payload, String partLabel, String namespacePrefix, String namespace, String fieldPath) {
208 PayloadOutputPart part = payload.getPart(partLabel);
211 Element element = part.asElement();
212 logger.info(partLabel + " part element =" + element.asXML());
214 Map<String, String> namespaceUris = new HashMap<String, String>();
215 namespaceUris.put(namespacePrefix, namespace);
217 XPath xPath = DocumentHelper.createXPath(fieldPath);
218 xPath.setNamespaceURIs(namespaceUris);
220 Node node = xPath.selectSingleNode(element);
221 // Node node = element.selectSingleNode(fieldPath);
224 value = node.getText();
231 private InvocationResults updateComputedCurrentLocations(List<String> csids) {
233 ResourceMap resourcemap = getResourceMap();
234 ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
235 ResourceBase movementResource = resourcemap.get(MovementClient.SERVICE_NAME);
236 PoxPayloadOut collectionObjectPayload;
238 String computedCurrentLocation;
243 // For each CollectionObject record:
244 for (String csid : csids) {
246 // Get the movement records related to this record
248 // FIXME: Create a convenience method for constructing queries like the following
249 String queryString = "rtObj=" + csid; // FIXME: Get from constant
250 URI uri = new URI(null, null, null, queryString, null);
251 UriInfo uriInfo = createUriInfo(uri.getRawQuery());
253 AbstractCommonList relatedMovements = movementResource.getList(uriInfo);
254 if (logger.isInfoEnabled()) {
255 logger.info("Identified " + relatedMovements.getTotalItems()
256 + " Movement records related to the object CollectionObject record " + csid);
259 // Get relation records in the reverse direction as well,
260 // merge with records obtained above, and remove duplicates
261 queryString = "rtSbj=" + csid; // FIXME: Get from constant
262 uri = new URI(null, null, null, queryString, null);
263 uriInfo = createUriInfo(uri.getRawQuery());
265 AbstractCommonList reverseRelatedMovements = movementResource.getList(uriInfo);
266 if (logger.isInfoEnabled()) {
267 logger.info("Identified " + reverseRelatedMovements.getTotalItems()
268 + " Movement records related to the subject CollectionObject record " + csid);
271 if ((relatedMovements.getTotalItems() == 0) && reverseRelatedMovements.getTotalItems() == 0) {
275 // Merge the two lists
276 relatedMovements.getListItem().addAll(reverseRelatedMovements.getListItem());
278 // Get the latest movement record from among those, and extract
279 // its current location value
280 computedCurrentLocation = "";
281 String currentLocation;
283 String mostRecentLocationDate = "";
284 for (AbstractCommonList.ListItem movementRecord : relatedMovements.getListItem()) {
286 // FIXME: Add 'de-duping' code here to avoid processing any
287 // related Movement record more than once
289 locationDate = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, LOCATION_DATE_ELEMENT_NAME);
290 if (Tools.isBlank(locationDate)) {
293 currentLocation = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CURRENT_LOCATION_ELEMENT_NAME);
294 if (Tools.isBlank(currentLocation)) {
297 if (logger.isInfoEnabled()) {
298 logger.info("Location date value = " + locationDate);
299 logger.info("Current location value = " + currentLocation);
301 // Assumes that all values for this element/field will be consistent ISO 8601
302 // date/time representations, each of which can be ordered via string comparison.
303 if (locationDate.compareTo(mostRecentLocationDate) > 0) {
304 mostRecentLocationDate = locationDate;
305 // FIXME: Add validation here that the currentLocation value parses successfully as an item refName
306 computedCurrentLocation = currentLocation;
311 // Update the computed current location value in the CollectionObject record
312 collectionObjectPayload = findByCsid(collectionObjectResource, csid);
313 if (Tools.notBlank(collectionObjectPayload.toXML())) {
314 if (logger.isInfoEnabled()) {
315 logger.info("Payload: " + "\n" + collectionObjectPayload);
317 // Silently fails at various places in dom4j calls (selectSingleNode, selectNode,
318 // createXpath) in any of the methods tried, without throwing an Exception.
320 // Those methods are now commented out, in favor of a replacement, however temporary,
323 // FIXME: Get namespace from constant; verify whether prefix or URI is required
324 objectNumber = getFieldValue(collectionObjectPayload,
325 COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
326 OBJECT_NUMBER_ELEMENT_NAME);
327 if (logger.isInfoEnabled()) {
328 logger.info("Object number: " + objectNumber);
330 if (Tools.notBlank(objectNumber)) {
331 String collectionObjectUpdatePayload =
332 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
333 + "<document name=\"collectionobject\">"
334 + "<ns2:collectionobjects_common xmlns:ns2=\"http://collectionspace.org/services/collectionobject\">"
335 + "<objectNumber>" + objectNumber + "</objectNumber>"
336 + "<computedCurrentLocation>" + computedCurrentLocation + "</computedCurrentLocation>"
337 + "</ns2:collectionobjects_common></document>";
338 if (logger.isInfoEnabled()) {
339 logger.info("Update payload: " + "\n" + collectionObjectUpdatePayload);
341 byte[] response = collectionObjectResource.update(resourcemap, null, csid, collectionObjectUpdatePayload);
343 if (logger.isTraceEnabled()) {
344 logger.trace("Computed current location value for CollectionObject " + csid + " set to " + computedCurrentLocation);
351 } catch (Exception e) {
352 String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage() + " ";
353 errMsg = errMsg + "Successfully updated " + numAffected + " CollectionObject record(s) prior to error.";
354 logger.error(errMsg);
355 setErrorResult(errMsg);
356 getResults().setNumAffected(numAffected);
360 getResults().setNumAffected(numAffected);