ref #8162 adapt cdmlip to new term package structure
[cdmlib.git] / cdmlib-model / src / main / java / eu / etaxonomy / cdm / model / taxon / TaxonNode.java
1 /**
2 * Copyright (C) 2007 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
10 package eu.etaxonomy.cdm.model.taxon;
11
12 import java.util.ArrayList;
13 import java.util.HashMap;
14 import java.util.HashSet;
15 import java.util.List;
16 import java.util.Map;
17 import java.util.Set;
18
19 import javax.persistence.Column;
20 import javax.persistence.Entity;
21 import javax.persistence.FetchType;
22 import javax.persistence.JoinTable;
23 import javax.persistence.ManyToOne;
24 import javax.persistence.MapKeyJoinColumn;
25 import javax.persistence.OneToMany;
26 import javax.persistence.OrderBy;
27 import javax.persistence.OrderColumn;
28 import javax.persistence.Table;
29 import javax.persistence.Transient;
30 import javax.xml.bind.annotation.XmlAccessType;
31 import javax.xml.bind.annotation.XmlAccessorType;
32 import javax.xml.bind.annotation.XmlAttribute;
33 import javax.xml.bind.annotation.XmlElement;
34 import javax.xml.bind.annotation.XmlElementWrapper;
35 import javax.xml.bind.annotation.XmlIDREF;
36 import javax.xml.bind.annotation.XmlRootElement;
37 import javax.xml.bind.annotation.XmlSchemaType;
38 import javax.xml.bind.annotation.XmlType;
39 import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
40
41 import org.apache.log4j.Logger;
42 import org.hibernate.LazyInitializationException;
43 import org.hibernate.annotations.Cascade;
44 import org.hibernate.annotations.CascadeType;
45 import org.hibernate.envers.Audited;
46 import org.hibernate.search.annotations.Analyze;
47 import org.hibernate.search.annotations.ContainedIn;
48 import org.hibernate.search.annotations.Field;
49 import org.hibernate.search.annotations.Index;
50 import org.hibernate.search.annotations.IndexedEmbedded;
51 import org.hibernate.search.annotations.Store;
52
53 import eu.etaxonomy.cdm.hibernate.HHH_9751_Util;
54 import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
55 import eu.etaxonomy.cdm.jaxb.MultilanguageTextAdapter;
56 import eu.etaxonomy.cdm.model.agent.TeamOrPersonBase;
57 import eu.etaxonomy.cdm.model.common.AnnotatableEntity;
58 import eu.etaxonomy.cdm.model.common.CdmBase;
59 import eu.etaxonomy.cdm.model.common.ITreeNode;
60 import eu.etaxonomy.cdm.model.common.Language;
61 import eu.etaxonomy.cdm.model.common.LanguageString;
62 import eu.etaxonomy.cdm.model.common.MultilanguageText;
63 import eu.etaxonomy.cdm.model.name.Rank;
64 import eu.etaxonomy.cdm.model.name.TaxonName;
65 import eu.etaxonomy.cdm.model.reference.Reference;
66 import eu.etaxonomy.cdm.model.term.DefinedTerm;
67 import eu.etaxonomy.cdm.validation.Level3;
68 import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustBeLowerRankThanParent;
69 import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustDeriveNameFromParent;
70 import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustNotSkipRanks;
71
72 /**
73 * @author a.mueller
74 * @since 31.03.2009
75 */
76 @XmlAccessorType(XmlAccessType.FIELD)
77 @XmlType(name = "TaxonNode", propOrder = {
78 "classification",
79 "taxon",
80 "parent",
81 "treeIndex",
82 "sortIndex",
83 "childNodes",
84 "referenceForParentChildRelation",
85 "microReferenceForParentChildRelation",
86 "countChildren",
87 "agentRelations",
88 "synonymToBeUsed",
89 "excludedNote"
90 })
91 @XmlRootElement(name = "TaxonNode")
92 @Entity
93 //@Indexed disabled to reduce clutter in indexes, since this type is not used by any search
94 //@Indexed(index = "eu.etaxonomy.cdm.model.taxon.TaxonNode")
95 @Audited
96 @Table(name="TaxonNode", indexes = { @javax.persistence.Index(name = "taxonNodeTreeIndex", columnList = "treeIndex") })
97 @ChildTaxaMustBeLowerRankThanParent(groups = Level3.class)
98 @ChildTaxaMustNotSkipRanks(groups = Level3.class)
99 @ChildTaxaMustDeriveNameFromParent(groups = Level3.class)
100 public class TaxonNode
101 extends AnnotatableEntity
102 implements ITaxonTreeNode, ITreeNode<TaxonNode>, Cloneable{
103
104 private static final long serialVersionUID = -4743289894926587693L;
105 private static final Logger logger = Logger.getLogger(TaxonNode.class);
106
107 @XmlElement(name = "taxon")
108 @XmlIDREF
109 @XmlSchemaType(name = "IDREF")
110 @ManyToOne(fetch = FetchType.LAZY)
111 @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
112 @ContainedIn
113 private Taxon taxon;
114
115
116 @XmlElement(name = "parent")
117 @XmlIDREF
118 @XmlSchemaType(name = "IDREF")
119 @ManyToOne(fetch = FetchType.LAZY)
120 @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
121 private TaxonNode parent;
122
123
124 @XmlElement(name = "treeIndex")
125 @Column(length=255)
126 @Field(store = Store.YES, index = Index.YES, analyze = Analyze.NO)
127 private String treeIndex;
128
129
130 @XmlElement(name = "classification")
131 @XmlIDREF
132 @XmlSchemaType(name = "IDREF")
133 @ManyToOne(fetch = FetchType.LAZY)
134 @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
135 // TODO @NotNull // avoids creating a UNIQUE key for this field
136 @IndexedEmbedded(includeEmbeddedObjectId=true)
137 private Classification classification;
138
139 @XmlElementWrapper(name = "childNodes")
140 @XmlElement(name = "childNode")
141 @XmlIDREF
142 @XmlSchemaType(name = "IDREF")
143 //see https://dev.e-taxonomy.eu/trac/ticket/3722
144 @OrderColumn(name="sortIndex")
145 @OrderBy("sortIndex")
146 @OneToMany(mappedBy="parent", fetch=FetchType.LAZY)
147 //@Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
148 private List<TaxonNode> childNodes = new ArrayList<>();
149
150 //see https://dev.e-taxonomy.eu/trac/ticket/3722
151 //see https://dev.e-taxonomy.eu/trac/ticket/4200
152 private Integer sortIndex = -1;
153
154 @XmlElement(name = "reference")
155 @XmlIDREF
156 @XmlSchemaType(name = "IDREF")
157 @ManyToOne(fetch = FetchType.LAZY)
158 @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
159 private Reference referenceForParentChildRelation;
160
161 @XmlElement(name = "microReference")
162 private String microReferenceForParentChildRelation;
163
164 @XmlElement(name = "countChildren")
165 private int countChildren;
166
167 @XmlElementWrapper(name = "agentRelations")
168 @XmlElement(name = "agentRelation")
169 @XmlIDREF
170 @XmlSchemaType(name = "IDREF")
171 @OneToMany(mappedBy="taxonNode", fetch=FetchType.LAZY)
172 @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
173 private Set<TaxonNodeAgentRelation> agentRelations = new HashSet<>();
174
175 @XmlAttribute(name= "unplaced")
176 private boolean unplaced = false;
177 public boolean isUnplaced() {return unplaced;}
178 public void setUnplaced(boolean unplaced) {this.unplaced = unplaced;}
179
180 @XmlAttribute(name= "excluded")
181 private boolean excluded = false;
182 public boolean isExcluded() {return excluded;}
183 public void setExcluded(boolean excluded) {this.excluded = excluded;}
184
185 @XmlElement(name = "excludedNote")
186 @XmlJavaTypeAdapter(MultilanguageTextAdapter.class)
187 @OneToMany(fetch = FetchType.LAZY, orphanRemoval=true)
188 @MapKeyJoinColumn(name="excludedNote_mapkey_id")
189 @JoinTable(name = "TaxonNode_ExcludedNote") //to make possible to add also unplacedNote
190 @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE, CascadeType.DELETE})
191 private Map<Language,LanguageString> excludedNote = new HashMap<>();
192
193 // private Taxon originalConcept;
194 // //or
195 @XmlElement(name = "synonymToBeUsed")
196 @XmlIDREF
197 @XmlSchemaType(name = "IDREF")
198 @ManyToOne(fetch = FetchType.LAZY)
199 @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
200 private Synonym synonymToBeUsed;
201
202 // ******************** CONSTRUCTOR **********************************************/
203
204 protected TaxonNode(){super();}
205
206 /**
207 * to create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}
208 * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}
209 * @param taxon
210 * @param classification
211 * @deprecated setting of classification is handled in the addTaxonNode() method,
212 * use TaxonNode(taxon) instead
213 */
214 @Deprecated
215 protected TaxonNode (Taxon taxon, Classification classification){
216 this(taxon);
217 setClassification(classification);
218 }
219
220 /**
221 * to create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}
222 * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}
223 *
224 * @param taxon
225 */
226 protected TaxonNode(Taxon taxon){
227 setTaxon(taxon);
228 }
229
230 // ************************* GETTER / SETTER *******************************/
231
232 @Transient
233 public Integer getSortIndex() {
234 return sortIndex;
235 }
236 /**
237 * SortIndex shall be handled only internally, therefore not public.
238 * However, as javaassist only supports protected methods it needs to be protected, not private.
239 * Alternatively we could use deproxy on every call of this method (see commented code)
240 * @param i
241 * @return
242 * @deprecated for internal use only
243 */
244 @Deprecated
245 protected void setSortIndex(Integer i) {
246 // CdmBase.deproxy(this, TaxonNode.class).sortIndex = i; //alternative solution for private, DON'T remove
247 sortIndex = i;
248 }
249
250
251 public Taxon getTaxon() {
252 return taxon;
253 }
254 public void setTaxon(Taxon taxon) {
255 this.taxon = taxon;
256 if (taxon != null){
257 taxon.addTaxonNode(this);
258 }
259 }
260
261
262 @Override
263 public List<TaxonNode> getChildNodes() {
264 return childNodes;
265 }
266 protected void setChildNodes(List<TaxonNode> childNodes) {
267 this.childNodes = childNodes;
268 }
269
270
271 public Classification getClassification() {
272 return classification;
273 }
274 /**
275 * THIS METHOD SHOULD NOT BE CALLED!
276 * invisible part of the bidirectional relationship, for public use TaxonomicView.addRoot() or TaxonNode.addChild()
277 * @param classification
278 * @deprecated for internal use only
279 */
280 @Deprecated
281 protected void setClassification(Classification classification) {
282 this.classification = classification;
283 }
284
285
286 @Override
287 public String getMicroReference() {
288 return microReferenceForParentChildRelation;
289 }
290 public void setMicroReference(String microReference) {
291 this.microReferenceForParentChildRelation = microReference;
292 }
293
294
295 @Override
296 public Reference getReference() {
297 return referenceForParentChildRelation;
298 }
299 public void setReference(Reference reference) {
300 this.referenceForParentChildRelation = reference;
301 }
302
303 //countChildren
304 public int getCountChildren() {
305 return countChildren;
306 }
307 /**
308 * @deprecated for internal use only
309 * @param countChildren
310 */
311 @Deprecated
312 protected void setCountChildren(int countChildren) {
313 this.countChildren = countChildren;
314 }
315
316
317 //parent
318 @Override
319 public TaxonNode getParent(){
320 return parent;
321 }
322 /**
323 * Sets the parent of this taxon node.<BR>
324 *
325 * In most cases you would want to call setParentTreeNode(ITreeNode) which
326 * handles updating of the bidirectional relationship
327 *
328 * @see setParentTreeNode(ITreeNode)
329 * @param parent
330 *
331 */
332 protected void setParent(TaxonNode parent) {
333 this.parent = parent;
334 // this.treeIndex = parent.treeIndex() +
335 }
336
337 // *************** Excluded Note ***************
338
339 /**
340 * Returns the {@link MultilanguageText multi-language text} to add a note to the
341 * excluded flag. The different {@link LanguageString language strings}
342 * contained in the multi-language text should all have the same meaning.
343 * @see #getExcludedNote()
344 * @see #putExcludedNote(Language, String)
345 */
346 public Map<Language,LanguageString> getExcludedNote(){
347 return this.excludedNote;
348 }
349
350 /**
351 * Returns the excluded note string in the given {@link Language language}
352 *
353 * @param language the language in which the description string looked for is formulated
354 * @see #getExcludedNote()
355 * @see #putExcludedNote(Language, String)
356 */
357 public String getExcludedNote(Language language){
358 LanguageString languageString = excludedNote.get(language);
359 if (languageString == null){
360 return null;
361 }else{
362 return languageString.getText();
363 }
364 }
365
366 /**
367 * Adds a translated {@link LanguageString text in a particular language}
368 * to the {@link MultilanguageText multilanguage text} used to add a note to
369 * the {@link #isExcluded() excluded} flag.
370 *
371 * @param excludedNote the language string adding a note to the excluded flag
372 * in a particular language
373 * @see #getExcludedNote()
374 * @see #putExcludedNote(String, Language)
375 */
376 public void putExcludedNote(LanguageString excludedNote){
377 this.excludedNote.put(excludedNote.getLanguage(), excludedNote);
378 }
379 /**
380 * Creates a {@link LanguageString language string} based on the given text string
381 * and the given {@link Language language} and adds it to the {@link MultilanguageText
382 * multi-language text} used to annotate the excluded flag.
383 *
384 * @param text the string annotating the excluded flag
385 * in a particular language
386 * @param language the language in which the text string is formulated
387 * @see #getExcludedNote()
388 * @see #putExcludedNote(LanguageString)
389 * @see #removeExcludedNote(Language)
390 */
391 public void putExcludedNote(Language language, String text){
392 this.excludedNote.put(language, LanguageString.NewInstance(text, language));
393 }
394
395 /**
396 * Removes from the {@link MultilanguageText multilanguage text} used to annotate
397 * the excluded flag the one {@link LanguageString language string}
398 * with the given {@link Language language}.
399 *
400 * @param lang the language in which the language string to be removed
401 * has been formulated
402 * @see #getExcludedNote()
403 */
404 public void removeExcludedNote(Language lang){
405 this.excludedNote.remove(lang);
406 }
407
408 // ****************** Agent Relations ****************************/
409
410
411 /**
412 * @return
413 */
414 public Set<TaxonNodeAgentRelation> getAgentRelations() {
415 return this.agentRelations;
416 }
417
418 public TaxonNodeAgentRelation addAgentRelation(DefinedTerm type, TeamOrPersonBase<?> agent){
419 TaxonNodeAgentRelation result = TaxonNodeAgentRelation.NewInstance(this, agent, type);
420 return result;
421 }
422 /**
423 * @param nodeAgentRelation
424 */
425 protected void addAgentRelation(TaxonNodeAgentRelation agentRelation) {
426 agentRelation.setTaxonNode(this);
427 this.agentRelations.add(agentRelation);
428 }
429
430 /**
431 * @param nodeAgentRelation
432 */
433 public void removeNodeAgent(TaxonNodeAgentRelation agentRelation) {
434 agentRelation.setTaxonNode(this);
435 agentRelations.remove(agentRelation);
436 }
437
438 //********************
439
440 //synonymToBeused
441 public Synonym getSynonymToBeUsed() {
442 return synonymToBeUsed;
443 }
444 public void setSynonymToBeUsed(Synonym synonymToBeUsed) {
445 this.synonymToBeUsed = synonymToBeUsed;
446 }
447
448
449 //treeindex
450 @Override
451 public String treeIndex() {
452 return treeIndex;
453 }
454 @Override
455 @Deprecated //for CDM lib internal use only, may be removed in future versions
456 public void setTreeIndex(String treeIndex) {
457 this.treeIndex = treeIndex;
458 }
459 @Override
460 public String treeIndexLike() {
461 return treeIndex + "%";
462 }
463 @Override
464 public String treeIndexWc() {
465 return treeIndex + "*";
466 }
467
468
469
470 //************************ METHODS **************************/
471
472 @Override
473 public TaxonNode addChildTaxon(Taxon taxon, Reference citation, String microCitation) {
474 return addChildTaxon(taxon, this.childNodes.size(), citation, microCitation);
475 }
476
477
478 @Override
479 public TaxonNode addChildTaxon(Taxon taxon, int index, Reference citation, String microCitation) {
480 Classification classification = CdmBase.deproxy(this.getClassification());
481 taxon = HibernateProxyHelper.deproxy(taxon, Taxon.class);
482 if (classification.isTaxonInTree(taxon)){
483 throw new IllegalArgumentException(String.format("Taxon may not be in a classification twice: %s", taxon.getTitleCache()));
484 }
485 return addChildNode(new TaxonNode(taxon), index, citation, microCitation);
486 }
487
488 /**
489 * Moves a taxon node to a new parent. Descendents of the node are moved as well
490 *
491 * @param childNode the taxon node to be moved to the new parent
492 * @return the child node in the state of having a new parent
493 */
494 @Override
495 public TaxonNode addChildNode(TaxonNode childNode, Reference reference, String microReference){
496 addChildNode(childNode, childNodes.size(), reference, microReference);
497 return childNode;
498 }
499
500 /**
501 * Inserts the given taxon node in the list of children of <i>this</i> taxon node
502 * at the given (index + 1) position. If the given index is out of bounds
503 * an exception will arise.<BR>
504 * Due to bidirectionality this method must also assign <i>this</i> taxon node
505 * as the parent of the given child.
506 *
507 * @param child the taxon node to be added
508 * @param index the integer indicating the position at which the child
509 * should be added
510 * @see #getChildNodes()
511 * @see #addChildNode(TaxonNode, Reference, String, Synonym)
512 * @see #deleteChildNode(TaxonNode)
513 * @see #deleteChildNode(int)
514 */
515 @Override
516 public TaxonNode addChildNode(TaxonNode child, int index, Reference reference, String microReference){
517 if (index < 0 || index > childNodes.size() + 1){
518 throw new IndexOutOfBoundsException("Wrong index: " + index);
519 }
520 // check if this node is a descendant of the childNode
521 if(child.getParent() != this && child.isAncestor(this)){
522 throw new IllegalAncestryException("New parent node is a descendant of the node to be moved.");
523 }
524
525 child.setParentTreeNode(this, index);
526
527 child.setReference(reference);
528 child.setMicroReference(microReference);
529
530 return child;
531 }
532
533 /**
534 * Sets this nodes classification. Updates classification of child nodes recursively.
535 *
536 * If the former and the actual tree are equal() this method does nothing.
537 *
538 * @throws IllegalArgumentException if newClassifciation is null
539 *
540 * @param newClassification
541 */
542 @Transient
543 private void setClassificationRecursively(Classification newClassification) {
544 if (newClassification == null){
545 throw new IllegalArgumentException("New Classification must not be 'null' when setting new classification.");
546 }
547 if(! newClassification.equals(this.getClassification())){
548 this.setClassification(newClassification);
549 for(TaxonNode childNode : this.getChildNodes()){
550 childNode.setClassificationRecursively(newClassification);
551 }
552 }
553 }
554
555 @Override
556 public boolean deleteChildNode(TaxonNode node) {
557 boolean result = removeChildNode(node);
558 Taxon taxon = HibernateProxyHelper.deproxy(node.getTaxon(), Taxon.class);
559 node = HibernateProxyHelper.deproxy(node, TaxonNode.class);
560 node.setTaxon(null);
561
562
563 ArrayList<TaxonNode> childNodes = new ArrayList<TaxonNode>(node.getChildNodes());
564 for(TaxonNode childNode : childNodes){
565 HibernateProxyHelper.deproxy(childNode, TaxonNode.class);
566 node.deleteChildNode(childNode);
567 }
568 taxon.removeTaxonNode(node);
569 return result;
570 }
571
572 /**
573 * Deletes the child node and also removes children of childnode
574 * recursively if delete children is <code>true</code>
575 * @param node
576 * @param deleteChildren
577 * @return
578 */
579 public boolean deleteChildNode(TaxonNode node, boolean deleteChildren) {
580 boolean result = removeChildNode(node);
581 Taxon taxon = node.getTaxon();
582 node.setTaxon(null);
583 taxon.removeTaxonNode(node);
584 if (deleteChildren){
585 ArrayList<TaxonNode> childNodes = new ArrayList<TaxonNode>(node.getChildNodes());
586 for(TaxonNode childNode : childNodes){
587 node.deleteChildNode(childNode, deleteChildren);
588 }
589 } else{
590 ArrayList<TaxonNode> childNodes = new ArrayList<TaxonNode>(node.getChildNodes());
591 for(TaxonNode childNode : childNodes){
592 this.addChildNode(childNode, null, null);
593 }
594 }
595
596 return result;
597 }
598
599 /**
600 * Removes the child node from this node. Sets the parent and the classification of the child
601 * node to null
602 *
603 * @param childNode
604 * @return
605 */
606 protected boolean removeChildNode(TaxonNode childNode){
607 boolean result = true;
608 //removeNullValueFromChildren();
609 if(childNode == null){
610 throw new IllegalArgumentException("TaxonNode may not be null");
611 }
612 int index = childNodes.indexOf(childNode);
613 if (index >= 0){
614 removeChild(index);
615 } else {
616 result = false;
617 }
618 return result;
619 }
620
621 /**
622 * Removes the child node placed at the given (index + 1) position
623 * from the list of {@link #getChildNodes() children} of <i>this</i> taxon node.
624 * Sets the parent and the classification of the child
625 * node to null.
626 * If the given index is out of bounds no child will be removed.
627 *
628 * @param index the integer indicating the position of the taxon node to
629 * be removed
630 * @see #getChildNodes()
631 * @see #addChildNode(TaxonNode, Reference, String)
632 * @see #addChildNode(TaxonNode, int, Reference, String)
633 * @see #deleteChildNode(TaxonNode)
634 */
635 public void removeChild(int index){
636 //TODO: Only as a workaround. We have to find out why merge creates null entries.
637
638 TaxonNode child = childNodes.get(index);
639 child = HibernateProxyHelper.deproxy(child, TaxonNode.class); //strange that this is required, but otherwise child.getParent() returns null for some lazy-loaded items.
640
641 if (child != null){
642
643 TaxonNode parent = HibernateProxyHelper.deproxy(child.getParent(), TaxonNode.class);
644 TaxonNode thisNode = HibernateProxyHelper.deproxy(this, TaxonNode.class);
645 if(parent != null && parent != thisNode){
646 throw new IllegalArgumentException("Child TaxonNode (id:" + child.getId() +") must be a child of this (id:" + thisNode.getId() + " node. Sortindex is: " + index + ", parent-id:" + parent.getId());
647 }else if (parent == null){
648 throw new IllegalStateException("Parent of child is null in TaxonNode.removeChild(int). This should not happen.");
649 }
650 childNodes.remove(index);
651 child.setClassification(null);
652
653 //update sortindex
654 //TODO workaround (see sortIndex doc)
655 this.countChildren = childNodes.size();
656 child.setParent(null);
657 child.setTreeIndex(null);
658 updateSortIndex(index);
659 child.setSortIndex(null);
660 }
661 }
662
663
664 /**
665 * Remove this taxonNode From its taxonomic parent
666 *
667 * @return true on success
668 */
669 public boolean delete(){
670 if(isTopmostNode()){
671 return classification.deleteChildNode(this);
672 }else{
673 return getParent().deleteChildNode(this);
674 }
675 }
676
677 /**
678 * Remove this taxonNode From its taxonomic parent
679 *
680 * @return true on success
681 */
682 public boolean delete(boolean deleteChildren){
683 if(isTopmostNode()){
684 return classification.deleteChildNode(this, deleteChildren);
685 }else{
686 return getParent().deleteChildNode(this, deleteChildren);
687 }
688 }
689
690 @Override
691 @Deprecated //for CDM lib internal use only, may be removed in future versions
692 public int treeId() {
693 if (this.classification == null){
694 logger.warn("TaxonNode has no classification. This should not happen."); //#3840
695 return -1;
696 }else{
697 return this.classification.getId();
698 }
699 }
700
701
702 /**
703 * Sets the parent of this taxon node to the given parent. Cleans up references to
704 * old parents and sets the classification to the new parents classification
705 *
706 * @param parent
707 */
708 @Transient
709 protected void setParentTreeNode(TaxonNode parent, int index){
710 // remove ourselves from the old parent
711 TaxonNode formerParent = this.getParent();
712 formerParent = CdmBase.deproxy(formerParent);
713 if (formerParent != null){
714 //special case, child already exists for same parent
715 //FIXME document / check for correctness
716 if (formerParent.equals(parent)){
717 int currentIndex = formerParent.getChildNodes().indexOf(this);
718 if (currentIndex != -1 && currentIndex < index){
719 index--;
720 }
721 }
722
723 //remove from old parent
724 formerParent.removeChildNode(this);
725 }
726
727 // set the new parent
728 setParent(parent);
729
730 // set the classification to the parents classification
731
732 Classification classification = parent.getClassification();
733 //FIXME also set the tree index here for performance reasons
734 classification = CdmBase.deproxy(classification);
735 setClassificationRecursively(classification);
736 // add this node to the parent's child nodes
737 parent = CdmBase.deproxy(parent);
738 List<TaxonNode> parentChildren = parent.getChildNodes();
739 //TODO: Only as a workaround. We have to find out why merge creates null entries.
740
741 // HHH_9751_Util.removeAllNull(parentChildren);
742 // parent.updateSortIndex(0);
743 if (index > parent.getChildNodes().size()){
744 index = parent.getChildNodes().size();
745 }
746 if (parentChildren.contains(this)){
747 //avoid duplicates
748 if (parentChildren.indexOf(this) < index){
749 index = index-1;
750 }
751 parentChildren.remove(this);
752 parentChildren.add(index, this);
753 }else{
754 parentChildren.add(index, this);
755 }
756
757
758 //sortIndex
759 //TODO workaround (see sortIndex doc)
760 // this.getParent().removeNullValueFromChildren();
761 this.getParent().updateSortIndex(index);
762 //only for debugging
763 if (! this.getSortIndex().equals(index)){
764 logger.warn("index and sortindex are not equal: " + this.getSortIndex() + ";" + index);
765 }
766
767 // update the children count
768 parent.setCountChildren(parent.getChildNodes().size());
769 }
770
771 /**
772 * As long as the sort index is not correctly handled through hibernate this is a workaround method
773 * to update the sort index manually
774 * @param parentChildren
775 * @param index
776 */
777 private void updateSortIndex(int index) {
778 if (this.hasChildNodes()){
779 List<TaxonNode> children = this.getChildNodes();
780 HHH_9751_Util.removeAllNull(children);
781 for(int i = index; i < children.size(); i++){
782 TaxonNode child = children.get(i);
783 if (child != null){
784 // child = CdmBase.deproxy(child, TaxonNode.class); //deproxy not needed as long as setSortIndex is protected or public #4200
785 child.setSortIndex(i);
786 }else{
787 String message = "A node in a taxon tree must never be null but is (ParentId: %d; sort index: %d; index: %d; i: %d)";
788 throw new IllegalStateException(String.format(message, getId(), sortIndex, index, i));
789 }
790 }
791 }
792 }
793
794
795 /**
796 * Returns a set containing this node and all nodes that are descendants of this node
797 *
798 * @return
799 */
800 @Transient
801 protected Set<TaxonNode> getDescendants(){
802 Set<TaxonNode> nodeSet = new HashSet<>();
803
804 nodeSet.add(this);
805
806 for(TaxonNode childNode : getChildNodes()){
807 nodeSet.addAll(childNode.getDescendants());
808 }
809
810 return nodeSet;
811 }
812
813 /**
814 * Returns a set containing a clone of this node and of all nodes that are descendants of this node
815 *
816 * @return
817 */
818 protected TaxonNode cloneDescendants(){
819
820 TaxonNode clone = (TaxonNode)this.clone();
821 TaxonNode childClone;
822
823 for(TaxonNode childNode : getChildNodes()){
824 childClone = (TaxonNode) childNode.clone();
825 for (TaxonNode childChild:childNode.getChildNodes()){
826 childClone.addChildNode(childChild.cloneDescendants(), childChild.getReference(), childChild.getMicroReference());
827 }
828 clone.addChildNode(childClone, childNode.getReference(), childNode.getMicroReference());
829 //childClone.addChildNode(childNode.cloneDescendants());
830 }
831 return clone;
832 }
833
834 /**
835 * Returns all ancestor nodes of this node
836 *
837 * @return a set of all parent nodes
838 */
839 @Transient
840 protected Set<TaxonNode> getAncestors(){
841 Set<TaxonNode> nodeSet = new HashSet<>();
842 if(this.getParent() != null){
843 TaxonNode parent = CdmBase.deproxy(this.getParent());
844 nodeSet.add(parent);
845 nodeSet.addAll(parent.getAncestors());
846 }
847 return nodeSet;
848 }
849
850 /**
851 * Retrieves the first ancestor of the given rank. If any of the ancestors
852 * has no taxon or has a rank > the given rank <code>null</code> is returned.
853 * If <code>this</code> taxon is already of given rank this taxon is returned.
854 * @param rank the rank the ancestor should have
855 * @return the first found instance of a parent taxon node with the given rank
856 */
857 @Transient
858 public TaxonNode getAncestorOfRank(Rank rank){
859 Taxon taxon = CdmBase.deproxy(this.getTaxon());
860 if (taxon == null){
861 return null;
862 }
863 TaxonName name = CdmBase.deproxy(taxon.getName());
864 if (name != null && name.getRank() != null){
865 if (name.getRank().isHigher(rank)){
866 return null;
867 }
868 if (name.getRank().equals(rank)){
869 return this;
870 }
871 }
872
873 if(this.getParent() != null){
874 TaxonNode parent = CdmBase.deproxy(this.getParent());
875 return parent.getAncestorOfRank(rank);
876 }
877 return null;
878 }
879
880 /**
881 * Returns the ancestor taxa, starting with the highest (e.g. kingdom)
882 * @return
883 */
884 @Transient
885 public List<Taxon> getAncestorTaxaList(){
886 List<Taxon> result = new ArrayList<>();
887 TaxonNode current = this;
888 while (current != null){
889 if (current.getTaxon() != null){
890 result.add(0, current.getTaxon());
891 }
892 current = current.getParent();
893 }
894 return result;
895 }
896
897 /**
898 * Returns the ancestor taxon nodes, that do have a taxon attached
899 * (excludes the root node) starting with the highest
900 *
901 * @return
902 */
903 @Transient
904 public List<TaxonNode> getAncestorList(){
905 List<TaxonNode> result = new ArrayList<>();
906 TaxonNode current = this.getParent();
907 while (current != null){
908 if (current.getTaxon() != null){
909 result.add(0, current);
910 }
911 current = current.getParent();
912 }
913 return result;
914 }
915
916
917 /**
918 * Whether this TaxonNode is a direct child of the classification TreeNode
919 * @return
920 */
921 @Transient
922 public boolean isTopmostNode(){
923 boolean parentCheck = false;
924 boolean classificationCheck = false;
925
926 if(getParent() != null) {
927 if(getParent().getTaxon() == null) {
928 parentCheck = true;
929 }
930 }
931
932 //TODO remove
933 // FIXME This should work but doesn't, due to missing sort indexes, can be removed after fixing #4200, #4098
934 if (classification != null){
935 classificationCheck = classification.getRootNode().getChildNodes().contains(this);
936 }else{
937 classificationCheck = false;
938 }
939
940 // The following is just for logging purposes for the missing sort indexes problem
941 // ticket #4098
942 if(parentCheck != classificationCheck) {
943 logger.warn("isTopmost node check " + parentCheck + " not same as classificationCheck : " + classificationCheck + " for taxon node ");
944 if(this.getParent() != null) {
945 logger.warn("-- with parent uuid " + this.getParent().getUuid().toString());
946 logger.warn("-- with parent id " + this.getParent().getId());
947 for(TaxonNode node : this.getParent().getChildNodes()) {
948 if(node == null) {
949 logger.warn("-- child node is null");
950 } else if (node.getTaxon() == null) {
951 logger.warn("-- child node taxon is null");
952 }
953 }
954 logger.warn("-- parent child count" + this.getParent().getChildNodes().size());
955 }
956 }
957
958 return parentCheck;
959 }
960
961 /**
962 * Whether this TaxonNode is a descendant of (or equal to) the given TaxonNode
963 *
964 * @param possibleParent
965 * @return <code>true</code> if <b>this</b> is a descendant
966 */
967 @Transient
968 public boolean isDescendant(TaxonNode possibleParent){
969 if (possibleParent == null || this.treeIndex() == null
970 || possibleParent.treeIndex() == null) {
971 return false;
972 }
973 return this.treeIndex().startsWith(possibleParent.treeIndex() );
974 }
975
976 /**
977 * Whether this TaxonNode is an ascendant of (or equal to) the given TaxonNode.
978 *
979 *
980 * @param possibleChild
981 * @return <code>true</code> if <b>this</b> is a ancestor of the given child parameter
982 */
983 @Transient
984 public boolean isAncestor(TaxonNode possibleChild){
985 if (possibleChild == null || this.treeIndex() == null || possibleChild.treeIndex() == null) {
986 return false;
987 }
988 // return possibleChild == null ? false : possibleChild.getAncestors().contains(this);
989 return possibleChild.treeIndex().startsWith(this.treeIndex());
990 }
991
992 /**
993 * Whether this taxon has child nodes
994 *
995 * @return true if the taxonNode has childNodes
996 */
997 @Transient
998 @Override
999 public boolean hasChildNodes(){
1000 return childNodes.size() > 0;
1001 }
1002
1003 public boolean hasTaxon() {
1004 return (taxon!= null);
1005 }
1006
1007 /**
1008 * @return
1009 */
1010 @Transient
1011 public Rank getNullSafeRank() {
1012 return hasTaxon() ? getTaxon().getNullSafeRank() : null;
1013 }
1014
1015 public void removeNullValueFromChildren(){
1016 try {
1017 //HHH_9751_Util.removeAllNull(childNodes);
1018 this.updateSortIndex(0);
1019 } catch (LazyInitializationException e) {
1020 logger.info("Cannot clean up uninitialized children without a session, skipping.");
1021 }
1022 }
1023
1024
1025
1026 //*********************** CLONE ********************************************************/
1027 /**
1028 * Clones <i>this</i> taxon node. This is a shortcut that enables to create
1029 * a new instance that differs only slightly from <i>this</i> taxon node by
1030 * modifying only some of the attributes.<BR><BR>
1031 * The child nodes are not copied.<BR>
1032 * The taxon and parent are the same as for the original taxon node. <BR>
1033 *
1034 * @see eu.etaxonomy.cdm.model.media.IdentifiableEntity#clone()
1035 * @see java.lang.Object#clone()
1036 */
1037 @Override
1038 public Object clone() {
1039 try{
1040 TaxonNode result = (TaxonNode)super.clone();
1041 result.getTaxon().addTaxonNode(result);
1042
1043 //childNodes
1044 result.childNodes = new ArrayList<>();
1045 result.countChildren = 0;
1046
1047 //agents
1048 result.agentRelations = new HashSet<>();
1049 for (TaxonNodeAgentRelation rel : this.agentRelations){
1050 result.addAgentRelation((TaxonNodeAgentRelation)rel.clone());
1051 }
1052
1053 //excludedNote
1054 result.excludedNote = new HashMap<>();
1055 for(Language lang : this.excludedNote.keySet()){
1056 result.excludedNote.put(lang, this.excludedNote.get(lang));
1057 }
1058
1059
1060 return result;
1061 }catch (CloneNotSupportedException e) {
1062 logger.warn("Object does not implement cloneable");
1063 e.printStackTrace();
1064 return null;
1065 }
1066 }
1067
1068 }