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 Element part = root.getChild(partLabel, partNamespace);
160 Element field = part.getChild(fieldPath, partNamespace);
161 value = field.getText();
162 } catch (Exception e) {
163 logger.error("Error getting value from field path " + fieldPath
164 + " in schema part " + partLabel);
171 * Get a field value from a PoxPayloadOut, given a part name and xpath
175 protected String getFieldValue(PoxPayloadOut payload, String partLabel, String fieldPath) {
177 PayloadOutputPart part = payload.getPart(partLabel);
180 Element element = part.asElement();
181 Node node = element.selectSingleNode(fieldPath);
184 value = node.getText();
192 * Get a field value from a PoxPayloadOut, given a part name and
193 * namespace-qualified xpath expression.
196 protected String getFieldValue(PoxPayloadOut payload, String partLabel, String namespacePrefix, String namespace, String fieldPath) {
198 PayloadOutputPart part = payload.getPart(partLabel);
201 Element element = part.asElement();
202 logger.info(partLabel + " part element =" + element.asXML());
204 Map<String, String> namespaceUris = new HashMap<String, String>();
205 namespaceUris.put(namespacePrefix, namespace);
207 XPath xPath = DocumentHelper.createXPath(fieldPath);
208 xPath.setNamespaceURIs(namespaceUris);
210 Node node = xPath.selectSingleNode(element);
211 // Node node = element.selectSingleNode(fieldPath);
214 value = node.getText();
221 private InvocationResults updateComputedCurrentLocations(List<String> csids) {
223 ResourceMap resourcemap = getResourceMap();
224 ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
225 ResourceBase movementResource = resourcemap.get(MovementClient.SERVICE_NAME);
226 PoxPayloadOut collectionObjectPayload;
228 String computedCurrentLocation;
233 // For each CollectionObject record:
234 for (String csid : csids) {
236 // Get the movement records related to this record
238 // FIXME: Create a convenience method for constructing queries like the following
239 String queryString = "rtObj=" + csid; // FIXME: Get from constant
240 URI uri = new URI(null, null, null, queryString, null);
241 UriInfo uriInfo = createUriInfo(uri.getRawQuery());
243 AbstractCommonList relatedMovements = movementResource.getList(uriInfo);
244 if (logger.isInfoEnabled()) {
245 logger.info("Identified " + relatedMovements.getTotalItems()
246 + " Movement records related to the object CollectionObject record " + csid);
249 // Get relation records in the reverse direction as well,
250 // merge with records obtained above, and remove duplicates
251 queryString = "rtSbj=" + csid; // FIXME: Get from constant
252 uri = new URI(null, null, null, queryString, null);
253 uriInfo = createUriInfo(uri.getRawQuery());
255 AbstractCommonList reverseRelatedMovements = movementResource.getList(uriInfo);
256 if (logger.isInfoEnabled()) {
257 logger.info("Identified " + reverseRelatedMovements.getTotalItems()
258 + " Movement records related to the subject CollectionObject record " + csid);
261 if ((relatedMovements.getTotalItems() == 0) && reverseRelatedMovements.getTotalItems() == 0) {
265 // Merge the two lists
266 relatedMovements.getListItem().addAll(reverseRelatedMovements.getListItem());
268 // Get the latest movement record from among those, and extract
269 // its current location value
270 computedCurrentLocation = "";
271 String currentLocation;
273 String mostRecentLocationDate = "";
274 for (AbstractCommonList.ListItem movementRecord : relatedMovements.getListItem()) {
276 // FIXME: Add 'de-duping' code here to avoid processing any
277 // related Movement record more than once
279 locationDate = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, LOCATION_DATE_ELEMENT_NAME);
280 if (Tools.isBlank(locationDate)) {
283 currentLocation = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CURRENT_LOCATION_ELEMENT_NAME);
284 if (Tools.isBlank(currentLocation)) {
287 if (logger.isInfoEnabled()) {
288 logger.info("Location date value = " + locationDate);
289 logger.info("Current location value = " + currentLocation);
291 // Assumes that all values for this element/field will be consistent ISO 8601
292 // date/time representations, each of which can be ordered via string comparison.
293 if (locationDate.compareTo(mostRecentLocationDate) > 0) {
294 mostRecentLocationDate = locationDate;
295 // FIXME: Add validation here that the currentLocation value parses successfully as an item refName
296 computedCurrentLocation = currentLocation;
301 // Update the computed current location value in the CollectionObject record
302 collectionObjectPayload = findByCsid(collectionObjectResource, csid);
303 if (Tools.notBlank(collectionObjectPayload.toXML())) {
304 if (logger.isInfoEnabled()) {
305 logger.info("Payload: " + "\n" + collectionObjectPayload);
307 // Silently fails at various places in dom4j calls (selectSingleNode, selectNode,
308 // createXpath) in any of the methods tried, without throwing an Exception.
310 // Those methods are now commented out, in favor of a replacement, however temporary,
313 // FIXME: Get namespace from constant; verify whether prefix or URI is required
314 objectNumber = getFieldValue(collectionObjectPayload,
315 COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
316 OBJECT_NUMBER_ELEMENT_NAME);
317 if (logger.isInfoEnabled()) {
318 logger.info("Object number: " + objectNumber);
320 if (Tools.notBlank(objectNumber)) {
321 String collectionObjectUpdatePayload =
322 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
323 + "<document name=\"collectionobject\">"
324 + "<ns2:collectionobjects_common xmlns:ns2=\"http://collectionspace.org/services/collectionobject\">"
325 + "<objectNumber>" + objectNumber + "</objectNumber>"
326 + "<computedCurrentLocation>" + computedCurrentLocation + "</computedCurrentLocation>"
327 + "</ns2:collectionobjects_common></document>";
328 if (logger.isInfoEnabled()) {
329 logger.info("Update payload: " + "\n" + collectionObjectUpdatePayload);
331 byte[] response = collectionObjectResource.update(resourcemap, null, csid, collectionObjectUpdatePayload);
333 if (logger.isTraceEnabled()) {
334 logger.trace("Computed current location value for CollectionObject " + csid + " set to " + computedCurrentLocation);
341 } catch (Exception e) {
342 String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage() + " ";
343 errMsg = errMsg + "Successfully updated " + numAffected + " CollectionObject record(s) prior to error.";
344 logger.error(errMsg);
345 setErrorResult(errMsg);
346 getResults().setNumAffected(numAffected);
350 getResults().setNumAffected(numAffected);