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.api.Tools;
14 import org.collectionspace.services.common.invocable.InvocationResults;
15 import org.collectionspace.services.common.vocabulary.AuthorityResource;
16 import org.collectionspace.services.taxonomy.nuxeo.TaxonBotGardenConstants;
17 import org.collectionspace.services.taxonomy.nuxeo.TaxonConstants;
18 import org.dom4j.DocumentException;
19 import org.slf4j.Logger;
20 import org.slf4j.LoggerFactory;
23 * A batch job that sets the access code on taxonomy records. The single CSID context is supported.
25 * If the document is a taxon record, the access codes of the taxon record and all of its descendant
26 * (narrower context) records are updated.
28 * If the document is a collectionobject, the access codes of all taxon records referenced by the
29 * collectionobject's taxonomic identification are updated, and propagated up the taxon
30 * hierarchy to the ancestors (broader contexts) of each taxon record.
35 public class UpdateAccessCodeBatchJob extends AbstractBatchJob {
36 final Logger logger = LoggerFactory.getLogger(UpdateAccessCodeBatchJob.class);
38 private final String[] TAXON_FIELD_NAME_PARTS = CollectionObjectBotGardenConstants.TAXON_FIELD_NAME.split("\\/");
39 private final String TAXON_FIELD_NAME_WITHOUT_PATH = TAXON_FIELD_NAME_PARTS[TAXON_FIELD_NAME_PARTS.length - 1];
41 public UpdateAccessCodeBatchJob() {
42 this.setSupportedInvocationModes(Arrays.asList(INVOCATION_MODE_SINGLE));
47 setCompletionStatus(STATUS_MIN_PROGRESS);
50 if (this.requestIsForInvocationModeSingle()) {
51 String csid = getInvocationContext().getSingleCSID();
53 if (StringUtils.isEmpty(csid)) {
54 throw new Exception("Missing context csid");
57 String docType = getInvocationContext().getDocType();
59 if (docType.equals(TaxonConstants.NUXEO_DOCTYPE)) {
60 setResults(updateAccessCode(csid, true));
61 //setResults(updateParentAccessCode(csid, true));
63 else if (docType.equals(CollectionObjectConstants.NUXEO_DOCTYPE)) {
64 setResults(updateReferencedAccessCodes(csid, true));
67 throw new Exception("Unsupported document type: " + docType);
71 throw new Exception("Unsupported invocation mode: " + this.getInvocationContext().getMode());
74 setCompletionStatus(STATUS_COMPLETE);
77 setCompletionStatus(STATUS_ERROR);
78 setErrorInfo(new InvocationError(INT_ERROR_STATUS, e.getMessage()));
84 * Updates the access code of the specified taxon record.
86 * @param taxonRefNameOrCsid The refname or csid of the taxon record.
87 * @param deep If true, update the access codes of all descendant (narrower context)
88 * taxon records. On a deep update, the access codes of all descendant
89 * records are updated first, before calculating the access code of the parent.
90 * This ensures that the access codes of children are up-to-date, and can be
91 * used to calculate an up-to-date value for the parent.
93 * If false, only the specified taxon record is updated. The calculation
94 * of the access code uses the access codes of child taxon records, so
95 * an accurate result depends on the accuracy of the children's access codes.
96 * @return The results of the invocation.
97 * @throws URISyntaxException
98 * @throws DocumentException
100 public InvocationResults updateAccessCode(String taxonRefNameOrCsid, boolean deep) throws URISyntaxException, DocumentException, Exception {
101 UpdateAccessCodeResults updateResults = updateAccessCode(taxonRefNameOrCsid, deep, false);
103 InvocationResults results = new InvocationResults();
104 results.setNumAffected(updateResults.getNumAffected());
105 results.setUserNote(updateResults.isChanged() ? "access code changed to " + updateResults.getAccessCode() : "access code not changed");
111 * Updates the access code of the parent (broader context) of the specified taxon record.
113 * @param taxonCsid The csid of the taxon record.
114 * @param propagate If true, propagate the access code up the taxon hierarchy to
115 * all ancestors of the taxon record. The propagation stops when
116 * the new value of the access code is the same as the old value,
117 * or when a root node (a node with no broader context) is reached.
119 * If false, update only the access code of the parent.
120 * @return The results of the invocation.
121 * @throws URISyntaxException
122 * @throws DocumentException
124 public InvocationResults updateParentAccessCode(String taxonCsid, boolean propagate) throws URISyntaxException, DocumentException, Exception {
125 PoxPayloadOut taxonPayload = findTaxonByCsid(taxonCsid);
126 String taxonRefName = getFieldValue(taxonPayload, TaxonConstants.REFNAME_SCHEMA_NAME, TaxonConstants.REFNAME_FIELD_NAME);
127 String accessCode = getFieldValue(taxonPayload, TaxonBotGardenConstants.ACCESS_CODE_SCHEMA_NAME, TaxonBotGardenConstants.ACCESS_CODE_FIELD_NAME);
129 logger.debug("updating parent access code: taxonRefName=" + taxonRefName + " propagate=" + propagate + " accessCode=" + accessCode);
131 UpdateAccessCodeResults updateResults = updateParentAccessCode(taxonCsid, accessCode, propagate);
133 InvocationResults results = new InvocationResults();
134 results.setNumAffected(updateResults.getNumAffected());
135 results.setUserNote(results.getNumAffected() + " access codes changed");
141 * Updates the access codes of all taxon records that are referenced in the taxonomic identification
142 * field of the specified collectionobject.
144 * @param collectionObjectCsid The csid of the collectionobject.
145 * @param propagate If true, propagate the access code up the taxon hierarchy to
146 * the ancestors of each referenced taxon record. The propagation stops when
147 * the new value of the access code is the same as the old value,
148 * or when a root node (a node with no broader context) is reached.
150 * If false, update only the access codes of the taxon records
151 * that are directly referenced.
152 * @return The results of the invocation.
153 * @throws URISyntaxException
154 * @throws DocumentException
156 public InvocationResults updateReferencedAccessCodes(String collectionObjectCsid, boolean propagate) throws URISyntaxException, DocumentException, Exception {
157 PoxPayloadOut collectionObjectPayload = findCollectionObjectByCsid(collectionObjectCsid);
159 String deadFlag = getFieldValue(collectionObjectPayload, CollectionObjectBotGardenConstants.DEAD_FLAG_SCHEMA_NAME,
160 CollectionObjectBotGardenConstants.DEAD_FLAG_FIELD_NAME);
161 boolean isAlive = (deadFlag == null) || (!deadFlag.equalsIgnoreCase("true"));
163 logger.debug("updating referenced access codes: collectionObjectCsid=" + collectionObjectCsid + " propagate=" + propagate + " isAlive=" + isAlive);
165 List<String> taxonRefNames = getFieldValues(collectionObjectPayload, CollectionObjectBotGardenConstants.TAXON_SCHEMA_NAME,
166 CollectionObjectBotGardenConstants.TAXON_FIELD_NAME);
167 long numAffected = 0;
169 for (String taxonRefName : taxonRefNames) {
170 PoxPayloadOut taxonPayload = findTaxonByRefName(taxonRefName);
171 if (taxonPayload != null) {
172 UpdateAccessCodeResults updateResults = updateAccessCode(taxonPayload, false, isAlive);
173 if (updateResults.isChanged()) {
174 numAffected += updateResults.getNumAffected();
177 UpdateAccessCodeResults parentUpdateResults = updateParentAccessCode(getCsid(taxonPayload), updateResults.getAccessCode(), true);
179 numAffected += parentUpdateResults.getNumAffected();
183 if (Tools.isBlank(taxonRefName) == false) {
184 String msg = String.format("%s found that cataloging/object record CSID=%s references taxon '%s' which could not be found.",
185 getClass().getName(), collectionObjectCsid, taxonRefName);
191 InvocationResults results = new InvocationResults();
192 results.setNumAffected(numAffected);
193 results.setUserNote(numAffected + " access codes changed");
199 * Updates the access code of the specified taxon record. The access code is determined by
200 * examining all collectionobjects that have a taxonomic identification that matches the
201 * refname of the taxon record, as well as the access codes of child (narrower context)
202 * taxon records. If all referencing collectionobjects are dead (as determined
203 * by the dead flag), and all child taxon records are dead (as determined by their access
204 * codes), then the access code is set to Dead. If any matching collectionobjects
205 * are not dead, or any child taxons are not dead, and the access code is currently Dead,
206 * the access code is set to Unrestricted. Otherwise, the access code is not changed.
208 * @param taxonPayload The services payload of the taxon record.
209 * @param deep If true, update the access code of all descendant taxon records.
210 * On a deep update, the access codes of all descendant
211 * records are updated first, before calculating the access code of the parent.
212 * This ensures that the access codes of children are up-to-date, and can be
213 * used to calculate an up-to-date value for the parent.
215 * If false, only the specified taxon record is updated. The calculation
216 * of the access code uses the access codes of child taxon records, so
217 * an accurate result depends on the accuracy of the children's access codes.
218 * @param knownAlive A hint that a child taxon of the specified taxon is known to be
219 * alive, or that a collectionobject of the specified taxon is known to be
220 * alive. This parameter allows for optimization when propagating
221 * access code changes up the hierarchy; if a child taxon or
222 * referencing collectionobject is known to be alive, and the
223 * current access code is Dead, then the access code can be changed
224 * to Unrestricted without examining any other records.
225 * @return The results of the update.
226 * @throws DocumentException
227 * @throws URISyntaxException
229 public UpdateAccessCodeResults updateAccessCode(PoxPayloadOut taxonPayload, boolean deep, boolean knownAlive) throws URISyntaxException, DocumentException, Exception {
230 UpdateAccessCodeResults results = new UpdateAccessCodeResults();
231 boolean foundAlive = knownAlive;
233 String taxonCsid = getCsid(taxonPayload);
234 String taxonRefName = getFieldValue(taxonPayload, TaxonConstants.REFNAME_SCHEMA_NAME, TaxonConstants.REFNAME_FIELD_NAME);
235 String accessCode = getFieldValue(taxonPayload, TaxonBotGardenConstants.ACCESS_CODE_SCHEMA_NAME, TaxonBotGardenConstants.ACCESS_CODE_FIELD_NAME);
237 logger.debug("updating access code: taxonRefName=" + taxonRefName + " deep=" + deep + " knownAlive=" + knownAlive);
239 if (accessCode == null) {
243 List<String> childTaxonCsids = findNarrower(taxonCsid);
246 long numChildrenChanged = 0;
248 // Update the access code on all the children, and track whether any are alive.
250 for (String childTaxonCsid : childTaxonCsids) {
251 UpdateAccessCodeResults childResults = updateAccessCode(childTaxonCsid, true, false);
253 if (!childResults.isSoftDeleted()) {
254 String childAccessCode = childResults.getAccessCode();
255 boolean isChildAlive = !childAccessCode.equals(TaxonBotGardenConstants.ACCESS_CODE_DEAD_VALUE);
261 if (childResults.isChanged()) {
262 numChildrenChanged++;
267 results.setNumAffected(numChildrenChanged);
271 // Check if any of the children are alive.
273 for (String childTaxonCsid : childTaxonCsids) {
274 PoxPayloadOut childTaxonPayload = findTaxonByCsid(childTaxonCsid);
276 String childAccessCode = getFieldValue(childTaxonPayload, TaxonBotGardenConstants.ACCESS_CODE_SCHEMA_NAME,
277 TaxonBotGardenConstants.ACCESS_CODE_FIELD_NAME);
278 boolean isChildAlive = !childAccessCode.equals(TaxonBotGardenConstants.ACCESS_CODE_DEAD_VALUE);
289 // Check if any examples of this taxon are alive.
291 RefName.AuthorityItem item = RefName.AuthorityItem.parse(taxonRefName);
292 String vocabularyShortId = item.getParentShortIdentifier();
294 List<String> collectionObjectCsids = findReferencingCollectionObjects(TaxonomyAuthorityClient.SERVICE_NAME, vocabularyShortId, taxonCsid,
295 CollectionObjectBotGardenConstants.TAXON_SCHEMA_NAME + ":" + TAXON_FIELD_NAME_WITHOUT_PATH);
297 for (String collectionObjectCsid : collectionObjectCsids) {
298 PoxPayloadOut collectionObjectPayload = findCollectionObjectByCsid(collectionObjectCsid);
300 String deadFlag = getFieldValue(collectionObjectPayload, CollectionObjectBotGardenConstants.DEAD_FLAG_SCHEMA_NAME,
301 CollectionObjectBotGardenConstants.DEAD_FLAG_FIELD_NAME);
302 boolean isDead = (deadFlag != null) && (deadFlag.equalsIgnoreCase("true"));
311 String newAccessCode;
313 // The access code only needs to be changed if:
315 // 1. There is a living example of the taxon, but the access code is dead.
316 // 2. There are no living examples, but the access code is not dead.
318 // Otherwise, the access code should stay the same. In particular, if there is a
319 // living example, and the access code is not dead, the current value of unrestricted
320 // or restricted should be retained.
322 if (foundAlive && (StringUtils.isEmpty(accessCode) || accessCode.equals(TaxonBotGardenConstants.ACCESS_CODE_DEAD_VALUE))) {
323 newAccessCode = TaxonBotGardenConstants.ACCESS_CODE_UNRESTRICTED_VALUE;
325 else if (!foundAlive) {
326 newAccessCode = TaxonBotGardenConstants.ACCESS_CODE_DEAD_VALUE;
329 newAccessCode = accessCode;
332 if (!newAccessCode.equals(accessCode)) {
333 String inAuthority = getFieldValue(taxonPayload, TaxonConstants.IN_AUTHORITY_SCHEMA_NAME, TaxonConstants.IN_AUTHORITY_FIELD_NAME);
335 setAccessCode(inAuthority, taxonCsid, newAccessCode);
337 results.setChanged(true);
338 results.setNumAffected(results.getNumAffected() + 1);
341 results.setAccessCode(newAccessCode);
347 * Updates the access code of the taxon record with the specified refname or csid.
349 * @param taxonRefNameOrCsid
353 * @throws URISyntaxException
354 * @throws DocumentException
356 public UpdateAccessCodeResults updateAccessCode(String taxonRefNameOrCsid, boolean deep, boolean knownAlive) throws URISyntaxException, DocumentException, Exception {
357 PoxPayloadOut taxonPayload;
359 if (RefName.AuthorityItem.parse(taxonRefNameOrCsid) == null) {
360 taxonPayload = findTaxonByCsid(taxonRefNameOrCsid);
363 taxonPayload = findTaxonByRefName(taxonRefNameOrCsid);
366 return updateAccessCode(taxonPayload, deep, knownAlive);
370 * Updates the access code of the taxon record with the specified refname or csid, when a new
371 * child taxon is known to have been added.
373 * @param taxonRefNameOrCsid
375 * @param newChildCsid The csid of the newly added child.
377 * @throws URISyntaxException
378 * @throws DocumentException
380 public UpdateAccessCodeResults updateAccessCode(String taxonRefNameOrCsid, boolean deep, String newChildTaxonCsid) throws URISyntaxException, DocumentException, Exception {
381 PoxPayloadOut newChildTaxonPayload = findTaxonByCsid(newChildTaxonCsid);
382 String newChildTaxonAccessCode = getFieldValue(newChildTaxonPayload, TaxonBotGardenConstants.ACCESS_CODE_SCHEMA_NAME, TaxonBotGardenConstants.ACCESS_CODE_FIELD_NAME);
384 if (newChildTaxonAccessCode == null) {
385 newChildTaxonAccessCode = "";
388 boolean knownAlive = !newChildTaxonAccessCode.equals(TaxonBotGardenConstants.ACCESS_CODE_DEAD_VALUE);
390 return updateAccessCode(taxonRefNameOrCsid, deep, knownAlive);
394 * Updates the access code of the parent (broader context) of the specified taxon record,
395 * whose access code is assumed to be a specified value.
397 * @param taxonCsid The csid of the taxon record.
398 * @param accessCode The access code of the taxon record.
399 * @param propagate If true, propagate the access code up the taxon hierarchy to
400 * all ancestors of the taxon record. The propagation stops when
401 * the new value of the access code is the same as the old value,
402 * or when a root node (a node with no broader context) is reached.
404 * If false, update only the access code of the parent.
405 * @return The results of the update.
406 * @throws URISyntaxException
407 * @throws DocumentException
409 public UpdateAccessCodeResults updateParentAccessCode(String taxonCsid, String accessCode, boolean propagate) throws URISyntaxException, DocumentException, Exception {
410 UpdateAccessCodeResults results = new UpdateAccessCodeResults();
411 String parentTaxonCsid = findBroader(taxonCsid);
412 long numAffected = 0;
414 logger.debug("updating parent access code: taxonCsid=" + taxonCsid + " accessCode=" + accessCode + " propagate=" + propagate);
416 if (parentTaxonCsid != null) {
417 boolean isAlive = (accessCode == null) || !accessCode.equals(TaxonBotGardenConstants.ACCESS_CODE_DEAD_VALUE);
419 UpdateAccessCodeResults parentUpdateResults = updateAccessCode(parentTaxonCsid, false, isAlive);
421 if (parentUpdateResults.isChanged()) {
422 // Except for numAffected, the result fields are probably not all that useful in this situation.
423 // Set the changed flag to whether the immediate parent was changed, and the access code to
424 // the immediate parent's.
426 results.setAccessCode(parentUpdateResults.getAccessCode());
427 results.setChanged(true);
429 numAffected += parentUpdateResults.getNumAffected();
432 UpdateAccessCodeResults grandparentUpdateResults = updateParentAccessCode(parentTaxonCsid, parentUpdateResults.getAccessCode(), true);
433 numAffected += grandparentUpdateResults.getNumAffected();
438 results.setNumAffected(numAffected);
444 * Sets the access code of the specified taxon record to the specified value.
446 * @param authorityCsid The csid of the authority containing the taxon record.
447 * @param taxonCsid The csid of the taxon record.
448 * @param accessCode The value of the access code.
449 * @throws URISyntaxException
451 private void setAccessCode(String authorityCsid, String taxonCsid, String accessCode) throws URISyntaxException {
452 String updatePayload =
453 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
454 "<document name=\"taxon\">" +
455 "<ns2:taxon_naturalhistory xmlns:ns2=\"http://collectionspace.org/services/taxonomy/domain/naturalhistory\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">" +
456 getFieldXml(TaxonBotGardenConstants.ACCESS_CODE_FIELD_NAME, accessCode) +
457 "</ns2:taxon_naturalhistory>" +
460 AuthorityResource<?, ?> resource = (AuthorityResource<?, ?>) getResourceMap().get(TaxonomyAuthorityClient.SERVICE_NAME);
461 resource.updateAuthorityItem(getServiceContext(), getResourceMap(), createUriInfo(), authorityCsid, taxonCsid, updatePayload);
464 public class UpdateAccessCodeResults {
465 private boolean isSoftDeleted = false;
466 private boolean isChanged = false;
467 private String accessCode = null;
468 private long numAffected = 0;
470 public boolean isSoftDeleted() {
471 return isSoftDeleted;
475 * @param isSoftDeleted
477 public void setSoftDeleted(boolean isSoftDeleted) {
478 this.isSoftDeleted = isSoftDeleted;
481 public boolean isChanged() {
485 public void setChanged(boolean isChanged) {
486 this.isChanged = isChanged;
489 public String getAccessCode() {
493 public void setAccessCode(String accessCode) {
494 this.accessCode = accessCode;
497 public long getNumAffected() {
501 public void setNumAffected(long numAffected) {
502 this.numAffected = numAffected;