]> git.aero2k.de Git - tmp/jakarta-migration.git/blob
b161d076a3e6d1fa33c81d4de5a998b215e82229
[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                 // 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
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 records?
171                 if (Tools.isBlank(computedCurrentLocation)) {
172                     continue;
173                 }
174                 // Update the value of the computed current location field
175                 // in the CollectionObject record
176                 numUpdated = updateComputedCurrentLocationValue(collectionObjectResource,
177                         collectionObjectCsid, computedCurrentLocation, resourcemap, numUpdated);
178             }
179
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);
186             return getResults();
187         }
188
189         logger.info("Updated computedCurrentLocation values in " + numUpdated + " CollectionObject record(s).");
190         getResults().setNumAffected(numUpdated);
191         return getResults();
192     }
193
194     private String computeCurrentLocation(AbstractCommonList relatedMovements) {
195         Set<String> alreadyProcessedMovementCsids = new HashSet<String>();
196         String computedCurrentLocation;
197         String movementCsid;
198         computedCurrentLocation = "";
199         String currentLocation;
200         String locationDate;
201         String mostRecentLocationDate = "";
202         for (AbstractCommonList.ListItem movementRecord : relatedMovements.getListItem()) {
203             movementCsid = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CSID_ELEMENT_NAME);
204             if (Tools.isBlank(movementCsid)) {
205                 continue;
206             }
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)) {
211                 continue;
212             } else {
213                 alreadyProcessedMovementCsids.add(movementCsid);
214             }
215             locationDate = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, LOCATION_DATE_ELEMENT_NAME);
216             if (Tools.isBlank(locationDate)) {
217                 continue;
218             }
219             currentLocation = AbstractCommonListUtils.ListItemGetElementValue(movementRecord, CURRENT_LOCATION_ELEMENT_NAME);
220             if (Tools.isBlank(currentLocation)) {
221                 continue;
222             }
223             if (logger.isTraceEnabled()) {
224                 logger.trace("Location date value = " + locationDate);
225                 logger.trace("Current location value = " + currentLocation);
226             }
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.
230             //
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.
234             //
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;
244             }
245
246         }
247         return computedCurrentLocation;
248     }
249
250     private int updateComputedCurrentLocationValue(ResourceBase collectionObjectResource,
251             String collectionObjectCsid, String computedCurrentLocation, ResourceMap resourcemap, int numUpdated)
252             throws DocumentException, URISyntaxException {
253         PoxPayloadOut collectionObjectPayload;
254         String objectNumber;
255         String previousComputedCurrentLocation;
256
257         collectionObjectPayload = findByCsid(collectionObjectResource, collectionObjectCsid);
258         if (Tools.isBlank(collectionObjectPayload.toXML())) {
259             return numUpdated;
260         } else {
261             if (logger.isTraceEnabled()) {
262                 logger.trace("Payload: " + "\n" + collectionObjectPayload);
263             }
264         }
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)) {
272             return numUpdated;
273         }
274         // Perform the update only if there is a non-blank object number available.
275         //
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);
284         }
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)) {
290             return numUpdated;
291         }
292
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>"
301                 + "</document>";
302         if (logger.isTraceEnabled()) {
303             logger.trace("Update payload: " + "\n" + collectionObjectUpdatePayload);
304         }
305         byte[] response = collectionObjectResource.update(resourcemap, null, collectionObjectCsid,
306                 collectionObjectUpdatePayload);
307         numUpdated++;
308         if (logger.isTraceEnabled()) {
309             logger.trace("Computed current location value for CollectionObject " + collectionObjectCsid
310                     + " was set to " + computedCurrentLocation);
311
312         }
313         return numUpdated;
314     }
315
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);
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 queryString) throws URISyntaxException {
345         URI uri = new URI(null, null, null, queryString, 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)
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)) {
386                 isDeleted = true;
387             }
388         }
389         return isDeleted;
390     }
391
392     private UriInfo addFilterToExcludeSoftDeletedRecords(UriInfo uriInfo) throws URISyntaxException {
393         if (uriInfo == null) {
394             uriInfo = createUriInfo();
395         }
396         uriInfo.getQueryParameters().add(WorkflowClient.WORKFLOW_QUERY_NONDELETED, Boolean.FALSE.toString());
397         return uriInfo;
398     }
399
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);
406         }
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);
413         }
414         return relatedRecords;
415     }
416
417     /**
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.
420      *
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
427      * record
428      * @throws URISyntaxException
429      */
430     private AbstractCommonList getRecordsRelatedToObjectCsid(ResourceBase resource, String csid, boolean excludeDeletedRecords) throws URISyntaxException {
431         return getRecordsRelatedToCsid(resource, csid, "rtObj", excludeDeletedRecords);
432     }
433
434     /**
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.
437      *
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
444      * record
445      * @throws URISyntaxException
446      */
447     private AbstractCommonList getRecordsRelatedToSubjectCsid(ResourceBase resource, String csid, boolean excludeDeletedRecords) throws URISyntaxException {
448         return getRecordsRelatedToCsid(resource, csid, "rtSbj", excludeDeletedRecords);
449     }
450
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());
459         }
460         if (recordsRelatedToSubjectCSID.getListItem().size() > 0) {
461             relatedRecords.getListItem().addAll(recordsRelatedToSubjectCSID.getListItem());
462         }
463         if (logger.isTraceEnabled()) {
464             logger.trace("Identified a total of " + relatedRecords.getListItem().size()
465                     + " record(s) related to the record with CSID " + csid);
466         }
467         return relatedRecords;
468     }
469
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));
474         }
475         return csids;
476     }
477
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);
482     }
483
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);
490         return memberCsids;
491     }
492
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;
501     }
502 }