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 COMPUTED_CURRENT_LOCATION_ELEMENT_NAME = "computedCurrentLocation";
38 private final static String CSID_ELEMENT_NAME = "csid";
39 private final static String CURRENT_LOCATION_ELEMENT_NAME = "currentLocation";
40 private final static String LIFECYCLE_STATE_ELEMENT_NAME = "currentLifeCycleState";
41 private final static String LOCATION_DATE_ELEMENT_NAME = "locationDate";
42 private final static String OBJECT_NUMBER_ELEMENT_NAME = "objectNumber";
43 private final static String WORKFLOW_COMMON_SCHEMA_NAME = "workflow_common";
44 private final static String WORKFLOW_COMMON_NAMESPACE_PREFIX = "ns2";
45 private final static String WORKFLOW_COMMON_NAMESPACE_URI =
46 "http://collectionspace.org/services/workflow";
47 private final static Namespace WORKFLOW_COMMON_NAMESPACE =
48 Namespace.getNamespace(
49 WORKFLOW_COMMON_NAMESPACE_PREFIX,
50 WORKFLOW_COMMON_NAMESPACE_URI);
51 private final static String COLLECTIONOBJECTS_COMMON_SCHEMA_NAME = "collectionobjects_common";
52 private final static String COLLECTIONOBJECTS_COMMON_NAMESPACE_PREFIX = "ns2";
53 private final static String COLLECTIONOBJECTS_COMMON_NAMESPACE_URI =
54 "http://collectionspace.org/services/collectionobject";
55 private final static Namespace COLLECTIONOBJECTS_COMMON_NAMESPACE =
56 Namespace.getNamespace(
57 COLLECTIONOBJECTS_COMMON_NAMESPACE_PREFIX,
58 COLLECTIONOBJECTS_COMMON_NAMESPACE_URI);
59 private final boolean EXCLUDE_DELETED = true;
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,
66 INVOCATION_MODE_GROUP, INVOCATION_MODE_NO_CONTEXT));
70 * The main work logic of the batch job. Will be called after setContext.
75 setCompletionStatus(STATUS_MIN_PROGRESS);
79 List<String> csids = new ArrayList<String>();
81 // Build a list of CollectionObject records to process via this
82 // batch job, depending on the invocation mode requested.
83 if (requestIsForInvocationModeSingle()) {
84 String singleCsid = getInvocationContext().getSingleCSID();
85 if (Tools.isBlank(singleCsid)) {
86 throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
88 csids.add(singleCsid);
90 } else if (requestIsForInvocationModeList()) {
91 List<String> listCsids = getListCsids();
92 if (listCsids.isEmpty()) {
93 throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
95 csids.addAll(listCsids);
96 } else if (requestIsForInvocationModeGroup()) {
97 String groupCsid = getInvocationContext().getGroupCSID();
98 if (Tools.isBlank(groupCsid)) {
99 throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
101 List<String> groupMemberCsids = getMemberCsidsFromGroup(CollectionObjectClient.SERVICE_NAME, groupCsid);
102 if (groupMemberCsids.isEmpty()) {
103 throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
105 csids.addAll(groupMemberCsids);
106 } else if (requestIsForInvocationModeNoContext()) {
107 List<String> noContextCsids = getNoContextCsids();
108 if (noContextCsids.isEmpty()) {
109 throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
111 csids.addAll(noContextCsids);
113 if (logger.isInfoEnabled()) {
114 logger.info("Identified " + csids.size() + " total CollectionObject(s) to be processed via the " + CLASSNAME + " batch job");
117 // Update the value of the computed current location field for each CollectionObject
118 setResults(updateComputedCurrentLocations(csids));
119 setCompletionStatus(STATUS_COMPLETE);
121 } catch (Exception e) {
122 String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage();
123 setErrorResult(errMsg);
128 private InvocationResults updateComputedCurrentLocations(List<String> csids) {
129 ResourceMap resourcemap = getResourceMap();
130 ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
131 ResourceBase movementResource = resourcemap.get(MovementClient.SERVICE_NAME);
132 String computedCurrentLocation;
137 // For each CollectionObject record
138 for (String collectionObjectCsid : csids) {
140 // FIXME: Optionally set competition status here to
141 // indicate what percentage of records have been processed.
143 // Skip over soft-deleted CollectionObject records
145 // (Invocations using the 'no context' mode have already
146 // filtered out soft-deleted records.)
147 if (!requestIsForInvocationModeNoContext()) {
148 if (isRecordDeleted(collectionObjectResource, collectionObjectCsid)) {
149 if (logger.isTraceEnabled()) {
150 logger.trace("Skipping soft-deleted CollectionObject record with CSID " + collectionObjectCsid);
155 // Get the Movement records related to this CollectionObject record
156 AbstractCommonList relatedMovements =
157 getRelatedRecords(movementResource, collectionObjectCsid, EXCLUDE_DELETED);
158 // Skip over CollectionObject records that have no related Movement records
159 if (relatedMovements.getListItem().isEmpty()) {
162 // Compute the current location of this CollectionObject,
163 // based on data in its related Movement records
164 computedCurrentLocation = computeCurrentLocation(relatedMovements);
165 // Skip over CollectionObject records where no current location
166 // value can be computed
168 // FIXME: Clarify: it ever necessary to 'unset' a computed
169 // current location value, by setting it to a null or empty value,
170 // if that value is no longer obtainable from related Movement records?
171 if (Tools.isBlank(computedCurrentLocation)) {
174 // Update the value of the computed current location field
175 // in the CollectionObject record
176 numUpdated = updateComputedCurrentLocationValue(collectionObjectResource,
177 collectionObjectCsid, computedCurrentLocation, resourcemap, numUpdated);
180 } catch (Exception e) {
181 String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage() + " ";
182 errMsg = errMsg + "Successfully updated " + numUpdated + " CollectionObject record(s) prior to error.";
183 logger.error(errMsg);
184 setErrorResult(errMsg);
185 getResults().setNumAffected(numUpdated);
189 logger.info("Updated computedCurrentLocation values in " + numUpdated + " CollectionObject record(s).");
190 getResults().setNumAffected(numUpdated);
194 private String computeCurrentLocation(AbstractCommonList relatedMovements) {
195 Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
196 String computedCurrentLocation;
198 computedCurrentLocation = "";
199 String currentLocation;
201 String mostRecentLocationDate = "";
202 for (AbstractCommonList.ListItem movementRecord : relatedMovements.getListItem()) {
203 movementCsid = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CSID_ELEMENT_NAME);
204 if (Tools.isBlank(movementCsid)) {
207 // Skip over any duplicates in the list, such as records that might
208 // appear as the subject of one relation record and the object of
209 // its reciprocal relation record
210 if (alreadyProcessedMovementCsids.contains(movementCsid)) {
213 alreadyProcessedMovementCsids.add(movementCsid);
215 locationDate = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, LOCATION_DATE_ELEMENT_NAME);
216 if (Tools.isBlank(locationDate)) {
219 currentLocation = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CURRENT_LOCATION_ELEMENT_NAME);
220 if (Tools.isBlank(currentLocation)) {
223 if (logger.isTraceEnabled()) {
224 logger.trace("Location date value = " + locationDate);
225 logger.trace("Current location value = " + currentLocation);
227 // If this record's location date value is more recent than that of other
228 // Movement records processed so far, set the computed current location
229 // to its current location value.
231 // The following comparison assumes that all values for this element/field
232 // will be consistent ISO 8601 date/time representations, each of which can
233 // be ordered via string comparison.
235 // If this is *not* the case, we should instead parse and convert these values
236 // to date/time objects.
237 if (locationDate.compareTo(mostRecentLocationDate) > 0) {
238 mostRecentLocationDate = locationDate;
239 // FIXME: Add optional validation here that the currentLocation value
240 // parses successfully as an item refName.
241 // Consider making this optional validation, in turn dependent on the
242 // value of a parameter passed in during batch job invocation.
243 computedCurrentLocation = currentLocation;
247 return computedCurrentLocation;
250 private int updateComputedCurrentLocationValue(ResourceBase collectionObjectResource,
251 String collectionObjectCsid, String computedCurrentLocation, ResourceMap resourcemap, int numUpdated)
252 throws DocumentException, URISyntaxException {
253 PoxPayloadOut collectionObjectPayload;
255 String previousComputedCurrentLocation;
257 collectionObjectPayload = findByCsid(collectionObjectResource, collectionObjectCsid);
258 if (Tools.isBlank(collectionObjectPayload.toXML())) {
261 if (logger.isTraceEnabled()) {
262 logger.trace("Payload: " + "\n" + collectionObjectPayload);
265 // Perform the update only if the computed current location value will change
266 // as a result of the update
267 previousComputedCurrentLocation = getFieldElementValue(collectionObjectPayload,
268 COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
269 COMPUTED_CURRENT_LOCATION_ELEMENT_NAME);
270 if (Tools.notBlank(previousComputedCurrentLocation)
271 && computedCurrentLocation.equals(previousComputedCurrentLocation)) {
274 // Perform the update only if there is a non-blank object number available.
276 // In the default CollectionObject validation handler, the object number
277 // is a required field and its (non-blank) value must be present in update
278 // payloads to successfully perform an update.
279 objectNumber = getFieldElementValue(collectionObjectPayload,
280 COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
281 OBJECT_NUMBER_ELEMENT_NAME);
282 if (logger.isTraceEnabled()) {
283 logger.trace("Object number: " + objectNumber);
285 // FIXME: Consider making the requirement that a non-blank object number
286 // be present dependent on the value of a parameter passed in during
287 // batch job invocation, as some implementations may have turned off that
288 // validation requirement.
289 if (Tools.isBlank(objectNumber)) {
293 String collectionObjectUpdatePayload =
294 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
295 + "<document name=\"collectionobject\">"
296 + " <ns2:collectionobjects_common "
297 + " xmlns:ns2=\"http://collectionspace.org/services/collectionobject\">"
298 + " <objectNumber>" + objectNumber + "</objectNumber>"
299 + " <computedCurrentLocation>" + computedCurrentLocation + "</computedCurrentLocation>"
300 + " </ns2:collectionobjects_common>"
302 if (logger.isTraceEnabled()) {
303 logger.trace("Update payload: " + "\n" + collectionObjectUpdatePayload);
305 byte[] response = collectionObjectResource.update(resourcemap, null, collectionObjectCsid,
306 collectionObjectUpdatePayload);
308 if (logger.isTraceEnabled()) {
309 logger.trace("Computed current location value for CollectionObject " + collectionObjectCsid
310 + " was set to " + computedCurrentLocation);
316 // #################################################################
317 // Ray Lee'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 queryString) throws URISyntaxException {
345 URI uri = new URI(null, null, null, queryString, 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)
377 throws URISyntaxException, DocumentException {
378 boolean isDeleted = false;
379 byte[] workflowResponse = resource.getWorkflow(createUriInfo(), collectionObjectCsid);
380 if (workflowResponse != null) {
381 PoxPayloadOut payloadOut = new PoxPayloadOut(workflowResponse);
382 String workflowState =
383 getFieldElementValue(payloadOut, WORKFLOW_COMMON_SCHEMA_NAME,
384 WORKFLOW_COMMON_NAMESPACE, LIFECYCLE_STATE_ELEMENT_NAME);
385 if (Tools.notBlank(workflowState) && workflowState.equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
392 private UriInfo addFilterToExcludeSoftDeletedRecords(UriInfo uriInfo) throws URISyntaxException {
393 if (uriInfo == null) {
394 uriInfo = createUriInfo();
396 uriInfo.getQueryParameters().add(WorkflowClient.WORKFLOW_QUERY_NONDELETED, Boolean.FALSE.toString());
400 private AbstractCommonList getRecordsRelatedToCsid(ResourceBase resource, String csid,
401 String relationshipDirection, boolean excludeDeletedRecords) throws URISyntaxException {
402 UriInfo uriInfo = createUriInfo();
403 uriInfo.getQueryParameters().add(relationshipDirection, csid);
404 if (excludeDeletedRecords) {
405 uriInfo = addFilterToExcludeSoftDeletedRecords(uriInfo);
407 // The 'resource' type used here identifies the record type of the
408 // related records to be retrieved
409 AbstractCommonList relatedRecords = resource.getList(uriInfo);
410 if (logger.isTraceEnabled()) {
411 logger.trace("Identified " + relatedRecords.getTotalItems()
412 + " record(s) related to the object record via direction " + relationshipDirection + " with CSID " + csid);
414 return relatedRecords;
418 * Returns the records of a specified type that are related to a specified
419 * record, where that record is the object of the relation.
421 * @param resource a resource. The type of this resource determines the type
422 * of related records that are returned.
423 * @param csid a CSID identifying a record
424 * @param excludeDeletedRecords true if 'soft-deleted' records should be
425 * excluded from results; false if those records should be included
426 * @return a list of records of a specified type, related to a specified
428 * @throws URISyntaxException
430 private AbstractCommonList getRecordsRelatedToObjectCsid(ResourceBase resource, String csid, boolean excludeDeletedRecords) throws URISyntaxException {
431 return getRecordsRelatedToCsid(resource, csid, "rtObj", excludeDeletedRecords);
435 * Returns the records of a specified type that are related to a specified
436 * record, where that record is the subject of the relation.
438 * @param resource a resource. The type of this resource determines the type
439 * of related records that are returned.
440 * @param csid a CSID identifying a record
441 * @param excludeDeletedRecords true if 'soft-deleted' records should be
442 * excluded from results; false if those records should be included
443 * @return a list of records of a specified type, related to a specified
445 * @throws URISyntaxException
447 private AbstractCommonList getRecordsRelatedToSubjectCsid(ResourceBase resource, String csid, boolean excludeDeletedRecords) throws URISyntaxException {
448 return getRecordsRelatedToCsid(resource, csid, "rtSbj", excludeDeletedRecords);
451 private AbstractCommonList getRelatedRecords(ResourceBase resource, String csid, boolean excludeDeletedRecords)
452 throws URISyntaxException, DocumentException {
453 AbstractCommonList relatedRecords = new AbstractCommonList();
454 AbstractCommonList recordsRelatedToObjectCSID = getRecordsRelatedToObjectCsid(resource, csid, excludeDeletedRecords);
455 AbstractCommonList recordsRelatedToSubjectCSID = getRecordsRelatedToSubjectCsid(resource, csid, excludeDeletedRecords);
456 // If either list contains any related records, merge in its items
457 if (recordsRelatedToObjectCSID.getListItem().size() > 0) {
458 relatedRecords.getListItem().addAll(recordsRelatedToObjectCSID.getListItem());
460 if (recordsRelatedToSubjectCSID.getListItem().size() > 0) {
461 relatedRecords.getListItem().addAll(recordsRelatedToSubjectCSID.getListItem());
463 if (logger.isTraceEnabled()) {
464 logger.trace("Identified a total of " + relatedRecords.getListItem().size()
465 + " record(s) related to the record with CSID " + csid);
467 return relatedRecords;
470 private List<String> getCsidsList(AbstractCommonList list) {
471 List<String> csids = new ArrayList<String>();
472 for (AbstractCommonList.ListItem listitem : list.getListItem()) {
473 csids.add(AbstractCommonListUtils.ListItemGetCSID(listitem));
478 private List<String> getMemberCsidsFromGroup(String serviceName, String groupCsid) throws URISyntaxException, DocumentException {
479 ResourceMap resourcemap = getResourceMap();
480 ResourceBase resource = resourcemap.get(serviceName);
481 return getMemberCsidsFromGroup(resource, groupCsid);
484 private List<String> getMemberCsidsFromGroup(ResourceBase resource, String groupCsid) throws URISyntaxException, DocumentException {
485 // The 'resource' type used here identifies the record type of the
486 // related records to be retrieved
487 AbstractCommonList relatedRecords =
488 getRelatedRecords(resource, groupCsid, EXCLUDE_DELETED);
489 List<String> memberCsids = getCsidsList(relatedRecords);
493 private List<String> getNoContextCsids() throws URISyntaxException {
494 ResourceMap resourcemap = getResourceMap();
495 ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
496 UriInfo uriInfo = createUriInfo();
497 uriInfo = addFilterToExcludeSoftDeletedRecords(uriInfo);
498 AbstractCommonList collectionObjects = collectionObjectResource.getList(uriInfo);
499 List<String> noContextCsids = getCsidsList(collectionObjects);
500 return noContextCsids;