]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
5e5b287f6b97c8a9e55a66ddb7d137c4461a7a25
[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 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());
62
63     // Initialization tasks
64     public UpdateObjectLocationBatchJob() {
65         setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE, INVOCATION_MODE_LIST,
66                 INVOCATION_MODE_GROUP, INVOCATION_MODE_NO_CONTEXT));
67     }
68
69     /**
70      * The main work logic of the batch job. Will be called after setContext.
71      */
72     @Override
73     public void run() {
74
75         setCompletionStatus(STATUS_MIN_PROGRESS);
76
77         try {
78
79             List<String> csids = new ArrayList<String>();
80
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);
87                 } else {
88                     csids.add(singleCsid);
89                 }
90             } else if (requestIsForInvocationModeList()) {
91                 List<String> listCsids = getListCsids();
92                 if (listCsids.isEmpty()) {
93                     throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
94                 }
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);
100                 }
101                 List<String> groupMemberCsids = getMemberCsidsFromGroup(CollectionObjectClient.SERVICE_NAME, groupCsid);
102                 if (groupMemberCsids.isEmpty()) {
103                     throw new Exception(CSID_VALUES_NOT_PROVIDED_IN_INVOCATION_CONTEXT);
104                 }
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);
110                 }
111                 csids.addAll(noContextCsids);
112             }
113             if (logger.isInfoEnabled()) {
114                 logger.info("Identified " + csids.size() + " total CollectionObject(s) to be processed via the " + CLASSNAME + " batch job");
115             }
116
117             // Update the value of the computed current location field for each CollectionObject
118             setResults(updateComputedCurrentLocations(csids));
119             setCompletionStatus(STATUS_COMPLETE);
120
121         } catch (Exception e) {
122             String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage();
123             setErrorResult(errMsg);
124         }
125
126     }
127
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;
133         int numUpdated = 0;
134
135         try {
136
137             // For each CollectionObject record
138             for (String collectionObjectCsid : csids) {
139
140                 // FIXME: Optionally set competition status here to
141                 // indicate what percentage of records have been processed.
142
143                 // Skip over soft-deleted CollectionObject records
144                 //
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);
151                         }
152                         continue;
153                     }
154                 }
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()) {
160                     continue;
161                 }
162                 // Get the most recent 'suitable' Movement record, one which
163                 // contains both a location date and a current location value
164                 AbstractCommonList.ListItem mostRecentMovement = getMostRecentMovement(relatedMovements);
165                 // Skip over CollectionObject records where no suitable
166                 // most recent Movement record can be identified.
167                 //
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
171                 // records, if any?
172                 if (mostRecentMovement == null) {
173                     continue;
174                 }
175                 // Update the value of the computed current location field
176                 // (and, via subclasses, this and/or other relevant fields)
177                 // in the CollectionObject record
178                 numUpdated = updateCollectionObjectValues(collectionObjectResource,
179                         collectionObjectCsid, mostRecentMovement, resourcemap, numUpdated);
180             }
181
182         } catch (Exception e) {
183             String errMsg = "Error encountered in " + CLASSNAME + ": " + e.getLocalizedMessage() + " ";
184             errMsg = errMsg + "Successfully updated " + numUpdated + " CollectionObject record(s) prior to error.";
185             logger.error(errMsg);
186             setErrorResult(errMsg);
187             getResults().setNumAffected(numUpdated);
188             return getResults();
189         }
190
191         logger.info("Updated computedCurrentLocation values in " + numUpdated + " CollectionObject record(s).");
192         getResults().setNumAffected(numUpdated);
193         return getResults();
194     }
195
196     private AbstractCommonList.ListItem getMostRecentMovement(AbstractCommonList relatedMovements) {
197         Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
198         AbstractCommonList.ListItem mostRecentMovement = null;
199         String movementCsid;
200         String currentLocation;
201         String locationDate;
202         String mostRecentLocationDate = "";
203         for (AbstractCommonList.ListItem movementListItem : relatedMovements.getListItem()) {
204             movementCsid = AbstractCommonListUtils.ListItemGetElementValue(movementListItem, CSID_ELEMENT_NAME);
205             if (Tools.isBlank(movementCsid)) {
206                 continue;
207             }
208             // Skip over any duplicates in the list, such as records that might
209             // appear as the subject of one relation record and the object of
210             // its reciprocal relation record
211             if (alreadyProcessedMovementCsids.contains(movementCsid)) {
212                 continue;
213             } else {
214                 alreadyProcessedMovementCsids.add(movementCsid);
215             }
216             locationDate = AbstractCommonListUtils.ListItemGetElementValue(movementListItem, LOCATION_DATE_ELEMENT_NAME);
217             if (Tools.isBlank(locationDate)) {
218                 continue;
219             }
220             currentLocation = AbstractCommonListUtils.ListItemGetElementValue(movementListItem, CURRENT_LOCATION_ELEMENT_NAME);
221             if (Tools.isBlank(currentLocation)) {
222                 continue;
223             }
224             // FIXME: Add optional validation here that this Movement record's
225             // currentLocation value parses successfully as an item refName,
226             // before identifying that record as the most recent Movement.
227             // Consider making this optional validation, in turn dependent on the
228             // value of a parameter passed in during batch job invocation.
229             if (logger.isTraceEnabled()) {
230                 logger.trace("Location date value = " + locationDate);
231                 logger.trace("Current location value = " + currentLocation);
232             }
233             // If this record's location date value is more recent than that of other
234             // Movement records processed so far, set the current Movement record
235             // as the most recent Movement.
236             //
237             // The following comparison assumes that all values for this element/field
238             // will be consistent ISO 8601 date/time representations, each of which can
239             // be ordered via string comparison.
240             //
241             // If this is *not* the case, we should instead parse and convert these values
242             // to date/time objects.
243             if (locationDate.compareTo(mostRecentLocationDate) > 0) {
244                 mostRecentLocationDate = locationDate;
245                 mostRecentMovement = movementListItem;
246             }
247
248         }
249         return mostRecentMovement;
250     }
251
252     // This method can be overridden and extended to update a custom set of
253     // values in the CollectionObject record by pulling in values from its
254     // most recent related Movement record.
255     //
256     // Note: any such values must first be exposed in Movement list items,
257     // in turn via configuration in Services tenant bindings ("listResultsField").
258     protected int updateCollectionObjectValues(ResourceBase collectionObjectResource,
259             String collectionObjectCsid, AbstractCommonList.ListItem mostRecentMovement,
260             ResourceMap resourcemap, int numUpdated)
261             throws DocumentException, URISyntaxException {
262         PoxPayloadOut collectionObjectPayload;
263         String computedCurrentLocation;
264         String objectNumber;
265         String previousComputedCurrentLocation;
266
267         collectionObjectPayload = findByCsid(collectionObjectResource, collectionObjectCsid);
268         if (Tools.isBlank(collectionObjectPayload.toXML())) {
269             return numUpdated;
270         } else {
271             if (logger.isTraceEnabled()) {
272                 logger.trace("Payload: " + "\n" + collectionObjectPayload);
273             }
274         }
275         // Perform the update only if the computed current location value will change
276         // as a result of the update
277         computedCurrentLocation =
278                 AbstractCommonListUtils.ListItemGetElementValue(mostRecentMovement, CURRENT_LOCATION_ELEMENT_NAME);
279         previousComputedCurrentLocation = getFieldElementValue(collectionObjectPayload,
280                 COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
281                 COMPUTED_CURRENT_LOCATION_ELEMENT_NAME);
282         if (Tools.notBlank(previousComputedCurrentLocation)
283                 && computedCurrentLocation.equals(previousComputedCurrentLocation)) {
284             return numUpdated;
285         }
286         // Perform the update only if there is a non-blank object number available.
287         //
288         // In the default CollectionObject validation handler, the object number
289         // is a required field and its (non-blank) value must be present in update
290         // payloads to successfully perform an update.
291         objectNumber = getFieldElementValue(collectionObjectPayload,
292                 COLLECTIONOBJECTS_COMMON_SCHEMA_NAME, COLLECTIONOBJECTS_COMMON_NAMESPACE,
293                 OBJECT_NUMBER_ELEMENT_NAME);
294         if (logger.isTraceEnabled()) {
295             logger.trace("Object number: " + objectNumber);
296         }
297         // FIXME: Consider making the requirement that a non-blank object number
298         // be present dependent on the value of a parameter passed in during
299         // batch job invocation, as some implementations may have turned off that
300         // validation requirement.
301         if (Tools.isBlank(objectNumber)) {
302             return numUpdated;
303         }
304
305         String collectionObjectUpdatePayload =
306                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
307                 + "<document name=\"collectionobject\">"
308                 + "  <ns2:collectionobjects_common "
309                 + "      xmlns:ns2=\"http://collectionspace.org/services/collectionobject\">"
310                 + "    <objectNumber>" + objectNumber + "</objectNumber>"
311                 + "    <computedCurrentLocation>" + computedCurrentLocation + "</computedCurrentLocation>"
312                 + "  </ns2:collectionobjects_common>"
313                 + "</document>";
314         if (logger.isTraceEnabled()) {
315             logger.trace("Update payload: " + "\n" + collectionObjectUpdatePayload);
316         }
317         byte[] response = collectionObjectResource.update(resourcemap, null, collectionObjectCsid,
318                 collectionObjectUpdatePayload);
319         numUpdated++;
320         if (logger.isTraceEnabled()) {
321             logger.trace("Computed current location value for CollectionObject " + collectionObjectCsid
322                     + " was set to " + computedCurrentLocation);
323
324         }
325         return numUpdated;
326     }
327
328     // #################################################################
329     // Ray Lee's convenience methods from his AbstractBatchJob class for the
330     // UC Berkeley Botanical Garden v2.4 implementation.
331     // #################################################################
332     protected PoxPayloadOut findByCsid(String serviceName, String csid) throws URISyntaxException, DocumentException {
333         ResourceBase resource = getResourceMap().get(serviceName);
334         return findByCsid(resource, csid);
335     }
336
337     protected PoxPayloadOut findByCsid(ResourceBase resource, String csid) throws URISyntaxException, DocumentException {
338         byte[] response = resource.get(null, createUriInfo(), csid);
339         PoxPayloadOut payload = new PoxPayloadOut(response);
340         return payload;
341     }
342
343     protected UriInfo createUriInfo() throws URISyntaxException {
344         return createUriInfo("");
345     }
346
347     protected UriInfo createUriInfo(String queryString) throws URISyntaxException {
348         URI absolutePath = new URI("");
349         URI baseUri = new URI("");
350         return new UriInfoImpl(absolutePath, baseUri, "", queryString, Collections.<PathSegment>emptyList());
351     }
352
353     // #################################################################
354     // Other convenience methods
355     // #################################################################
356     protected UriInfo createRelatedRecordsUriInfo(String queryString) throws URISyntaxException {
357         URI uri = new URI(null, null, null, queryString, null);
358         return createUriInfo(uri.getRawQuery());
359     }
360
361     protected String getFieldElementValue(PoxPayloadOut payload, String partLabel, Namespace partNamespace, String fieldPath) {
362         String value = null;
363         SAXBuilder builder = new SAXBuilder();
364         try {
365             Document document = builder.build(new StringReader(payload.toXML()));
366             Element root = document.getRootElement();
367             // The part element is always expected to have an explicit namespace.
368             Element part = root.getChild(partLabel, partNamespace);
369             // Try getting the field element both with and without a namespace.
370             // Even though a field element that lacks a namespace prefix
371             // may yet inherit its namespace from a parent, JDOM may require that
372             // the getChild() call be made without a namespace.
373             Element field = part.getChild(fieldPath, partNamespace);
374             if (field == null) {
375                 field = part.getChild(fieldPath);
376             }
377             if (field != null) {
378                 value = field.getText();
379             }
380         } catch (Exception e) {
381             logger.error("Error getting value from field path " + fieldPath
382                     + " in schema part " + partLabel);
383             return null;
384         }
385         return value;
386     }
387
388     private boolean isRecordDeleted(ResourceBase resource, String collectionObjectCsid)
389             throws URISyntaxException, DocumentException {
390         boolean isDeleted = false;
391         byte[] workflowResponse = resource.getWorkflow(createUriInfo(), collectionObjectCsid);
392         if (workflowResponse != null) {
393             PoxPayloadOut payloadOut = new PoxPayloadOut(workflowResponse);
394             String workflowState =
395                     getFieldElementValue(payloadOut, WORKFLOW_COMMON_SCHEMA_NAME,
396                     WORKFLOW_COMMON_NAMESPACE, LIFECYCLE_STATE_ELEMENT_NAME);
397             if (Tools.notBlank(workflowState) && workflowState.equals(WorkflowClient.WORKFLOWSTATE_DELETED)) {
398                 isDeleted = true;
399             }
400         }
401         return isDeleted;
402     }
403
404     private UriInfo addFilterToExcludeSoftDeletedRecords(UriInfo uriInfo) throws URISyntaxException {
405         if (uriInfo == null) {
406             uriInfo = createUriInfo();
407         }
408         uriInfo.getQueryParameters().add(WorkflowClient.WORKFLOW_QUERY_NONDELETED, Boolean.FALSE.toString());
409         return uriInfo;
410     }
411
412     private AbstractCommonList getRecordsRelatedToCsid(ResourceBase resource, String csid,
413             String relationshipDirection, boolean excludeDeletedRecords) throws URISyntaxException {
414         UriInfo uriInfo = createUriInfo();
415         uriInfo.getQueryParameters().add(relationshipDirection, csid);
416         if (excludeDeletedRecords) {
417             uriInfo = addFilterToExcludeSoftDeletedRecords(uriInfo);
418         }
419         // The 'resource' type used here identifies the record type of the
420         // related records to be retrieved
421         AbstractCommonList relatedRecords = resource.getList(uriInfo);
422         if (logger.isTraceEnabled()) {
423             logger.trace("Identified " + relatedRecords.getTotalItems()
424                     + " record(s) related to the object record via direction " + relationshipDirection + " with CSID " + csid);
425         }
426         return relatedRecords;
427     }
428
429     /**
430      * Returns the records of a specified type that are related to a specified
431      * record, where that record is the object of the relation.
432      *
433      * @param resource a resource. The type of this resource determines the type
434      * of related records that are returned.
435      * @param csid a CSID identifying a record
436      * @param excludeDeletedRecords true if 'soft-deleted' records should be
437      * excluded from results; false if those records should be included
438      * @return a list of records of a specified type, related to a specified
439      * record
440      * @throws URISyntaxException
441      */
442     private AbstractCommonList getRecordsRelatedToObjectCsid(ResourceBase resource, String csid, boolean excludeDeletedRecords) throws URISyntaxException {
443         return getRecordsRelatedToCsid(resource, csid, "rtObj", excludeDeletedRecords);
444     }
445
446     /**
447      * Returns the records of a specified type that are related to a specified
448      * record, where that record is the subject of the relation.
449      *
450      * @param resource a resource. The type of this resource determines the type
451      * of related records that are returned.
452      * @param csid a CSID identifying a record
453      * @param excludeDeletedRecords true if 'soft-deleted' records should be
454      * excluded from results; false if those records should be included
455      * @return a list of records of a specified type, related to a specified
456      * record
457      * @throws URISyntaxException
458      */
459     private AbstractCommonList getRecordsRelatedToSubjectCsid(ResourceBase resource, String csid, boolean excludeDeletedRecords) throws URISyntaxException {
460         return getRecordsRelatedToCsid(resource, csid, "rtSbj", excludeDeletedRecords);
461     }
462
463     private AbstractCommonList getRelatedRecords(ResourceBase resource, String csid, boolean excludeDeletedRecords)
464             throws URISyntaxException, DocumentException {
465         AbstractCommonList relatedRecords = new AbstractCommonList();
466         AbstractCommonList recordsRelatedToObjectCSID = getRecordsRelatedToObjectCsid(resource, csid, excludeDeletedRecords);
467         AbstractCommonList recordsRelatedToSubjectCSID = getRecordsRelatedToSubjectCsid(resource, csid, excludeDeletedRecords);
468         // If either list contains any related records, merge in its items
469         if (recordsRelatedToObjectCSID.getListItem().size() > 0) {
470             relatedRecords.getListItem().addAll(recordsRelatedToObjectCSID.getListItem());
471         }
472         if (recordsRelatedToSubjectCSID.getListItem().size() > 0) {
473             relatedRecords.getListItem().addAll(recordsRelatedToSubjectCSID.getListItem());
474         }
475         if (logger.isTraceEnabled()) {
476             logger.trace("Identified a total of " + relatedRecords.getListItem().size()
477                     + " record(s) related to the record with CSID " + csid);
478         }
479         return relatedRecords;
480     }
481
482     private List<String> getCsidsList(AbstractCommonList list) {
483         List<String> csids = new ArrayList<String>();
484         for (AbstractCommonList.ListItem listitem : list.getListItem()) {
485             csids.add(AbstractCommonListUtils.ListItemGetCSID(listitem));
486         }
487         return csids;
488     }
489
490     private List<String> getMemberCsidsFromGroup(String serviceName, String groupCsid) throws URISyntaxException, DocumentException {
491         ResourceMap resourcemap = getResourceMap();
492         ResourceBase resource = resourcemap.get(serviceName);
493         return getMemberCsidsFromGroup(resource, groupCsid);
494     }
495
496     private List<String> getMemberCsidsFromGroup(ResourceBase resource, String groupCsid) throws URISyntaxException, DocumentException {
497         // The 'resource' type used here identifies the record type of the
498         // related records to be retrieved
499         AbstractCommonList relatedRecords =
500                 getRelatedRecords(resource, groupCsid, EXCLUDE_DELETED);
501         List<String> memberCsids = getCsidsList(relatedRecords);
502         return memberCsids;
503     }
504
505     private List<String> getNoContextCsids() throws URISyntaxException {
506         ResourceMap resourcemap = getResourceMap();
507         ResourceBase collectionObjectResource = resourcemap.get(CollectionObjectClient.SERVICE_NAME);
508         UriInfo uriInfo = createUriInfo();
509         uriInfo = addFilterToExcludeSoftDeletedRecords(uriInfo);
510         AbstractCommonList collectionObjects = collectionObjectResource.getList(uriInfo);
511         List<String> noContextCsids = getCsidsList(collectionObjects);
512         return noContextCsids;
513     }
514 }