ref #10273, ref #10446 add subsection and ipni ID to content export and doc for backb...
[cdmlib.git] / cdmlib-io / src / main / java / eu / etaxonomy / cdm / io / wfo / out / WfoBackboneExport.java
1 /**
2 * Copyright (C) 2017 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
5 *
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
8 */
9 package eu.etaxonomy.cdm.io.wfo.out;
10
11 import java.io.File;
12 import java.util.ArrayList;
13 import java.util.HashSet;
14 import java.util.Iterator;
15 import java.util.List;
16 import java.util.Map;
17 import java.util.Set;
18 import java.util.UUID;
19
20 import org.apache.commons.lang3.StringUtils;
21 import org.joda.time.DateTime;
22 import org.joda.time.DateTimeFieldType;
23 import org.joda.time.Partial;
24 import org.joda.time.format.DateTimeFormatter;
25 import org.joda.time.format.DateTimeFormatterBuilder;
26 import org.springframework.stereotype.Component;
27
28 import eu.etaxonomy.cdm.common.CdmUtils;
29 import eu.etaxonomy.cdm.common.URI;
30 import eu.etaxonomy.cdm.common.monitor.IProgressMonitor;
31 import eu.etaxonomy.cdm.filter.TaxonNodeFilter;
32 import eu.etaxonomy.cdm.format.reference.NomenclaturalSourceFormatter;
33 import eu.etaxonomy.cdm.io.common.CdmExportBase;
34 import eu.etaxonomy.cdm.io.common.ExportResult.ExportResultState;
35 import eu.etaxonomy.cdm.io.common.TaxonNodeOutStreamPartitioner;
36 import eu.etaxonomy.cdm.io.common.XmlExportState;
37 import eu.etaxonomy.cdm.io.common.mapping.out.IExportTransformer;
38 import eu.etaxonomy.cdm.model.common.AnnotatableEntity;
39 import eu.etaxonomy.cdm.model.common.Annotation;
40 import eu.etaxonomy.cdm.model.common.AnnotationType;
41 import eu.etaxonomy.cdm.model.common.CdmBase;
42 import eu.etaxonomy.cdm.model.common.ICdmBase;
43 import eu.etaxonomy.cdm.model.common.Identifier;
44 import eu.etaxonomy.cdm.model.common.Language;
45 import eu.etaxonomy.cdm.model.common.LanguageString;
46 import eu.etaxonomy.cdm.model.common.TimePeriod;
47 import eu.etaxonomy.cdm.model.description.DescriptionBase;
48 import eu.etaxonomy.cdm.model.description.DescriptionElementBase;
49 import eu.etaxonomy.cdm.model.description.Feature;
50 import eu.etaxonomy.cdm.model.description.IDescribable;
51 import eu.etaxonomy.cdm.model.description.TextData;
52 import eu.etaxonomy.cdm.model.name.HomotypicalGroup;
53 import eu.etaxonomy.cdm.model.name.NameRelationship;
54 import eu.etaxonomy.cdm.model.name.NameRelationshipType;
55 import eu.etaxonomy.cdm.model.name.Rank;
56 import eu.etaxonomy.cdm.model.name.TaxonName;
57 import eu.etaxonomy.cdm.model.reference.OriginalSourceBase;
58 import eu.etaxonomy.cdm.model.reference.Reference;
59 import eu.etaxonomy.cdm.model.taxon.Classification;
60 import eu.etaxonomy.cdm.model.taxon.Synonym;
61 import eu.etaxonomy.cdm.model.taxon.Taxon;
62 import eu.etaxonomy.cdm.model.taxon.TaxonBase;
63 import eu.etaxonomy.cdm.model.taxon.TaxonNode;
64 import eu.etaxonomy.cdm.model.taxon.TaxonNodeStatus;
65 import eu.etaxonomy.cdm.model.term.IdentifierType;
66 import eu.etaxonomy.cdm.strategy.cache.agent.TeamDefaultCacheStrategy;
67
68 /**
69 * Classification or taxon tree exporter into WFO Backbone format.
70 *
71 * @see https://dev.e-taxonomy.eu/redmine/issues/10446
72 *
73 * @author a.mueller
74 * @since 2023-12-08
75 */
76 /**
77 * @author muellera
78 * @since 01.02.2024
79 */
80 @Component
81 public class WfoBackboneExport
82 extends CdmExportBase<WfoBackboneExportConfigurator,WfoBackboneExportState,IExportTransformer,File>{
83
84 private static final long serialVersionUID = -4560488499411723333L;
85
86 public WfoBackboneExport() {
87 this.ioName = this.getClass().getSimpleName();
88 }
89
90 @Override
91 public long countSteps(WfoBackboneExportState state) {
92 TaxonNodeFilter filter = state.getConfig().getTaxonNodeFilter();
93 return getTaxonNodeService().count(filter);
94 }
95
96 @Override
97 protected void doInvoke(WfoBackboneExportState state) {
98
99 try {
100 IProgressMonitor monitor = state.getConfig().getProgressMonitor();
101 WfoBackboneExportConfigurator config = state.getConfig();
102
103 //set root node
104 if (config.getTaxonNodeFilter().hasClassificationFilter()) {
105 Classification classification = getClassificationService()
106 .load(config.getTaxonNodeFilter().getClassificationFilter().get(0).getUuid());
107 state.setRootId(classification.getRootNode().getUuid());
108 } else if (config.getTaxonNodeFilter().hasSubtreeFilter()) {
109 state.setRootId(config.getTaxonNodeFilter().getSubtreeFilter().get(0).getUuid());
110 }
111
112 @SuppressWarnings({ "unchecked", "rawtypes" })
113 TaxonNodeOutStreamPartitioner<XmlExportState> partitioner = TaxonNodeOutStreamPartitioner.NewInstance(this,
114 state, state.getConfig().getTaxonNodeFilter(), 100, monitor, null);
115
116 // handleMetaData(state); //FIXME metadata;
117 monitor.subTask("Start partitioning");
118
119 //test configurator
120 String baseUrl = state.getConfig().getSourceLinkBaseUrl();
121 if (isBlank(baseUrl)){
122 String message = "No base url provided.";
123 state.getResult().addWarning(message);
124 } else if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")){
125 String message = "Source link base url is not a http based url.";
126 state.getResult().addWarning(message);
127 }
128
129 //taxon nodes
130 TaxonNode node = partitioner.next();
131 while (node != null) {
132 handleTaxonNode(state, node);
133 node = partitioner.next();
134 }
135
136 state.getProcessor().createFinalResult(state);
137 } catch (Exception e) {
138 state.getResult().addException(e,
139 "An unexpected error occurred in main method doInvoke() " + e.getMessage());
140 e.printStackTrace();
141 }
142 }
143
144 private void handleTaxonNode(WfoBackboneExportState state, TaxonNode taxonNode) {
145
146 if (taxonNode == null) {
147 // TODO 5 taxon node not found
148 String message = "TaxonNode for given taxon node UUID not found.";
149 state.getResult().addError(message);
150 } else {
151 try {
152 boolean exclude = filterTaxon(state, taxonNode);
153
154 //handle taxon
155 String parentWfoId = getParentWfoId(state, taxonNode);
156 if (taxonNode.hasTaxon()) {
157 if (exclude) {
158 state.putTaxonNodeWfoId(taxonNode, parentWfoId); //always use parent instead
159 } else {
160 String wfoId = handleTaxon(state, taxonNode, parentWfoId);
161 state.putTaxonNodeWfoId(taxonNode, wfoId);
162 }
163 }
164 } catch (Exception e) {
165 state.getResult().addException(e, "An unexpected error occurred when handling taxonNode "
166 + taxonNode.getUuid() + ": " + e.getMessage() + e.getStackTrace());
167 }
168 }
169 }
170
171 private String getParentWfoId(WfoBackboneExportState state, TaxonNode taxonNode) {
172 TaxonNode parentNode = taxonNode.getParent();
173 if (parentNode == null) {
174 return null;
175 }
176 String wfoId = state.getTaxonNodeWfoId(parentNode);
177 if (wfoId != null) {
178 return wfoId;
179 }else {
180 wfoId = parentNode.getTaxon() == null ? null
181 : getWfoId(state, parentNode.getTaxon().getName(), false);
182 if (wfoId != null) {
183 state.putTaxonNodeWfoId(parentNode, wfoId);
184 state.putTaxonWfoId(parentNode.getTaxon(), wfoId);
185 state.putNameWfoId(parentNode.getTaxon().getName(), wfoId);
186 }
187
188 return wfoId;
189 }
190 }
191
192 private boolean filterTaxon(WfoBackboneExportState state, TaxonNode taxonNode) {
193 Taxon taxon = taxonNode.getTaxon();
194 if (taxon == null) {
195 return true;
196 }
197 TaxonName taxonName = taxon.getName();
198 if (taxonName == null) {
199 return true;
200 }else if (taxonName.isHybridFormula()) {
201 return true;
202 }else {
203 String wfoId = getWfoId(state, taxonName, false);
204 if (wfoId == null) {
205 return true;
206 }
207 }
208
209 Rank rank = taxonName.getRank();
210 if (rank == null) {
211 //TODO 3 is missing rank handling correct?
212 return true;
213 }else {
214 if (rank.isSpeciesAggregate()){
215 return true;
216 }
217 }
218 return false;
219 }
220
221 /**
222 * @return the WFO-ID of the taxon
223 */
224 private String handleTaxon(WfoBackboneExportState state, TaxonNode taxonNode, String parentWfoId) {
225
226 //check null
227 if (taxonNode == null) {
228 state.getResult().addError("The taxonNode was null.", "handleTaxon");
229 state.getResult().setState(ExportResultState.INCOMPLETE_WITH_ERROR);
230 return null;
231 }
232 //check no taxon
233 if (taxonNode.getTaxon() == null) {
234 state.getResult().addError("There was a taxon node without a taxon: " + taxonNode.getUuid(),
235 "handleTaxon");
236 state.getResult().setState(ExportResultState.INCOMPLETE_WITH_ERROR);
237 return null;
238 }
239
240 //deproxy, just in case
241 Taxon taxon = CdmBase.deproxy(taxonNode.getTaxon());
242 String wfoId = null;
243
244 //handle taxon
245 try {
246
247 //classification csvLine
248 WfoBackboneExportTable table = WfoBackboneExportTable.CLASSIFICATION;
249 String[] csvLine = new String[table.getSize()];
250
251 //accepted name
252 TaxonName name = taxon.getName();
253 wfoId = handleName(state, table, csvLine, name, null);
254
255 //... parentNameUsageID
256 csvLine[table.getIndex(WfoBackboneExportTable.TAX_PARENT_ID)] = parentWfoId;
257
258 //... higher taxa
259 csvLine[table.getIndex(WfoBackboneExportTable.TAX_SUBFAMILY)] = null;
260 csvLine[table.getIndex(WfoBackboneExportTable.TAX_TRIBE)] = null;
261 csvLine[table.getIndex(WfoBackboneExportTable.TAX_SUBTRIBE)] = null;
262 //TODO 2 is subgenus handling correct?
263 csvLine[table.getIndex(WfoBackboneExportTable.TAX_SUBGENUS)] = name.isInfraGeneric()? name.getInfraGenericEpithet() : null ;
264
265 //... tax status, TODO 2 are there other status for accepted or other reasons for being ambiguous
266 String taxonStatus = taxon.isDoubtful()? "ambiguous" : "Accepted";
267 csvLine[table.getIndex(WfoBackboneExportTable.TAX_STATUS)] = taxonStatus;
268
269 //remarks
270 csvLine[table.getIndex(WfoBackboneExportTable.TAXON_REMARKS)] = getRemarks(state, taxon);
271
272 //TODO 7 URL to taxon, take it from a repository information (currently not yet possible, but maybe we could use a CDM preference instead)
273 if (isNotBlank(state.getConfig().getSourceLinkBaseUrl())) {
274 String taxonSourceLink = makeTaxonSourceLink(state, taxon);
275 csvLine[table.getIndex(WfoBackboneExportTable.REFERENCES)] = taxonSourceLink;
276 }
277
278 //excluded info
279 csvLine[table.getIndex(WfoBackboneExportTable.EXCLUDE)] = makeExcluded(state, taxonNode);
280
281 handleTaxonBase(state, table, csvLine, taxon);
282
283 handleSynonyms(state, taxon);
284
285 //process taxon line
286 state.getProcessor().put(table, taxon, csvLine);
287
288 return wfoId;
289
290 } catch (Exception e) {
291 e.printStackTrace();
292 state.getResult().addException(e,
293 "An unexpected problem occurred when trying to export taxon with id " + taxon.getId() + " " + taxon.getTitleCache());
294 state.getResult().setState(ExportResultState.INCOMPLETE_WITH_ERROR);
295 return null;
296 }
297 }
298
299 private String makeExcluded(@SuppressWarnings("unused") WfoBackboneExportState state, TaxonNode taxonNode) {
300 TaxonNodeStatus status = taxonNode.getStatus();
301 if (status == null || (status != TaxonNodeStatus.EXCLUDED && !status.isKindOf(TaxonNodeStatus.EXCLUDED))) {
302 return null;
303 }else {
304 Language lang = Language.getDefaultLanguage(); //TODO 7 language for status note
305
306 String result = status == TaxonNodeStatus.EXCLUDED ? "Excluded" :
307 status == TaxonNodeStatus.EXCLUDED_TAX? "Taxonomically out of scope" : status.getLabel();
308 String note = taxonNode.preferredStatusNote(lang);
309 result = CdmUtils.concat(": ", result, note);
310 return result;
311 }
312 }
313
314 private void handleTaxonBase(WfoBackboneExportState state, WfoBackboneExportTable table, String[] csvLine,
315 TaxonBase<?> taxonBase) {
316
317 //secundum reference
318 Reference secRef = taxonBase.getSec();
319 csvLine[table.getIndex(WfoBackboneExportTable.TAX_NAME_ACCORDING_TO_ID)] = getId(state, secRef);
320 if (secRef != null
321 && (!state.getReferenceStore().contains((secRef.getUuid())))) {
322 handleReference(state, taxonBase.getSecSource());
323 }
324
325 //TODO 2 created
326 csvLine[table.getIndex(WfoBackboneExportTable.CREATED)] = null;
327
328 //TODO 2 modified
329 csvLine[table.getIndex(WfoBackboneExportTable.MODIFIED)] = null;
330
331 }
332
333 private String makeTaxonSourceLink(WfoBackboneExportState state, Taxon taxon) {
334 String baseUrl = state.getConfig().getSourceLinkBaseUrl();
335 if (!baseUrl.endsWith("/")) {
336 baseUrl += "/";
337 }
338 String result = baseUrl + "cdm_dataportal/taxon/" + taxon.getUuid() ;
339 return result;
340 }
341
342 private String makeSynonymSourceLink(WfoBackboneExportState state, Synonym synonym) {
343 String baseUrl = state.getConfig().getSourceLinkBaseUrl();
344 if (!baseUrl.endsWith("/")) {
345 baseUrl += "/";
346 }
347 String result = baseUrl + "cdm_dataportal/taxon/" +
348 synonym.getAcceptedTaxon().getUuid()
349 + "/synonymy?highlite=" + synonym.getUuid();
350 return result;
351 }
352
353 private void handleSynonyms(WfoBackboneExportState state, Taxon taxon) {
354
355 if (!state.getConfig().isDoSynonyms()) {
356 return;
357 }
358
359 //homotypic group / synonyms
360 HomotypicalGroup homotypicGroup = taxon.getHomotypicGroup();
361 handleHomotypicalGroup(state, homotypicGroup, taxon);
362 for (Synonym syn : taxon.getSynonymsInGroup(homotypicGroup)) {
363 handleSynonym(state, syn, true);
364 }
365
366 List<HomotypicalGroup> heterotypicHomotypicGroups = taxon.getHeterotypicSynonymyGroups();
367 for (HomotypicalGroup group: heterotypicHomotypicGroups){
368 handleHomotypicalGroup(state, group, taxon);
369 for (Synonym syn : taxon.getSynonymsInGroup(group)) {
370 handleSynonym(state, syn, false);
371 }
372 }
373 }
374
375 private boolean isUrl(String url) {
376 try {
377 if (url.startsWith("http")) {
378 URI.fromString(url);
379 return true;
380 }
381 } catch (Exception e) {
382 //exception should return false
383 }
384 return false;
385 }
386
387 private String toIsoDate(TimePeriod mediaCreated) {
388 //TODO 2 date, what if end or freetext exist?
389 Partial partial = mediaCreated.getStart();
390 if (partial == null || !partial.isSupported(DateTimeFieldType.year())
391 || !partial.isSupported(DateTimeFieldType.monthOfYear()) && partial.isSupported(DateTimeFieldType.dayOfMonth())) {
392 //TODO 2 date, log warning, also if mediaCreated.getEnd() != null or so
393 return null;
394 } else {
395 DateTimeFormatter formatter = new DateTimeFormatterBuilder()
396 .appendYear(4, 4).appendLiteral('-')
397 .appendMonthOfYear(2).appendLiteral('-')
398 .appendDayOfMonth(2)
399 .toFormatter();
400 return partial.toString(formatter);
401 }
402 }
403
404 /**
405 * transforms the given date to an iso date
406 */
407 protected String toIsoDate(DateTime dateTime) {
408 if (dateTime == null) {
409 return null;
410 }
411 DateTimeFormatter formatter = new DateTimeFormatterBuilder()
412 .appendYear(4, 4).appendLiteral('-')
413 .appendMonthOfYear(2).appendLiteral('-')
414 .appendDayOfMonth(2)
415 .toFormatter();
416 return formatter.print(dateTime);
417 }
418
419 //TODO 4 is remark handling correct?
420 private String getRemarks(WfoBackboneExportState state, TaxonBase<?> taxonBase) {
421
422 String remarks = null;
423
424 Set<UUID> includedAnnotationTypes = new HashSet<>();
425 //TODO 5 make annotation types configurable
426 includedAnnotationTypes.add(AnnotationType.uuidEditorial);
427
428 Set<UUID> includedTaxonFactTypes = new HashSet<>(); //make taxon remark facts configurable
429 includedTaxonFactTypes.add(Feature.uuidNotes);
430 Set<UUID> includedNameFactTypes = null; //TODO 7 make name facts configurable
431
432
433 String nameAnnotations = getAnnotations(state, taxonBase.getName(), includedAnnotationTypes);
434 String nameFacts = getFacts(state, taxonBase.getName(), includedNameFactTypes);
435 String taxonAnnotations = getAnnotations(state, taxonBase, includedAnnotationTypes);
436 String taxonFacts = !taxonBase.isInstanceOf(Taxon.class)? null :
437 getFacts(state, CdmBase.deproxy(taxonBase, Taxon.class), includedTaxonFactTypes);
438
439 remarks = CdmUtils.concat("; ", nameAnnotations, nameFacts, taxonAnnotations, taxonFacts);
440
441 return remarks;
442 }
443
444 //TODO 4 move to a more general place for reusing and/or make it more performant
445 private String getFacts(@SuppressWarnings("unused") WfoBackboneExportState state,
446 IDescribable<?> entity,
447 Set<UUID> includedFeatures) {
448
449 if (entity == null) {
450 return null;
451 }
452 String result = null;
453 for (DescriptionBase<?> db : entity.getDescriptions()) {
454 if (db.isPublish()) {
455 for(DescriptionElementBase deb : db.getElements()){
456 UUID featureUuid = deb.getFeature()==null ? null: deb.getFeature().getUuid();
457 if (includedFeatures == null ||
458 includedFeatures.contains(featureUuid)){
459 //TODO 9 other fact types
460 if (deb.isInstanceOf(TextData.class)) {
461 TextData td = CdmBase.deproxy(deb, TextData.class);
462 //TODO 7 handle locale
463 LanguageString text = td.getPreferredLanguageString(Language.DEFAULT());
464 if (text != null) {
465 result = CdmUtils.concat(";", result, text.getText());
466 }
467 }
468 }
469 }
470 }
471 }
472 return result;
473 }
474
475 private String getAnnotations(@SuppressWarnings("unused") WfoBackboneExportState state,
476 AnnotatableEntity entity,
477 Set<UUID> includedAnnotationTypes) {
478
479 if (entity == null) {
480 return null;
481 }
482 String result = null;
483 for (Annotation a : entity.getAnnotations()) {
484 UUID typeUuid = a.getAnnotationType()==null ? null: a.getAnnotationType().getUuid();
485 if (includedAnnotationTypes == null ||
486 includedAnnotationTypes.contains(typeUuid)){
487 result = CdmUtils.concat(";", result, a.getText());
488 }
489 }
490 return result;
491 }
492
493 private String createMultilanguageString(Map<Language, LanguageString> multilanguageText) {
494 String text = "";
495 int index = multilanguageText.size();
496 for (LanguageString langString : multilanguageText.values()) {
497 text += langString.getText();
498 if (index > 1) {
499 text += "; ";
500 }
501 index--;
502 }
503 return text;
504 }
505
506 private String createAnnotationsString(Set<Annotation> annotations) {
507 StringBuffer strBuff = new StringBuffer();
508
509 for (Annotation ann : annotations) {
510 if (ann.getAnnotationType() == null || !ann.getAnnotationType().equals(AnnotationType.TECHNICAL())) {
511 strBuff.append(ann.getText());
512 strBuff.append("; ");
513 }
514 }
515
516 if (strBuff.length() > 2) {
517 return strBuff.substring(0, strBuff.length() - 2);
518 } else {
519 return null;
520 }
521 }
522
523 private String getId(WfoBackboneExportState state, ICdmBase cdmBase) {
524 if (cdmBase == null) {
525 return "";
526 }
527 // TODO 4 id type, make configurable
528 return cdmBase.getUuid().toString();
529 }
530
531 private void handleSynonym(WfoBackboneExportState state, Synonym synonym,
532 boolean isHomotypic) {
533 try {
534 if (isUnpublished(state.getConfig(), synonym)) {
535 return;
536 }
537
538 WfoBackboneExportTable table = WfoBackboneExportTable.CLASSIFICATION;
539 String[] csvLine = new String[table.getSize()];
540
541 TaxonName name = synonym.getName();
542
543 //accepted name id
544 String acceptedWfoId = null;
545 if (synonym.getAcceptedTaxon()!= null && synonym.getAcceptedTaxon().getName() != null) {
546 TaxonName acceptedName = synonym.getAcceptedTaxon().getName();
547 acceptedWfoId = getWfoId(state, acceptedName, false);
548 if (acceptedWfoId == null) {
549 String message = "WFO-ID for accepted name is missing. This should not happen. Synonym: " + name.getTitleCache() + "; Accepted name: " + acceptedName.getTitleCache();
550 state.getResult().addError(message, "handleName");
551 state.getResult().setState(ExportResultState.INCOMPLETE_WITH_ERROR);
552 }
553 csvLine[table.getIndex(WfoBackboneExportTable.TAX_ACCEPTED_NAME_ID)] = acceptedWfoId;
554 }
555
556 String wfoId = handleName(state, table, csvLine, name, acceptedWfoId);
557 if (wfoId == null) {
558 return;
559 }
560
561
562 //status
563 csvLine[table.getIndex(WfoBackboneExportTable.TAX_STATUS)] = isHomotypic ? "homotypicSynonym" : "heterotypicSynonym";
564
565 //TODO 5 URL to taxon, take it from a repository information (currently not yet possible, but maybe we could use a CDM preference instead)
566 if (isNotBlank(state.getConfig().getSourceLinkBaseUrl())) {
567 String taxonSourceLink = makeSynonymSourceLink(state, synonym);
568 csvLine[table.getIndex(WfoBackboneExportTable.REFERENCES)] = taxonSourceLink;
569 }
570
571 handleTaxonBase(state, table, csvLine, synonym);
572
573 state.getProcessor().put(table, synonym, csvLine);
574 } catch (Exception e) {
575 state.getResult().addException(e, "An unexpected error occurred when handling synonym "
576 + cdmBaseStr(synonym) + ": " + e.getMessage());
577 }
578 }
579
580 private String handleName(WfoBackboneExportState state, WfoBackboneExportTable table, String[] csvLine,
581 TaxonName name, String acceptedNameWfoId) {
582
583 name = CdmBase.deproxy(name);
584 if (name == null || state.getNameStore().containsKey(name.getId())) {
585 if (name == null) {
586 state.getResult().addError("No name was given for taxon.", "handleName");
587 state.getResult().setState(ExportResultState.INCOMPLETE_WITH_ERROR);
588 }
589 return null;
590 }
591 String wfoId = null;
592 try {
593
594 state.getNameStore().put(name.getId(), name.getUuid());
595
596 //taxon ID
597 wfoId = getWfoId(state, name, false);
598 if (isBlank(wfoId)) {
599 String message = "No WFO-ID given for taxon name " + name.getTitleCache() + ". Taxon/Synonym/Name ignored.";
600 state.getResult().addError(message);
601 state.getResult().setState(ExportResultState.INCOMPLETE_WITH_ERROR);
602 return null;
603 }else {
604 csvLine[table.getIndex(WfoBackboneExportTable.TAXON_ID)] = wfoId;
605 }
606
607 boolean warnIfNotExists = false;
608 csvLine[table.getIndex(WfoBackboneExportTable.NAME_SCIENTIFIC_NAME_ID)] = getIpniId(state, name, warnIfNotExists);
609
610 //localID
611 csvLine[table.getIndex(WfoBackboneExportTable.NAME_LOCAL_ID)] = getId(state, name);
612
613 //scientificName
614 if (name.isProtectedTitleCache()) {
615 //TODO 7 make it configurable if we should always take titleCache if titleCache is protected, as nameCache may not necessarily
616 // have complete data if titleCache is protected as it is considered to be irrelevant or at least preliminary
617 String message = "";
618 if (StringUtils.isNotEmpty(name.getNameCache())) {
619 csvLine[table.getIndex(WfoBackboneExportTable.NAME_SCIENTIFIC_NAME)] = name.getNameCache();
620 message = "ScientificName: Name cache " + name.getNameCache() + " used for name with protected titleCache " + name.getTitleCache();
621 }else {
622 csvLine[table.getIndex(WfoBackboneExportTable.NAME_SCIENTIFIC_NAME)] = name.getTitleCache();
623 message = "ScientificName: Name has protected titleCache and no explicit nameCache: " + name.getTitleCache();
624 }
625 state.getResult().addWarning(message); //TODO 7 add location to warning
626 } else {
627 csvLine[table.getIndex(WfoBackboneExportTable.NAME_SCIENTIFIC_NAME)] = name.getNameCache();
628 }
629
630 //rank
631 Rank rank = name.getRank();
632 String rankStr = state.getTransformer().getCacheByRank(rank);
633 if (rankStr == null) {
634 String message = rank == null ? "No rank" : ("Rank not supported by WFO:" + rank.getLabel())
635 + "Taxon not handled in export: " + name.getTitleCache();
636 state.getResult().addWarning(message); //TODO 2 warning sufficient for missing rank? + location
637 return wfoId;
638 }
639 csvLine[table.getIndex(WfoBackboneExportTable.RANK)] = rankStr;
640
641 //authorship
642 //TODO 3 handle empty authorship cache warning
643 csvLine[table.getIndex(WfoBackboneExportTable.NAME_AUTHORSHIP)]
644 = normalizedAuthor(state, name);
645
646 //family (use familystr if provided, otherwise try to compute from the family taxon
647 String familyStr = state.getFamilyStr();
648 if (StringUtils.isBlank(familyStr)){
649 if (Rank.FAMILY().equals(name.getRank())){
650 familyStr = name.getNameCache();
651 }
652 if (StringUtils.isNotBlank(familyStr)) {
653 state.setFamilyStr(familyStr);
654 }else {
655 String message = "Obligatory family information is missing";
656 state.getResult().addWarning(message);
657 }
658 }
659 csvLine[table.getIndex(WfoBackboneExportTable.TAX_FAMILY)] = state.getFamilyStr();
660
661 //name parts
662 csvLine[table.getIndex(WfoBackboneExportTable.TAX_GENUS)] = name.isSupraGeneric()? null : name.getGenusOrUninomial();
663 csvLine[table.getIndex(WfoBackboneExportTable.NAME_SPECIFIC_EPITHET)] = name.getSpecificEpithet();
664 csvLine[table.getIndex(WfoBackboneExportTable.NAME_INFRASPECIFIC_EPITHET)] = name.getInfraSpecificEpithet();
665
666 //TODO 3 verbatimTaxonRank, is this needed at all?
667 csvLine[table.getIndex(WfoBackboneExportTable.NAME_VERBATIM_RANK)] = rankStr;
668
669 //name status
670 csvLine[table.getIndex(WfoBackboneExportTable.NAME_STATUS)] = makeNameStatus(state, name);
671
672 //nom. ref
673 String nomRef = NomenclaturalSourceFormatter.INSTANCE().format(name.getNomenclaturalSource());
674 csvLine[table.getIndex(WfoBackboneExportTable.NAME_PUBLISHED_IN)] = nomRef;
675
676 //originalNameID
677 TaxonName originalName = name.getBasionym(); //TODO 5 basionym, order in case there are >1 basionyms
678 if (originalName == null) {
679 originalName = name.getReplacedSynonyms().stream().findFirst().orElse(null);
680 }
681
682 if (originalName != null) {
683 if (!state.getNameStore().containsKey(originalName.getId())) {
684 //TODO 2 handle basionym is in file assertion
685 }
686 String basionymId = getWfoId(state, originalName, false);
687 csvLine[table.getIndex(WfoBackboneExportTable.NAME_ORIGINAL_NAME_ID)] = basionymId;
688 }
689
690 //original spelling
691 TaxonName originalSpelling = name.getOriginalSpelling();
692 acceptedNameWfoId = acceptedNameWfoId == null? wfoId : acceptedNameWfoId;
693 if (originalSpelling != null) {
694 handleOrthographicVariants(state, table, originalSpelling, name, acceptedNameWfoId);
695 }
696
697 //orth. var.
698 Set<TaxonName> orthVars = getOrthographicVariants(name);
699 for (TaxonName orthVar : orthVars) {
700 handleOrthographicVariants(state, table, orthVar, name, acceptedNameWfoId);
701 }
702
703 } catch (Exception e) {
704 state.getResult().addException(e,
705 "An unexpected error occurred when handling the name " + cdmBaseStr(name) + ": " + name.getTitleCache() + ": " + e.getMessage());
706 e.printStackTrace();
707 }
708
709 return wfoId;
710 }
711
712 private String normalizedAuthor(WfoBackboneExportState state, TaxonName name) {
713 if (name == null) {
714 return null;
715 } else if (state.getConfig().isNormalizeAuthorsToIpniStandard()) {
716 return TeamDefaultCacheStrategy.removeWhitespaces(name.getAuthorshipCache());
717 } else {
718 String result = name.getAuthorshipCache();
719 if (result == null) {
720 return null;
721 }else {
722 return result.replaceAll("\\s+", " ").trim();
723 }
724 }
725 }
726
727 private Set<TaxonName> getOrthographicVariants(TaxonName name) {
728 Set<TaxonName> result = new HashSet<>();
729 Set<NameRelationship> rels = name.getRelationsToThisName();
730 for(NameRelationship rel : rels) {
731 //TODO 2 handle also other type and other direction for orth. var.
732 if(rel.getType().getUuid().equals(NameRelationshipType.uuidOrthographicVariant)) {
733 result.add(rel.getFromName());
734 }
735 }
736 return result;
737 }
738
739 /**
740 * Handle names not being handled via taxonbase.
741 * @param acceptedNameWfoId
742 */
743 private void handleOrthographicVariants(WfoBackboneExportState state, WfoBackboneExportTable table,
744 TaxonName name, TaxonName mainName, String acceptedNameWfoId) {
745
746 //TODO 1 names only check if implemented correctly
747 if (!name.getTaxonBases().isEmpty()) {
748 //TODO 2 find a better way to guarantee that the name is not added as a taxonbase elsewhere
749 return;
750 }
751
752 String[] csvLine = new String[table.getSize()];
753 String wfoID = handleName(state, table, csvLine, name, acceptedNameWfoId);
754 if (wfoID == null) {
755 String message = "Original spelling, orthographic variant or misspelling "
756 + "'" + name + "' for name '" + mainName +"' does not have a WFO-ID"
757 + " and therefore can not be exported";
758 state.getResult().addWarning(message);
759 return;
760 }
761
762 csvLine[table.getIndex(WfoBackboneExportTable.TAX_ACCEPTED_NAME_ID)] = acceptedNameWfoId;
763
764 //authorship, take from mainname if it does not exist
765 //TODO 3 take from csvLine of both names
766 if (isBlank(normalizedAuthor(state, name))) {
767 csvLine[table.getIndex(WfoBackboneExportTable.NAME_AUTHORSHIP)]
768 = normalizedAuthor(state, mainName);
769 }
770
771 //nom. ref, take from main name if it does not exist
772 //TODO 3 take from csvLine of both names
773 String nomRef = NomenclaturalSourceFormatter.INSTANCE().format(name.getNomenclaturalSource());
774 if (isBlank(nomRef)) {
775 nomRef = NomenclaturalSourceFormatter.INSTANCE().format(mainName.getNomenclaturalSource());
776 csvLine[table.getIndex(WfoBackboneExportTable.NAME_PUBLISHED_IN)] = nomRef;
777 }
778
779 //TODO 2 tax status correct?
780 csvLine[table.getIndex(WfoBackboneExportTable.TAX_STATUS)] = "Synonym";
781
782 csvLine[table.getIndex(WfoBackboneExportTable.NAME_STATUS)] = "orthografia";
783
784 //TODO 2 remarks, REFERENCES, family, taxonBase, created, modified
785
786 //process original spelling
787 state.getProcessor().put(table, name, csvLine); // TODO Auto-generated method stub
788
789 }
790
791 private String getWfoId(WfoBackboneExportState state, TaxonName name, boolean warnIfNotExists) {
792 Identifier wfoId = name.getIdentifier(IdentifierType.uuidWfoNameIdentifier);
793 if (wfoId == null && warnIfNotExists) {
794 String message = "No wfo-id given for name: " + name.getTitleCache()+"/"+ name.getUuid();
795 state.getResult().addWarning(message); //TODO 5 data location
796 }
797 return wfoId == null ? null : wfoId.getIdentifier();
798 }
799
800 private String getIpniId(WfoBackboneExportState state, TaxonName name, boolean warnIfNotExists) {
801 Identifier ipniId = name.getIdentifier(IdentifierType.uuidIpniNameIdentifier);
802 if (ipniId == null && warnIfNotExists) {
803 String message = "No ipni-id given for name: " + name.getTitleCache()+"/"+ name.getUuid();
804 state.getResult().addWarning(message); //TODO 5 data location
805 }
806 return ipniId == null ? null : ipniId.getIdentifier();
807 }
808
809 private String makeNameStatus(WfoBackboneExportState state, TaxonName name) {
810 try {
811
812 //TODO 2 what is with dubium
813 if (name.isLegitimate()) {
814 if (name.isConserved()) {
815 return "Conserved";
816 }else {
817 return "Valid";
818 }
819 } else if (name.isRejected()) {
820 return "Rejected";
821 } else if (name.isIllegitimate()) {
822 return "Illegitimate";
823 } else if (name.isInvalid()) {
824 if (name.isOrthographicVariant()) {
825 return "orthografia";
826 }else {
827 return "invalid";
828 }
829 } else {
830 String message = "Unhandled name status case for name: " + name.getTitleCache() +
831 ". Status not handled correctly.";
832 state.getResult().addWarning(message);
833 return "undefined";
834 }
835 } catch (Exception e) {
836 state.getResult().addException(e, "An unexpected error occurred when extracting status string for "
837 + cdmBaseStr(name) + ": " + e.getMessage());
838 return "";
839 }
840 }
841
842 private void handleHomotypicalGroup(WfoBackboneExportState state, HomotypicalGroup group, Taxon acceptedTaxon) {
843 try {
844
845 List<TaxonName> typifiedNames = new ArrayList<>();
846 if (acceptedTaxon != null){
847 List<Synonym> synonymsInGroup = acceptedTaxon.getSynonymsInGroup(group);
848 if (group.equals(acceptedTaxon.getHomotypicGroup())){
849 typifiedNames.add(acceptedTaxon.getName());
850 }
851 synonymsInGroup.stream().forEach(synonym -> typifiedNames.add(CdmBase.deproxy(synonym.getName())));
852 }
853
854
855 TaxonName firstname = null;
856 for (TaxonName name: typifiedNames){
857 Iterator<Taxon> taxa = name.getTaxa().iterator();
858 while(taxa.hasNext()){
859 Taxon taxon = taxa.next();
860 if(!(taxon.isMisapplication() || taxon.isProparteSynonym())){
861 firstname = name;
862 break;
863 }
864 }
865 }
866
867 } catch (Exception e) {
868 state.getResult().addException(e, "An unexpected error occurred when handling homotypic group "
869 + cdmBaseStr(group) + ": " + e.getMessage());
870 }
871 }
872
873 private void handleReference(WfoBackboneExportState state, OriginalSourceBase source) {
874
875 if (source == null || source.getCitation() == null) {
876 return;
877 }
878 Reference reference = source.getCitation();
879
880 //TODO 5 allow handle sec detail in ref table (if allowed by the export guide)
881 // this is also important for adding external links which are source specific (see below)
882 try {
883 if (state.getReferenceStore().contains(reference.getUuid())) {
884 return;
885 }
886 reference = CdmBase.deproxy(reference);
887
888 state.addReferenceToStore(reference);
889 WfoBackboneExportTable table = WfoBackboneExportTable.REFERENCE;
890 String[] csvLine = new String[table.getSize()];
891
892 csvLine[table.getIndex(WfoBackboneExportTable.IDENTIFIER)] = getId(state, reference);
893
894 //TODO 2 correct?, ref biblio citation
895 csvLine[table.getIndex(WfoBackboneExportTable.REF_BIBLIO_CITATION)] = reference.getCitation();
896
897 //ref uri TODO 6 make ref URIs configurable
898 if (reference.getDoi() != null) {
899 csvLine[table.getIndex(WfoBackboneExportTable.REF_URI)] = reference.getDoiString();
900 }else if (reference.getUri() != null) {
901 csvLine[table.getIndex(WfoBackboneExportTable.REF_URI)] = reference.getUri().toString();
902 }else{
903 //TODO 5 not yet added as it is source specific but uuid is reference specific
904 // String uri = null;
905 // for (ExternalLink link : source.getLinks()) {
906 // uri = CdmUtils.concat("; ", uri, link.getUri() == null ? null : link.getUri().toString());
907 // }
908 // csvLine[table.getIndex(WfoBackboneExportTable.REF_URI)] = uri;
909 }
910
911 state.getProcessor().put(table, reference, csvLine);
912 } catch (Exception e) {
913 e.printStackTrace();
914 state.getResult().addException(e, "An unexpected error occurred when handling reference "
915 + cdmBaseStr(reference) + ": " + e.getMessage());
916 }
917 }
918
919 /**
920 * Returns a string representation of the {@link CdmBase cdmBase} object for
921 * result messages.
922 */
923 private String cdmBaseStr(CdmBase cdmBase) {
924 if (cdmBase == null) {
925 return "-no object available-";
926 } else {
927 return cdmBase.getClass().getSimpleName() + ": " + cdmBase.getUuid();
928 }
929 }
930
931 @Override
932 protected boolean doCheck(WfoBackboneExportState state) {
933 return false;
934 }
935
936 @Override
937 protected boolean isIgnore(WfoBackboneExportState state) {
938 return false;
939 }
940 }