1 package org.collectionspace.services.batch.nuxeo;
3 import java.net.URISyntaxException;
4 import java.util.Arrays;
7 import org.apache.commons.lang.StringUtils;
8 import org.collectionspace.services.client.PoxPayloadOut;
9 import org.collectionspace.services.client.TaxonomyAuthorityClient;
10 import org.collectionspace.services.collectionobject.nuxeo.CollectionObjectBotGardenConstants;
11 import org.collectionspace.services.collectionobject.nuxeo.CollectionObjectConstants;
12 import org.collectionspace.services.common.api.RefName;
13 import org.collectionspace.services.common.invocable.InvocationResults;
14 import org.collectionspace.services.common.vocabulary.AuthorityResource;
15 import org.collectionspace.services.taxonomy.nuxeo.TaxonBotGardenConstants;
16 import org.collectionspace.services.taxonomy.nuxeo.TaxonConstants;
17 import org.dom4j.DocumentException;
18 import org.slf4j.Logger;
19 import org.slf4j.LoggerFactory;
22 * A batch job that sets the access code on taxonomy records. The single CSID context is supported.
24 * If the document is a taxon record, the access codes of the taxon record and all of its descendant
25 * (narrower context) records are updated.
27 * If the document is a collectionobject, the access codes of all taxon records referenced by the
28 * collectionobject's taxonomic identification are updated, and propagated up the taxon
29 * hierarchy to the ancestors (broader contexts) of each taxon record.
34 public class UpdateAccessCodeBatchJob extends AbstractBatchJob {
35 final Logger logger = LoggerFactory.getLogger(UpdateAccessCodeBatchJob.class);
37 private final String[] TAXON_FIELD_NAME_PARTS = CollectionObjectBotGardenConstants.TAXON_FIELD_NAME.split("\\/");
38 private final String TAXON_FIELD_NAME_WITHOUT_PATH = TAXON_FIELD_NAME_PARTS[TAXON_FIELD_NAME_PARTS.length - 1];
40 public UpdateAccessCodeBatchJob() {
41 this.setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE));
46 setCompletionStatus(STATUS_MIN_PROGRESS);
49 if (this.requestIsForInvocationModeSingle()) {
50 String csid = getInvocationContext().getSingleCSID();
52 if (StringUtils.isEmpty(csid)) {
53 throw new Exception("Missing context csid");
56 String docType = getInvocationContext().getDocType();
58 if (docType.equals(TaxonConstants.NUXEO_DOCTYPE)) {
59 setResults(updateAccessCode(csid, true));
60 //setResults(updateParentAccessCode(csid, true));
62 else if (docType.equals(CollectionObjectConstants.NUXEO_DOCTYPE)) {
63 setResults(updateReferencedAccessCodes(csid, true));
66 throw new Exception("Unsupported document type: " + docType);
70 throw new Exception("Unsupported invocation mode: " + this.getInvocationContext().getMode());
73 setCompletionStatus(STATUS_COMPLETE);
76 setCompletionStatus(STATUS_ERROR);
77 setErrorInfo(new InvocationError(INT_ERROR_STATUS, e.getMessage()));
83 * Updates the access code of the specified taxon record.
85 * @param taxonRefNameOrCsid The refname or csid of the taxon record.
86 * @param deep If true, update the access codes of all descendant (narrower context)
87 * taxon records. On a deep update, the access codes of all descendant
88 * records are updated first, before calculating the access code of the parent.
89 * This ensures that the access codes of children are up-to-date, and can be
90 * used to calculate an up-to-date value for the parent.
92 * If false, only the specified taxon record is updated. The calculation
93 * of the access code uses the access codes of child taxon records, so
94 * an accurate result depends on the accuracy of the children's access codes.
95 * @return The results of the invocation.
96 * @throws URISyntaxException
97 * @throws DocumentException
99 public InvocationResults updateAccessCode(String taxonRefNameOrCsid, boolean deep) throws URISyntaxException, DocumentException {
100 UpdateAccessCodeResults updateResults = updateAccessCode(taxonRefNameOrCsid, deep, false);
102 InvocationResults results = new InvocationResults();
103 results.setNumAffected(updateResults.getNumAffected());
104 results.setUserNote(updateResults.isChanged() ? "access code changed to " + updateResults.getAccessCode() : "access code not changed");
110 * Updates the access code of the parent (broader context) of the specified taxon record.
112 * @param taxonCsid The csid of the taxon record.
113 * @param propagate If true, propagate the access code up the taxon hierarchy to
114 * all ancestors of the taxon record. The propagation stops when
115 * the new value of the access code is the same as the old value,
116 * or when a root node (a node with no broader context) is reached.
118 * If false, update only the access code of the parent.
119 * @return The results of the invocation.
120 * @throws URISyntaxException
121 * @throws DocumentException
123 public InvocationResults updateParentAccessCode(String taxonCsid, boolean propagate) throws URISyntaxException, DocumentException {
124 PoxPayloadOut taxonPayload = findTaxonByCsid(taxonCsid);
125 String taxonRefName = getFieldValue(taxonPayload, TaxonConstants.REFNAME_SCHEMA_NAME, TaxonConstants.REFNAME_FIELD_NAME);
126 String accessCode = getFieldValue(taxonPayload, TaxonBotGardenConstants.ACCESS_CODE_SCHEMA_NAME, TaxonBotGardenConstants.ACCESS_CODE_FIELD_NAME);
128 logger.debug("updating parent access code: taxonRefName=" + taxonRefName + " propagate=" + propagate + " accessCode=" + accessCode);
130 UpdateAccessCodeResults updateResults = updateParentAccessCode(taxonCsid, accessCode, propagate);
132 InvocationResults results = new InvocationResults();
133 results.setNumAffected(updateResults.getNumAffected());
134 results.setUserNote(results.getNumAffected() + " access codes changed");
140 * Updates the access codes of all taxon records that are referenced in the taxonomic identification
141 * field of the specified collectionobject.
143 * @param collectionObjectCsid The csid of the collectionobject.
144 * @param propagate If true, propagate the access code up the taxon hierarchy to
145 * the ancestors of each referenced taxon record. The propagation stops when
146 * the new value of the access code is the same as the old value,
147 * or when a root node (a node with no broader context) is reached.
149 * If false, update only the access codes of the taxon records
150 * that are directly referenced.
151 * @return The results of the invocation.
152 * @throws URISyntaxException
153 * @throws DocumentException
155 public InvocationResults updateReferencedAccessCodes(String collectionObjectCsid, boolean propagate) throws URISyntaxException, DocumentException {
156 PoxPayloadOut collectionObjectPayload = findCollectionObjectByCsid(collectionObjectCsid);
158 String deadFlag = getFieldValue(collectionObjectPayload, CollectionObjectBotGardenConstants.DEAD_FLAG_SCHEMA_NAME,
159 CollectionObjectBotGardenConstants.DEAD_FLAG_FIELD_NAME);
160 boolean isAlive = (deadFlag == null) || (!deadFlag.equalsIgnoreCase("true"));
162 logger.debug("updating referenced access codes: collectionObjectCsid=" + collectionObjectCsid + " propagate=" + propagate + " isAlive=" + isAlive);
164 List<String> taxonRefNames = getFieldValues(collectionObjectPayload, CollectionObjectBotGardenConstants.TAXON_SCHEMA_NAME,
165 CollectionObjectBotGardenConstants.TAXON_FIELD_NAME);
166 long numAffected = 0;
168 for (String taxonRefName : taxonRefNames) {
169 PoxPayloadOut taxonPayload = findTaxonByRefName(taxonRefName);
170 UpdateAccessCodeResults updateResults = updateAccessCode(taxonPayload, false, isAlive);
172 if (updateResults.isChanged()) {
173 numAffected += updateResults.getNumAffected();
176 UpdateAccessCodeResults parentUpdateResults = updateParentAccessCode(getCsid(taxonPayload), updateResults.getAccessCode(), true);
178 numAffected += parentUpdateResults.getNumAffected();
183 InvocationResults results = new InvocationResults();
184 results.setNumAffected(numAffected);
185 results.setUserNote(numAffected + " access codes changed");
191 * Updates the access code of the specified taxon record. The access code is determined by
192 * examining all collectionobjects that have a taxonomic identification that matches the
193 * refname of the taxon record, as well as the access codes of child (narrower context)
194 * taxon records. If all referencing collectionobjects are dead (as determined
195 * by the dead flag), and all child taxon records are dead (as determined by their access
196 * codes), then the access code is set to Dead. If any matching collectionobjects
197 * are not dead, or any child taxons are not dead, and the access code is currently Dead,
198 * the access code is set to Unrestricted. Otherwise, the access code is not changed.
200 * @param taxonPayload The services payload of the taxon record.
201 * @param deep If true, update the access code of all descendant taxon records.
202 * On a deep update, the access codes of all descendant
203 * records are updated first, before calculating the access code of the parent.
204 * This ensures that the access codes of children are up-to-date, and can be
205 * used to calculate an up-to-date value for the parent.
207 * If false, only the specified taxon record is updated. The calculation
208 * of the access code uses the access codes of child taxon records, so
209 * an accurate result depends on the accuracy of the children's access codes.
210 * @param knownAlive A hint that a child taxon of the specified taxon is known to be
211 * alive, or that a collectionobject of the specified taxon is known to be
212 * alive. This parameter allows for optimization when propagating
213 * access code changes up the hierarchy; if a child taxon or
214 * referencing collectionobject is known to be alive, and the
215 * current access code is Dead, then the access code can be changed
216 * to Unrestricted without examining any other records.
217 * @return The results of the update.
218 * @throws DocumentException
219 * @throws URISyntaxException
221 public UpdateAccessCodeResults updateAccessCode(PoxPayloadOut taxonPayload, boolean deep, boolean knownAlive) throws URISyntaxException, DocumentException {
222 UpdateAccessCodeResults results = new UpdateAccessCodeResults();
223 boolean foundAlive = knownAlive;
225 String taxonCsid = getCsid(taxonPayload);
226 String taxonRefName = getFieldValue(taxonPayload, TaxonConstants.REFNAME_SCHEMA_NAME, TaxonConstants.REFNAME_FIELD_NAME);
227 String accessCode = getFieldValue(taxonPayload, TaxonBotGardenConstants.ACCESS_CODE_SCHEMA_NAME, TaxonBotGardenConstants.ACCESS_CODE_FIELD_NAME);
229 logger.debug("updating access code: taxonRefName=" + taxonRefName + " deep=" + deep + " knownAlive=" + knownAlive);
231 if (accessCode == null) {
235 List<String> childTaxonCsids = findNarrower(taxonCsid);
238 long numChildrenChanged = 0;
240 // Update the access code on all the children, and track whether any are alive.
242 for (String childTaxonCsid : childTaxonCsids) {
243 UpdateAccessCodeResults childResults = updateAccessCode(childTaxonCsid, true, false);
245 if (!childResults.isSoftDeleted()) {
246 String childAccessCode = childResults.getAccessCode();
247 boolean isChildAlive = !childAccessCode.equals(TaxonBotGardenConstants.ACCESS_CODE_DEAD_VALUE);
253 if (childResults.isChanged()) {
254 numChildrenChanged++;
259 results.setNumAffected(numChildrenChanged);
263 // Check if any of the children are alive.
265 for (String childTaxonCsid : childTaxonCsids) {
266 PoxPayloadOut childTaxonPayload = findTaxonByCsid(childTaxonCsid);
268 String childAccessCode = getFieldValue(childTaxonPayload, TaxonBotGardenConstants.ACCESS_CODE_SCHEMA_NAME,
269 TaxonBotGardenConstants.ACCESS_CODE_FIELD_NAME);
270 boolean isChildAlive = !childAccessCode.equals(TaxonBotGardenConstants.ACCESS_CODE_DEAD_VALUE);
281 // Check if any examples of this taxon are alive.
283 RefName.AuthorityItem item = RefName.AuthorityItem.parse(taxonRefName);
284 String vocabularyShortId = item.getParentShortIdentifier();
286 List<String> collectionObjectCsids = findReferencingCollectionObjects(TaxonomyAuthorityClient.SERVICE_NAME, vocabularyShortId, taxonCsid,
287 CollectionObjectBotGardenConstants.TAXON_SCHEMA_NAME + ":" + TAXON_FIELD_NAME_WITHOUT_PATH);
289 for (String collectionObjectCsid : collectionObjectCsids) {
290 PoxPayloadOut collectionObjectPayload = findCollectionObjectByCsid(collectionObjectCsid);
292 String deadFlag = getFieldValue(collectionObjectPayload, CollectionObjectBotGardenConstants.DEAD_FLAG_SCHEMA_NAME,
293 CollectionObjectBotGardenConstants.DEAD_FLAG_FIELD_NAME);
294 boolean isDead = (deadFlag != null) && (deadFlag.equalsIgnoreCase("true"));
303 String newAccessCode;
305 // The access code only needs to be changed if:
307 // 1. There is a living example of the taxon, but the access code is dead.
308 // 2. There are no living examples, but the access code is not dead.
310 // Otherwise, the access code should stay the same. In particular, if there is a
311 // living example, and the access code is not dead, the current value of unrestricted
312 // or restricted should be retained.
314 if (foundAlive && (StringUtils.isEmpty(accessCode) || accessCode.equals(TaxonBotGardenConstants.ACCESS_CODE_DEAD_VALUE))) {
315 newAccessCode = TaxonBotGardenConstants.ACCESS_CODE_UNRESTRICTED_VALUE;
317 else if (!foundAlive) {
318 newAccessCode = TaxonBotGardenConstants.ACCESS_CODE_DEAD_VALUE;
321 newAccessCode = accessCode;
324 if (!newAccessCode.equals(accessCode)) {
325 String inAuthority = getFieldValue(taxonPayload, TaxonConstants.IN_AUTHORITY_SCHEMA_NAME, TaxonConstants.IN_AUTHORITY_FIELD_NAME);
327 setAccessCode(inAuthority, taxonCsid, newAccessCode);
329 results.setChanged(true);
330 results.setNumAffected(results.getNumAffected() + 1);
333 results.setAccessCode(newAccessCode);
339 * Updates the access code of the taxon record with the specified refname or csid.
341 * @param taxonRefNameOrCsid
345 * @throws URISyntaxException
346 * @throws DocumentException
348 public UpdateAccessCodeResults updateAccessCode(String taxonRefNameOrCsid, boolean deep, boolean knownAlive) throws URISyntaxException, DocumentException {
349 PoxPayloadOut taxonPayload;
351 if (RefName.AuthorityItem.parse(taxonRefNameOrCsid) == null) {
352 taxonPayload = findTaxonByCsid(taxonRefNameOrCsid);
355 taxonPayload = findTaxonByRefName(taxonRefNameOrCsid);
358 return updateAccessCode(taxonPayload, deep, knownAlive);
362 * Updates the access code of the parent (broader context) of the specified taxon record,
363 * whose access code is assumed to be a specified value.
365 * @param taxonCsid The csid of the taxon record.
366 * @param accessCode The access code of the taxon record.
367 * @param propagate If true, propagate the access code up the taxon hierarchy to
368 * all ancestors of the taxon record. The propagation stops when
369 * the new value of the access code is the same as the old value,
370 * or when a root node (a node with no broader context) is reached.
372 * If false, update only the access code of the parent.
373 * @return The results of the update.
374 * @throws URISyntaxException
375 * @throws DocumentException
377 public UpdateAccessCodeResults updateParentAccessCode(String taxonCsid, String accessCode, boolean propagate) throws URISyntaxException, DocumentException {
378 UpdateAccessCodeResults results = new UpdateAccessCodeResults();
379 String parentTaxonCsid = findBroader(taxonCsid);
380 long numAffected = 0;
382 logger.debug("updating parent access code: taxonCsid=" + taxonCsid + " accessCode=" + accessCode + " propagate=" + propagate);
384 if (parentTaxonCsid != null) {
385 boolean isAlive = (accessCode == null) || !accessCode.equals(TaxonBotGardenConstants.ACCESS_CODE_DEAD_VALUE);
387 UpdateAccessCodeResults parentUpdateResults = updateAccessCode(parentTaxonCsid, false, isAlive);
389 if (parentUpdateResults.isChanged()) {
390 // Except for numAffected, the result fields are probably not all that useful in this situation.
391 // Set the changed flag to whether the immediate parent was changed, and the access code to
392 // the immediate parent's.
394 results.setAccessCode(parentUpdateResults.getAccessCode());
395 results.setChanged(true);
397 numAffected += parentUpdateResults.getNumAffected();
400 UpdateAccessCodeResults grandparentUpdateResults = updateParentAccessCode(parentTaxonCsid, parentUpdateResults.getAccessCode(), true);
401 numAffected += grandparentUpdateResults.getNumAffected();
406 results.setNumAffected(numAffected);
412 * Sets the access code of the specified taxon record to the specified value.
414 * @param authorityCsid The csid of the authority containing the taxon record.
415 * @param taxonCsid The csid of the taxon record.
416 * @param accessCode The value of the access code.
417 * @throws URISyntaxException
419 private void setAccessCode(String authorityCsid, String taxonCsid, String accessCode) throws URISyntaxException {
420 String updatePayload =
421 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
422 "<document name=\"taxon\">" +
423 "<ns2:taxon_naturalhistory xmlns:ns2=\"http://collectionspace.org/services/taxonomy/domain/naturalhistory\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" +
424 getFieldXml(TaxonBotGardenConstants.ACCESS_CODE_FIELD_NAME, accessCode) +
425 "</ns2:taxon_naturalhistory>" +
428 AuthorityResource<?, ?> resource = (AuthorityResource<?, ?>) getResourceMap().get(TaxonomyAuthorityClient.SERVICE_NAME);
429 resource.updateAuthorityItem(getResourceMap(), createUriInfo(), authorityCsid, taxonCsid, updatePayload);
432 private class UpdateAccessCodeResults {
433 private boolean isSoftDeleted = false;
434 private boolean isChanged = false;
435 private String accessCode = null;
436 private long numAffected = 0;
438 public boolean isSoftDeleted() {
439 return isSoftDeleted;
443 * @param isSoftDeleted
445 public void setSoftDeleted(boolean isSoftDeleted) {
446 this.isSoftDeleted = isSoftDeleted;
449 public boolean isChanged() {
453 public void setChanged(boolean isChanged) {
454 this.isChanged = isChanged;
457 public String getAccessCode() {
461 public void setAccessCode(String accessCode) {
462 this.accessCode = accessCode;
465 public long getNumAffected() {
469 public void setNumAffected(long numAffected) {
470 this.numAffected = numAffected;