2 * Copyright (C) 2007 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
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.
10 package eu
.etaxonomy
.cdm
.model
.taxon
;
12 import java
.util
.ArrayList
;
13 import java
.util
.HashMap
;
14 import java
.util
.HashSet
;
15 import java
.util
.List
;
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
;
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
;
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
;
76 @XmlAccessorType(XmlAccessType
.FIELD
)
77 @XmlType(name
= "TaxonNode", propOrder
= {
84 "referenceForParentChildRelation",
85 "microReferenceForParentChildRelation",
91 @XmlRootElement(name
= "TaxonNode")
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")
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
{
104 private static final long serialVersionUID
= -4743289894926587693L;
105 private static final Logger logger
= Logger
.getLogger(TaxonNode
.class);
107 @XmlElement(name
= "taxon")
109 @XmlSchemaType(name
= "IDREF")
110 @ManyToOne(fetch
= FetchType
.LAZY
)
111 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
})
116 @XmlElement(name
= "parent")
118 @XmlSchemaType(name
= "IDREF")
119 @ManyToOne(fetch
= FetchType
.LAZY
)
120 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
})
121 private TaxonNode parent
;
124 @XmlElement(name
= "treeIndex")
126 @Field(store
= Store
.YES
, index
= Index
.YES
, analyze
= Analyze
.NO
)
127 private String treeIndex
;
130 @XmlElement(name
= "classification")
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
;
139 @XmlElementWrapper(name
= "childNodes")
140 @XmlElement(name
= "childNode")
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
<>();
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;
154 @XmlElement(name
= "reference")
156 @XmlSchemaType(name
= "IDREF")
157 @ManyToOne(fetch
= FetchType
.LAZY
)
158 @Cascade({CascadeType
.SAVE_UPDATE
,CascadeType
.MERGE
})
159 private Reference referenceForParentChildRelation
;
161 @XmlElement(name
= "microReference")
162 private String microReferenceForParentChildRelation
;
164 @XmlElement(name
= "countChildren")
165 private int countChildren
;
167 @XmlElementWrapper(name
= "agentRelations")
168 @XmlElement(name
= "agentRelation")
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
<>();
175 @XmlAttribute(name
= "unplaced")
176 private boolean unplaced
= false;
177 public boolean isUnplaced() {return unplaced
;}
178 public void setUnplaced(boolean unplaced
) {this.unplaced
= unplaced
;}
180 @XmlAttribute(name
= "excluded")
181 private boolean excluded
= false;
182 public boolean isExcluded() {return excluded
;}
183 public void setExcluded(boolean excluded
) {this.excluded
= excluded
;}
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
<>();
193 // private Taxon originalConcept;
195 @XmlElement(name
= "synonymToBeUsed")
197 @XmlSchemaType(name
= "IDREF")
198 @ManyToOne(fetch
= FetchType
.LAZY
)
199 @Cascade({CascadeType
.SAVE_UPDATE
,CascadeType
.MERGE
})
200 private Synonym synonymToBeUsed
;
202 // ******************** CONSTRUCTOR **********************************************/
204 protected TaxonNode(){super();}
207 * to create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}
208 * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}
210 * @param classification
211 * @deprecated setting of classification is handled in the addTaxonNode() method,
212 * use TaxonNode(taxon) instead
215 protected TaxonNode (Taxon taxon
, Classification classification
){
217 setClassification(classification
);
221 * to create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}
222 * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}
226 protected TaxonNode(Taxon taxon
){
230 // ************************* GETTER / SETTER *******************************/
233 public Integer
getSortIndex() {
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)
242 * @deprecated for internal use only
245 protected void setSortIndex(Integer i
) {
246 // CdmBase.deproxy(this, TaxonNode.class).sortIndex = i; //alternative solution for private, DON'T remove
251 public Taxon
getTaxon() {
254 public void setTaxon(Taxon taxon
) {
257 taxon
.addTaxonNode(this);
263 public List
<TaxonNode
> getChildNodes() {
266 protected void setChildNodes(List
<TaxonNode
> childNodes
) {
267 this.childNodes
= childNodes
;
271 public Classification
getClassification() {
272 return classification
;
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
281 protected void setClassification(Classification classification
) {
282 this.classification
= classification
;
287 public String
getMicroReference() {
288 return microReferenceForParentChildRelation
;
290 public void setMicroReference(String microReference
) {
291 this.microReferenceForParentChildRelation
= microReference
;
296 public Reference
getReference() {
297 return referenceForParentChildRelation
;
299 public void setReference(Reference reference
) {
300 this.referenceForParentChildRelation
= reference
;
304 public int getCountChildren() {
305 return countChildren
;
308 * @deprecated for internal use only
309 * @param countChildren
312 protected void setCountChildren(int countChildren
) {
313 this.countChildren
= countChildren
;
319 public TaxonNode
getParent(){
323 * Sets the parent of this taxon node.<BR>
325 * In most cases you would want to call setParentTreeNode(ITreeNode) which
326 * handles updating of the bidirectional relationship
328 * @see setParentTreeNode(ITreeNode)
332 protected void setParent(TaxonNode parent
) {
333 this.parent
= parent
;
334 // this.treeIndex = parent.treeIndex() +
337 // *************** Excluded Note ***************
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)
346 public Map
<Language
,LanguageString
> getExcludedNote(){
347 return this.excludedNote
;
351 * Returns the excluded note string in the given {@link Language language}
353 * @param language the language in which the description string looked for is formulated
354 * @see #getExcludedNote()
355 * @see #putExcludedNote(Language, String)
357 public String
getExcludedNote(Language language
){
358 LanguageString languageString
= excludedNote
.get(language
);
359 if (languageString
== null){
362 return languageString
.getText();
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.
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)
376 public void putExcludedNote(LanguageString excludedNote
){
377 this.excludedNote
.put(excludedNote
.getLanguage(), excludedNote
);
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.
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)
391 public void putExcludedNote(Language language
, String text
){
392 this.excludedNote
.put(language
, LanguageString
.NewInstance(text
, language
));
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}.
400 * @param lang the language in which the language string to be removed
401 * has been formulated
402 * @see #getExcludedNote()
404 public void removeExcludedNote(Language lang
){
405 this.excludedNote
.remove(lang
);
408 // ****************** Agent Relations ****************************/
414 public Set
<TaxonNodeAgentRelation
> getAgentRelations() {
415 return this.agentRelations
;
418 public TaxonNodeAgentRelation
addAgentRelation(DefinedTerm type
, TeamOrPersonBase
<?
> agent
){
419 TaxonNodeAgentRelation result
= TaxonNodeAgentRelation
.NewInstance(this, agent
, type
);
423 * @param nodeAgentRelation
425 protected void addAgentRelation(TaxonNodeAgentRelation agentRelation
) {
426 agentRelation
.setTaxonNode(this);
427 this.agentRelations
.add(agentRelation
);
431 * @param nodeAgentRelation
433 public void removeNodeAgent(TaxonNodeAgentRelation agentRelation
) {
434 agentRelation
.setTaxonNode(this);
435 agentRelations
.remove(agentRelation
);
438 //********************
441 public Synonym
getSynonymToBeUsed() {
442 return synonymToBeUsed
;
444 public void setSynonymToBeUsed(Synonym synonymToBeUsed
) {
445 this.synonymToBeUsed
= synonymToBeUsed
;
451 public String
treeIndex() {
455 @Deprecated //for CDM lib internal use only, may be removed in future versions
456 public void setTreeIndex(String treeIndex
) {
457 this.treeIndex
= treeIndex
;
460 public String
treeIndexLike() {
461 return treeIndex
+ "%";
464 public String
treeIndexWc() {
465 return treeIndex
+ "*";
470 //************************ METHODS **************************/
473 public TaxonNode
addChildTaxon(Taxon taxon
, Reference citation
, String microCitation
) {
474 return addChildTaxon(taxon
, this.childNodes
.size(), citation
, microCitation
);
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()));
485 return addChildNode(new TaxonNode(taxon
), index
, citation
, microCitation
);
489 * Moves a taxon node to a new parent. Descendents of the node are moved as well
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
495 public TaxonNode
addChildNode(TaxonNode childNode
, Reference reference
, String microReference
){
496 addChildNode(childNode
, childNodes
.size(), reference
, microReference
);
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.
507 * @param child the taxon node to be added
508 * @param index the integer indicating the position at which the child
510 * @see #getChildNodes()
511 * @see #addChildNode(TaxonNode, Reference, String, Synonym)
512 * @see #deleteChildNode(TaxonNode)
513 * @see #deleteChildNode(int)
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
);
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.");
525 child
.setParentTreeNode(this, index
);
527 child
.setReference(reference
);
528 child
.setMicroReference(microReference
);
534 * Sets this nodes classification. Updates classification of child nodes recursively.
536 * If the former and the actual tree are equal() this method does nothing.
538 * @throws IllegalArgumentException if newClassifciation is null
540 * @param newClassification
543 private void setClassificationRecursively(Classification newClassification
) {
544 if (newClassification
== null){
545 throw new IllegalArgumentException("New Classification must not be 'null' when setting new classification.");
547 if(! newClassification
.equals(this.getClassification())){
548 this.setClassification(newClassification
);
549 for(TaxonNode childNode
: this.getChildNodes()){
550 childNode
.setClassificationRecursively(newClassification
);
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);
563 ArrayList
<TaxonNode
> childNodes
= new ArrayList
<TaxonNode
>(node
.getChildNodes());
564 for(TaxonNode childNode
: childNodes
){
565 HibernateProxyHelper
.deproxy(childNode
, TaxonNode
.class);
566 node
.deleteChildNode(childNode
);
568 taxon
.removeTaxonNode(node
);
573 * Deletes the child node and also removes children of childnode
574 * recursively if delete children is <code>true</code>
576 * @param deleteChildren
579 public boolean deleteChildNode(TaxonNode node
, boolean deleteChildren
) {
580 boolean result
= removeChildNode(node
);
581 Taxon taxon
= node
.getTaxon();
583 taxon
.removeTaxonNode(node
);
585 ArrayList
<TaxonNode
> childNodes
= new ArrayList
<TaxonNode
>(node
.getChildNodes());
586 for(TaxonNode childNode
: childNodes
){
587 node
.deleteChildNode(childNode
, deleteChildren
);
590 ArrayList
<TaxonNode
> childNodes
= new ArrayList
<TaxonNode
>(node
.getChildNodes());
591 for(TaxonNode childNode
: childNodes
){
592 this.addChildNode(childNode
, null, null);
600 * Removes the child node from this node. Sets the parent and the classification of the child
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");
612 int index
= childNodes
.indexOf(childNode
);
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
626 * If the given index is out of bounds no child will be removed.
628 * @param index the integer indicating the position of the taxon node to
630 * @see #getChildNodes()
631 * @see #addChildNode(TaxonNode, Reference, String)
632 * @see #addChildNode(TaxonNode, int, Reference, String)
633 * @see #deleteChildNode(TaxonNode)
635 public void removeChild(int index
){
636 //TODO: Only as a workaround. We have to find out why merge creates null entries.
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.
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.");
650 childNodes
.remove(index
);
651 child
.setClassification(null);
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);
665 * Remove this taxonNode From its taxonomic parent
667 * @return true on success
669 public boolean delete(){
671 return classification
.deleteChildNode(this);
673 return getParent().deleteChildNode(this);
678 * Remove this taxonNode From its taxonomic parent
680 * @return true on success
682 public boolean delete(boolean deleteChildren
){
684 return classification
.deleteChildNode(this, deleteChildren
);
686 return getParent().deleteChildNode(this, deleteChildren
);
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
697 return this.classification
.getId();
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
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
){
723 //remove from old parent
724 formerParent
.removeChildNode(this);
727 // set the new parent
730 // set the classification to the parents classification
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.
741 // HHH_9751_Util.removeAllNull(parentChildren);
742 // parent.updateSortIndex(0);
743 if (index
> parent
.getChildNodes().size()){
744 index
= parent
.getChildNodes().size();
746 if (parentChildren
.contains(this)){
748 if (parentChildren
.indexOf(this) < index
){
751 parentChildren
.remove(this);
752 parentChildren
.add(index
, this);
754 parentChildren
.add(index
, this);
759 //TODO workaround (see sortIndex doc)
760 // this.getParent().removeNullValueFromChildren();
761 this.getParent().updateSortIndex(index
);
763 if (! this.getSortIndex().equals(index
)){
764 logger
.warn("index and sortindex are not equal: " + this.getSortIndex() + ";" + index
);
767 // update the children count
768 parent
.setCountChildren(parent
.getChildNodes().size());
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
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
);
784 // child = CdmBase.deproxy(child, TaxonNode.class); //deproxy not needed as long as setSortIndex is protected or public #4200
785 child
.setSortIndex(i
);
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
));
796 * Returns a set containing this node and all nodes that are descendants of this node
801 protected Set
<TaxonNode
> getDescendants(){
802 Set
<TaxonNode
> nodeSet
= new HashSet
<>();
806 for(TaxonNode childNode
: getChildNodes()){
807 nodeSet
.addAll(childNode
.getDescendants());
814 * Returns a set containing a clone of this node and of all nodes that are descendants of this node
818 protected TaxonNode
cloneDescendants(){
820 TaxonNode clone
= (TaxonNode
)this.clone();
821 TaxonNode childClone
;
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());
828 clone
.addChildNode(childClone
, childNode
.getReference(), childNode
.getMicroReference());
829 //childClone.addChildNode(childNode.cloneDescendants());
835 * Returns all ancestor nodes of this node
837 * @return a set of all parent nodes
840 protected Set
<TaxonNode
> getAncestors(){
841 Set
<TaxonNode
> nodeSet
= new HashSet
<>();
842 if(this.getParent() != null){
843 TaxonNode parent
= CdmBase
.deproxy(this.getParent());
845 nodeSet
.addAll(parent
.getAncestors());
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
858 public TaxonNode
getAncestorOfRank(Rank rank
){
859 Taxon taxon
= CdmBase
.deproxy(this.getTaxon());
863 TaxonName name
= CdmBase
.deproxy(taxon
.getName());
864 if (name
!= null && name
.getRank() != null){
865 if (name
.getRank().isHigher(rank
)){
868 if (name
.getRank().equals(rank
)){
873 if(this.getParent() != null){
874 TaxonNode parent
= CdmBase
.deproxy(this.getParent());
875 return parent
.getAncestorOfRank(rank
);
881 * Returns the ancestor taxa, starting with the highest (e.g. kingdom)
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());
892 current
= current
.getParent();
898 * Returns the ancestor taxon nodes, that do have a taxon attached
899 * (excludes the root node) starting with the highest
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
);
911 current
= current
.getParent();
918 * Whether this TaxonNode is a direct child of the classification TreeNode
922 public boolean isTopmostNode(){
923 boolean parentCheck
= false;
924 boolean classificationCheck
= false;
926 if(getParent() != null) {
927 if(getParent().getTaxon() == null) {
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);
937 classificationCheck
= false;
940 // The following is just for logging purposes for the missing sort indexes problem
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()) {
949 logger
.warn("-- child node is null");
950 } else if (node
.getTaxon() == null) {
951 logger
.warn("-- child node taxon is null");
954 logger
.warn("-- parent child count" + this.getParent().getChildNodes().size());
962 * Whether this TaxonNode is a descendant of (or equal to) the given TaxonNode
964 * @param possibleParent
965 * @return <code>true</code> if <b>this</b> is a descendant
968 public boolean isDescendant(TaxonNode possibleParent
){
969 if (possibleParent
== null || this.treeIndex() == null
970 || possibleParent
.treeIndex() == null) {
973 return this.treeIndex().startsWith(possibleParent
.treeIndex() );
977 * Whether this TaxonNode is an ascendant of (or equal to) the given TaxonNode.
980 * @param possibleChild
981 * @return <code>true</code> if <b>this</b> is a ancestor of the given child parameter
984 public boolean isAncestor(TaxonNode possibleChild
){
985 if (possibleChild
== null || this.treeIndex() == null || possibleChild
.treeIndex() == null) {
988 // return possibleChild == null ? false : possibleChild.getAncestors().contains(this);
989 return possibleChild
.treeIndex().startsWith(this.treeIndex());
993 * Whether this taxon has child nodes
995 * @return true if the taxonNode has childNodes
999 public boolean hasChildNodes(){
1000 return childNodes
.size() > 0;
1003 public boolean hasTaxon() {
1004 return (taxon
!= null);
1011 public Rank
getNullSafeRank() {
1012 return hasTaxon() ?
getTaxon().getNullSafeRank() : null;
1015 public void removeNullValueFromChildren(){
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.");
1026 //*********************** CLONE ********************************************************/
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>
1034 * @see eu.etaxonomy.cdm.model.media.IdentifiableEntity#clone()
1035 * @see java.lang.Object#clone()
1038 public Object
clone() {
1040 TaxonNode result
= (TaxonNode
)super.clone();
1041 result
.getTaxon().addTaxonNode(result
);
1044 result
.childNodes
= new ArrayList
<>();
1045 result
.countChildren
= 0;
1048 result
.agentRelations
= new HashSet
<>();
1049 for (TaxonNodeAgentRelation rel
: this.agentRelations
){
1050 result
.addAgentRelation((TaxonNodeAgentRelation
)rel
.clone());
1054 result
.excludedNote
= new HashMap
<>();
1055 for(Language lang
: this.excludedNote
.keySet()){
1056 result
.excludedNote
.put(lang
, this.excludedNote
.get(lang
));
1061 }catch (CloneNotSupportedException e
) {
1062 logger
.warn("Object does not implement cloneable");
1063 e
.printStackTrace();