]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
fed02d2a77e87cc0513600ce2833633ff03ca799
[tmp/jakarta-migration.git] /
1 package org.collectionspace.services.batch.nuxeo;
2
3 import java.io.StringReader;
4 import java.net.URI;
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;
11 import java.util.Set;
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;
33
34 public class UpdateObjectLocationBatchJob extends AbstractBatchInvocable {
35
36     // FIXME: Where appropriate, get from existing constants rather than local declarations
37     private final static String CSID_ELEMENT_NAME = "csid";
38     private final static String CURRENT_LOCATION_ELEMENT_NAME = "currentLocation";
39     private final static String LIFECYCLE_STATE_ELEMENT_NAME = "currentLifeCycleState";
40     private final static String LOCATION_DATE_ELEMENT_NAME = "locationDate";
41     private final static String OBJECT_NUMBER_ELEMENT_NAME = "objectNumber";
42     private final static String WORKFLOW_COMMON_SCHEMA_NAME = "workflow_common";
43     private final static String WORKFLOW_COMMON_NAMESPACE_PREFIX = "ns2";
44     private final static String WORKFLOW_COMMON_NAMESPACE_URI =
45             "http://collectionspace.org/services/workflow";
46     private final static Namespace WORKFLOW_COMMON_NAMESPACE =
47             Namespace.getNamespace(
48             WORKFLOW_COMMON_NAMESPACE_PREFIX,
49             WORKFLOW_COMMON_NAMESPACE_URI);
50     private final static String COLLECTIONOBJECTS_COMMON_SCHEMA_NAME = "collectionobjects_common";
51     private final static String COLLECTIONOBJECTS_COMMON_NAMESPACE_PREFIX = "ns2";
52     private final static String COLLECTIONOBJECTS_COMMON_NAMESPACE_URI =
53             "http://collectionspace.org/services/collectionobject";
54     private final static Namespace COLLECTIONOBJECTS_COMMON_NAMESPACE =
55             Namespace.getNamespace(
56             COLLECTIONOBJECTS_COMMON_NAMESPACE_PREFIX,
57             COLLECTIONOBJECTS_COMMON_NAMESPACE_URI);
58     private final static String NONDELETED_QUERY_COMPONENT =
59             "&" + WorkflowClient.WORKFLOW_QUERY_NONDELETED + "=false";
60     private final String CLASSNAME = this.getClass().getSimpleName();
61     private final Logger logger = LoggerFactory.getLogger(this.getClass());
62
63     // Initialization tasks
64     public UpdateObjectLocationBatchJob() {
65         setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE, INVOCATION_MODE_LIST));
66     }
67
68     /**
69      * The main work logic of the batch job. Will be called after setContext.
70      */
71     @Override
72     public void run() {
73
74         setCompletionStatus(STATUS_MIN_PROGRESS);
75
76         try {
77
78             List<String> csids = new ArrayList<String>();
79
80             // Build a list of CollectionObject records to process via this
81             // batch job, depending on the invocation mode requested.
82             if (requestIsForInvocationModeSingle()) {
83                 String singleCsid = getInvocationContext().getSingleCSID();
84                 if (Tools.isBlank(singleCsid)) {
85                     throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
86                 } else {
87                     csids.add(singleCsid);
88                 }
89             } else if (requestIsForInvocationModeList()) {
90                 List<String> listCsids = getListCsids();
91                 if (listCsids.isEmpty()) {
92                     throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
93                 }
94                 csids.addAll(listCsids);
95             } else if (requestIsForInvocationModeGroup()) {
96                 // Currently not supported
97                 // FIXME: Get individual CSIDs, if any, from the group
98                 // String groupCsid = getInvocationContext().getGroupCSID();
99                 // ...
100             } else if (requestIsForInvocationModeNoContext()) {
101                 // Currently not supported
102                 // FIXME: Add code to invoke batch job on every active CollectionObject
103             }
104
105             // Update the value of the computed current location field for each CollectionObject
106             setResults(updateComputedCurrentLocations(csids));
107             setCompletionStatus(STATUS_COMPLETE);
108
109         } catch (Exception e) {
110             String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage();
111             setErrorResult(errMsg);
112         }
113
114     }
115
116     private InvocationResults updateComputedCurrentLocations(List<String> csids) {
117
118         ResourceMap resourcemap = getResourceMap();
119         ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
120         ResourceBase movementResource = resourcemap.get(MovementClient.SERVICE_NAME);
121         String computedCurrentLocation;
122         PoxPayloadOut payloadOut;
123         int numAffected = 0;
124
125         try {
126
127             // For each CollectionObject record
128             for (String collectionObjectCsid : csids) {
129
130                 // Skip over soft-deleted CollectionObject records
131                 if (isRecordDeleted(collectionObjectResource, collectionObjectCsid)) {
132                     if (logger.isTraceEnabled()) {
133                         logger.trace("Skipping soft-deleted CollectionObject record with CSID " + collectionObjectCsid);
134                     }
135                     continue;
136                 }
137
138                 // Get the Movement records related to this record
139                 AbstractCommonList relatedMovements = getRelatedMovements(movementResource, collectionObjectCsid);
140
141                 // Skip over CollectionObject records that have no related Movement records
142                 if (relatedMovements.getListItem().isEmpty()) {
143                     continue;
144                 }
145
146                 // Based on data in its related Movement records, compute the
147                 // current location of this CollectionObject
148                 computedCurrentLocation = computeCurrentLocation(relatedMovements);
149
150                 // Skip over CollectionObject records where no computed current
151                 // location value can be obtained from related Movement records.
152                 //
153                 // FIXME: Clarify: it ever necessary to 'unset' a computed
154                 // current location value, by setting it to a null or empty value,
155                 // if that value is no longer obtainable from related Movement records?
156                 if (Tools.isBlank(computedCurrentLocation)) {
157                     continue;
158                 }
159
160                 // Update the value of the computed current location field
161                 // in the CollectionObject record
162                 numAffected = updateComputedCurrentLocationValue(collectionObjectResource,
163                         collectionObjectCsid, computedCurrentLocation, resourcemap, numAffected);
164             }
165
166         } catch (Exception e) {
167             String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage() + " ";
168             errMsg = errMsg + "Successfully updated " + numAffected + " CollectionObject record(s) prior to error.";
169             logger.error(errMsg);
170             setErrorResult(errMsg);
171             getResults().setNumAffected(numAffected);
172             return getResults();
173         }
174
175         logger.info("Updated computedCurrentLocation values in " + numAffected + " CollectionObject record(s).");
176         getResults().setNumAffected(numAffected);
177         return getResults();
178     }
179
180     private AbstractCommonList getRelatedMovements(ResourceBase movementResource, String csid)
181             throws URISyntaxException, DocumentException {
182
183         // Get movement records related to a record, specified by its CSID,
184         // where the record is the object of the relation
185         // FIXME: Get query string(s) from constant(s), where appropriate
186         String queryString = "rtObj=" + csid + NONDELETED_QUERY_COMPONENT;
187         UriInfo uriInfo = createRelatedRecordsUriInfo(queryString);
188         AbstractCommonList relatedMovements = movementResource.getList(uriInfo);
189         if (logger.isTraceEnabled()) {
190             logger.trace("Identified " + relatedMovements.getTotalItems()
191                     + " Movement record(s) related to the object CollectionObject record " + csid);
192         }
193
194         // Get movement records related to a record, specified by its CSID,
195         // where the record is the subject of the relation
196         // FIXME: Get query string(s) from constant(s), where appropriate
197         queryString = "rtSbj=" + csid + NONDELETED_QUERY_COMPONENT;
198         uriInfo = createRelatedRecordsUriInfo(queryString);
199         AbstractCommonList reverseRelatedMovements = movementResource.getList(uriInfo);
200         if (logger.isTraceEnabled()) {
201             logger.trace("Identified " + reverseRelatedMovements.getTotalItems()
202                     + " Movement record(s) related to the subject CollectionObject record " + csid);
203         }
204
205         // If the second list contains any related movement records,
206         // merge it into the first list
207         if (reverseRelatedMovements.getListItem().size() > 0) {
208             relatedMovements.getListItem().addAll(reverseRelatedMovements.getListItem());
209         }
210
211         if (logger.isTraceEnabled()) {
212             logger.trace("Identified a total of " + relatedMovements.getListItem().size()
213                     + " Movement record(s) related to the subject CollectionObject record " + csid);
214         }
215
216         return relatedMovements;
217     }
218
219     private String computeCurrentLocation(AbstractCommonList relatedMovements) {
220         String computedCurrentLocation;
221         String movementCsid;
222         Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
223         computedCurrentLocation = "";
224         String currentLocation;
225         String locationDate;
226         String mostRecentLocationDate = "";
227         for (AbstractCommonList.ListItem movementRecord : relatedMovements.getListItem()) {
228             movementCsid = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CSID_ELEMENT_NAME);
229             if (Tools.isBlank(movementCsid)) {
230                 continue;
231             }
232             // Avoid processing any related Movement record more than once,
233             // regardless of the directionality of its relation(s) to this
234             // CollectionObject record.
235             if (alreadyProcessedMovementCsids.contains(movementCsid)) {
236                 continue;
237             } else {
238                 alreadyProcessedMovementCsids.add(movementCsid);
239             }
240             locationDate = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, LOCATION_DATE_ELEMENT_NAME);
241             if (Tools.isBlank(locationDate)) {
242                 continue;
243             }
244             currentLocation = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CURRENT_LOCATION_ELEMENT_NAME);
245             if (Tools.isBlank(currentLocation)) {
246                 continue;
247             }
248             if (logger.isTraceEnabled()) {
249                 logger.trace("Location date value = " + locationDate);
250                 logger.trace("Current location value = " + currentLocation);
251             }
252             // If this record's location date value is more recent than that of other
253             // Movement records processed so far, set the computed current location
254             // to its current location value.
255             //
256             // Assumes that all values for this element/field will be consistent ISO 8601
257             // date/time representations, each of which can be ordered via string comparison.
258             //
259             // If this is *not* the case, we can instead parse and convert these values
260             // to date/time objects.
261             if (locationDate.compareTo(mostRecentLocationDate) > 0) {
262                 mostRecentLocationDate = locationDate;
263                 // FIXME: Add optional validation here that the currentLocation value
264                 // parses successfully as an item refName. (We might make that validation
265                 // dependent on the value of a parameter passed in during batch job invocation.)
266                 computedCurrentLocation = currentLocation;
267             }
268
269         }
270         return computedCurrentLocation;
271     }
272
273     private int updateComputedCurrentLocationValue(ResourceBase collectionObjectResource,
274             String collectionObjectCsid, String computedCurrentLocation, ResourceMap resourcemap, int numAffected)
275             throws DocumentException, URISyntaxException {
276         PoxPayloadOut collectionObjectPayload;
277         String objectNumber;
278
279         collectionObjectPayload = findByCsid(collectionObjectResource, collectionObjectCsid);
280         if (Tools.notBlank(collectionObjectPayload.toXML())) {
281             if (logger.isTraceEnabled()) {
282                 logger.trace("Payload: " + "\n" + collectionObjectPayload);
283             }
284             objectNumber = getFieldElementValue(collectionObjectPayload,
285                     COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
286                     OBJECT_NUMBER_ELEMENT_NAME);
287             if (logger.isTraceEnabled()) {
288                 logger.trace("Object number: " + objectNumber);
289             }
290             if (Tools.notBlank(objectNumber)) {
291                 String collectionObjectUpdatePayload =
292                         "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
293                         + "<document name=\"collectionobject\">"
294                         + "  <ns2:collectionobjects_common "
295                         + "      xmlns:ns2=\"http://collectionspace.org/services/collectionobject\">"
296                         + "    <objectNumber>" + objectNumber + "</objectNumber>"
297                         + "    <computedCurrentLocation>" + computedCurrentLocation + "</computedCurrentLocation>"
298                         + "  </ns2:collectionobjects_common>"
299                         + "</document>";
300                 if (logger.isTraceEnabled()) {
301                     logger.trace("Update payload: " + "\n" + collectionObjectUpdatePayload);
302                 }
303                 byte[] response = collectionObjectResource.update(resourcemap, null, collectionObjectCsid,
304                         collectionObjectUpdatePayload);
305                 numAffected++;
306                 if (logger.isTraceEnabled()) {
307                     logger.trace("Computed current location value for CollectionObject " + collectionObjectCsid
308                             + " was set to " + computedCurrentLocation);
309                 }
310             }
311
312         }
313         return numAffected;
314     }
315
316     // #################################################################
317     // Ray'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);
323     }
324
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);
328         return payload;
329     }
330
331     protected UriInfo createUriInfo() throws URISyntaxException {
332         return createUriInfo("");
333     }
334
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());
339     }
340
341     // #################################################################
342     // Other convenience methods
343     // #################################################################
344     protected UriInfo createRelatedRecordsUriInfo(String query) throws URISyntaxException {
345         URI uri = new URI(null, null, null, query, null);
346         return createUriInfo(uri.getRawQuery());
347     }
348
349     protected String getFieldElementValue(PoxPayloadOut payload, String partLabel, Namespace partNamespace, String fieldPath) {
350         String value = null;
351         SAXBuilder builder = new SAXBuilder();
352         try {
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);
362             if (field == null) {
363                 field = part.getChild(fieldPath);
364             }
365             if (field != null) {
366                 value = field.getText();
367             }
368         } catch (Exception e) {
369             logger.error("Error getting value from field path " + fieldPath
370                     + " in schema part " + partLabel);
371             return null;
372         }
373         return value;
374     }
375
376     private boolean isRecordDeleted(ResourceBase resource, String collectionObjectCsid) throws URISyntaxException, DocumentException {
377         boolean isDeleted = false;
378         byte[] workflowResponse = resource.getWorkflow(createUriInfo(), collectionObjectCsid);
379         if (workflowResponse != null) {
380             PoxPayloadOut payloadOut = new PoxPayloadOut(workflowResponse);
381             String workflowState =
382                     getFieldElementValue(payloadOut, WORKFLOW_COMMON_SCHEMA_NAME,
383                     WORKFLOW_COMMON_NAMESPACE, LIFECYCLE_STATE_ELEMENT_NAME);
384             if (Tools.notBlank(workflowState) && workflowState.contentEquals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
385                 isDeleted = true;
386             }
387         }
388         return isDeleted;
389     }
390 }