3 * Copyright (C) 2007 EDIT
4 * European Distributed Institute of Taxonomy
5 * http://www.e-taxonomy.eu
7 * The contents of this file are subject to the Mozilla Public License Version 1.1
8 * See LICENSE.TXT at the top of this package for the full license terms.
11 package eu
.etaxonomy
.cdm
.model
.taxon
;
13 import java
.util
.ArrayList
;
14 import java
.util
.HashSet
;
15 import java
.util
.List
;
18 import javax
.persistence
.Entity
;
19 import javax
.persistence
.FetchType
;
20 import javax
.persistence
.ManyToOne
;
21 import javax
.persistence
.OneToMany
;
22 import javax
.persistence
.OrderBy
;
23 import javax
.persistence
.OrderColumn
;
24 import javax
.persistence
.Transient
;
25 import javax
.validation
.constraints
.Size
;
26 import javax
.xml
.bind
.annotation
.XmlAccessType
;
27 import javax
.xml
.bind
.annotation
.XmlAccessorType
;
28 import javax
.xml
.bind
.annotation
.XmlElement
;
29 import javax
.xml
.bind
.annotation
.XmlElementWrapper
;
30 import javax
.xml
.bind
.annotation
.XmlIDREF
;
31 import javax
.xml
.bind
.annotation
.XmlRootElement
;
32 import javax
.xml
.bind
.annotation
.XmlSchemaType
;
33 import javax
.xml
.bind
.annotation
.XmlType
;
35 import org
.apache
.log4j
.Logger
;
36 import org
.hibernate
.annotations
.Cascade
;
37 import org
.hibernate
.annotations
.CascadeType
;
38 import org
.hibernate
.annotations
.Index
;
39 import org
.hibernate
.annotations
.Table
;
40 import org
.hibernate
.envers
.Audited
;
41 import org
.hibernate
.search
.annotations
.ContainedIn
;
42 import org
.hibernate
.search
.annotations
.Indexed
;
43 import org
.hibernate
.search
.annotations
.IndexedEmbedded
;
45 import eu
.etaxonomy
.cdm
.hibernate
.HibernateProxyHelper
;
46 import eu
.etaxonomy
.cdm
.model
.common
.AnnotatableEntity
;
47 import eu
.etaxonomy
.cdm
.model
.common
.CdmBase
;
48 import eu
.etaxonomy
.cdm
.model
.common
.ITreeNode
;
49 import eu
.etaxonomy
.cdm
.model
.reference
.Reference
;
55 @XmlAccessorType(XmlAccessType
.FIELD
)
56 @XmlType(name
= "TaxonNode", propOrder
= {
63 "referenceForParentChildRelation",
64 "microReferenceForParentChildRelation",
68 @XmlRootElement(name
= "TaxonNode")
70 @Indexed(index
= "eu.etaxonomy.cdm.model.taxon.TaxonNode")
72 @Table(appliesTo
="TaxonNode", indexes
= { @Index(name
= "taxonNodeTreeIndex", columnNames
= { "treeIndex" }) })
73 public class TaxonNode
extends AnnotatableEntity
implements ITaxonTreeNode
, ITreeNode
<TaxonNode
>, Cloneable
{
74 private static final long serialVersionUID
= -4743289894926587693L;
75 private static final Logger logger
= Logger
.getLogger(TaxonNode
.class);
77 @XmlElement(name
= "taxon")
79 @XmlSchemaType(name
= "IDREF")
80 @ManyToOne(fetch
= FetchType
.LAZY
)
81 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
})
86 @XmlElement(name
= "parent")
88 @XmlSchemaType(name
= "IDREF")
89 @ManyToOne(fetch
= FetchType
.LAZY
)
90 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
})
91 private TaxonNode parent
;
94 @XmlElement(name
= "treeIndex")
96 private String treeIndex
;
99 @XmlElement(name
= "classification")
101 @XmlSchemaType(name
= "IDREF")
102 @ManyToOne(fetch
= FetchType
.LAZY
)
103 @Cascade({CascadeType
.SAVE_UPDATE
})
104 // TODO @NotNull // avoids creating a UNIQUE key for this field
106 private Classification classification
;
108 @XmlElementWrapper(name
= "childNodes")
109 @XmlElement(name
= "childNode")
111 @XmlSchemaType(name
= "IDREF")
112 //see https://dev.e-taxonomy.eu/trac/ticket/3722
113 @OrderColumn(name
="sortIndex")
114 @OrderBy("sortIndex")
115 @OneToMany(mappedBy
="parent", fetch
=FetchType
.LAZY
)
116 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
})
117 private List
<TaxonNode
> childNodes
= new ArrayList
<TaxonNode
>();
119 //see https://dev.e-taxonomy.eu/trac/ticket/3722
120 //see https://dev.e-taxonomy.eu/trac/ticket/4200
121 private Integer sortIndex
= -1;
123 @XmlElement(name
= "reference")
125 @XmlSchemaType(name
= "IDREF")
126 @ManyToOne(fetch
= FetchType
.LAZY
)
127 @Cascade({CascadeType
.SAVE_UPDATE
})
128 private Reference
<?
> referenceForParentChildRelation
;
130 @XmlElement(name
= "microReference")
131 private String microReferenceForParentChildRelation
;
133 @XmlElement(name
= "countChildren")
134 private int countChildren
;
136 // private Taxon originalConcept;
138 @XmlElement(name
= "synonymToBeUsed")
140 @XmlSchemaType(name
= "IDREF")
141 @ManyToOne(fetch
= FetchType
.LAZY
)
142 @Cascade({CascadeType
.SAVE_UPDATE
})
143 private Synonym synonymToBeUsed
;
145 // ******************** CONSTRUCTOR **********************************************/
147 protected TaxonNode(){super();}
150 * to create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}
151 * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}
153 * @param classification
154 * @deprecated setting of classification is handled in the addTaxonNode() method,
155 * use TaxonNode(taxon) instead
158 protected TaxonNode (Taxon taxon
, Classification classification
){
160 setClassification(classification
);
164 * to create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}
165 * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}
169 protected TaxonNode(Taxon taxon
){
173 // ************************* GETTER / SETTER *******************************/
175 public Integer
getSortIndex() {
179 * SortIndex shall be handled only internally, therefore not public.
180 * However, as javaassist only supports protected methods it needs to be protected, not private.
181 * Alternatively we could use deproxy on every call of this method (see commented code)
184 * @deprecated for internal use only
186 protected void setSortIndex(Integer i
) {
187 // CdmBase.deproxy(this, TaxonNode.class).sortIndex = i; //alternative solution for private, DON'T remove
192 public Taxon
getTaxon() {
195 protected void setTaxon(Taxon taxon
) {
198 taxon
.addTaxonNode(this);
204 public List
<TaxonNode
> getChildNodes() {
207 protected void setChildNodes(List
<TaxonNode
> childNodes
) {
208 this.childNodes
= childNodes
;
212 public Classification
getClassification() {
213 return classification
;
216 * THIS METHOD SHOULD NOT BE CALLED!
217 * invisible part of the bidirectional relationship, for public use TaxonomicView.addRoot() or TaxonNode.addChild()
218 * @param classification
219 * @deprecated for internal use only
221 protected void setClassification(Classification classification
) {
222 this.classification
= classification
;
227 public String
getMicroReference() {
228 return microReferenceForParentChildRelation
;
230 public void setMicroReference(String microReference
) {
231 this.microReferenceForParentChildRelation
= microReference
;
236 public Reference
getReference() {
237 return referenceForParentChildRelation
;
239 public void setReference(Reference reference
) {
240 this.referenceForParentChildRelation
= reference
;
244 public int getCountChildren() {
245 return countChildren
;
248 * @deprecated for internal use only
249 * @param countChildren
251 protected void setCountChildren(int countChildren
) {
252 this.countChildren
= countChildren
;
258 public TaxonNode
getParent(){
262 * Sets the parent of this taxon node.<BR>
264 * In most cases you would want to call setParentTreeNode(ITreeNode) which
265 * handles updating of the bidirectional relationship
267 * @see setParentTreeNode(ITreeNode)
271 protected void setParent(TaxonNode parent
) {
272 this.parent
= parent
;
277 public Synonym
getSynonymToBeUsed() {
278 return synonymToBeUsed
;
280 public void setSynonymToBeUsed(Synonym synonymToBeUsed
) {
281 this.synonymToBeUsed
= synonymToBeUsed
;
287 public String
treeIndex() {
291 @Deprecated //for CDM lib internal use only, may be removed in future versions
292 public void setTreeIndex(String treeIndex
) {
293 this.treeIndex
= treeIndex
;
298 //************************ METHODS **************************/
301 public TaxonNode
addChildTaxon(Taxon taxon
, Reference citation
, String microCitation
) {
302 return addChildTaxon(taxon
, this.childNodes
.size(), citation
, microCitation
);
307 public TaxonNode
addChildTaxon(Taxon taxon
, int index
, Reference citation
, String microCitation
) {
308 if (this.getClassification().isTaxonInTree(taxon
)){
309 throw new IllegalArgumentException(String
.format("Taxon may not be in a classification twice: %s", taxon
.getTitleCache()));
311 return addChildNode(new TaxonNode(taxon
), index
, citation
, microCitation
);
315 * Moves a taxon node to a new parent. Descendents of the node are moved as well
317 * @param childNode the taxon node to be moved to the new parent
318 * @return the child node in the state of having a new parent
321 public TaxonNode
addChildNode(TaxonNode childNode
, Reference reference
, String microReference
){
322 addChildNode(childNode
, childNodes
.size(), reference
, microReference
);
327 * Inserts the given taxon node in the list of children of <i>this</i> taxon node
328 * at the given (index + 1) position. If the given index is out of bounds
329 * an exception will arise.<BR>
330 * Due to bidirectionality this method must also assign <i>this</i> taxon node
331 * as the parent of the given child.
333 * @param child the taxon node to be added
334 * @param index the integer indicating the position at which the child
336 * @see #getChildNodes()
337 * @see #addChildNode(TaxonNode, Reference, String, Synonym)
338 * @see #deleteChildNode(TaxonNode)
339 * @see #deleteChildNode(int)
342 public TaxonNode
addChildNode(TaxonNode child
, int index
, Reference reference
, String microReference
){
343 if (index
< 0 || index
> childNodes
.size() + 1){
344 throw new IndexOutOfBoundsException("Wrong index: " + index
);
346 // check if this node is a descendant of the childNode
347 if(child
.getParent() != this && child
.isAncestor(this)){
348 throw new IllegalAncestryException("New parent node is a descendant of the node to be moved.");
351 child
.setParentTreeNode(this, index
);
353 child
.setReference(reference
);
354 child
.setMicroReference(microReference
);
360 * Sets this nodes classification. Updates classification of child nodes recursively.
362 * If the former and the actual tree are equal() this method does nothing.
364 * @throws IllegalArgumentException if newClassifciation is null
366 * @param newClassification
369 private void setClassificationRecursively(Classification newClassification
) {
370 if (newClassification
== null){
371 throw new IllegalArgumentException("New Classification must not be 'null' when setting new classification.");
373 if(! newClassification
.equals(this.getClassification())){
374 this.setClassification(newClassification
);
375 for(TaxonNode childNode
: this.getChildNodes()){
376 childNode
.setClassificationRecursively(newClassification
);
382 public boolean deleteChildNode(TaxonNode node
) {
383 boolean result
= removeChildNode(node
);
384 Taxon taxon
= node
.getTaxon();
386 taxon
.removeTaxonNode(node
);
388 ArrayList
<TaxonNode
> childNodes
= new ArrayList
<TaxonNode
>(node
.getChildNodes());
389 for(TaxonNode childNode
: childNodes
){
390 node
.deleteChildNode(childNode
);
397 * Deletes the child node and also removes children of childnode
398 * recursively if delete children is <code>true</code>
400 * @param deleteChildren
403 public boolean deleteChildNode(TaxonNode node
, boolean deleteChildren
) {
404 boolean result
= removeChildNode(node
);
405 Taxon taxon
= node
.getTaxon();
407 taxon
.removeTaxonNode(node
);
409 ArrayList
<TaxonNode
> childNodes
= new ArrayList
<TaxonNode
>(node
.getChildNodes());
410 for(TaxonNode childNode
: childNodes
){
411 node
.deleteChildNode(childNode
, deleteChildren
);
414 ArrayList
<TaxonNode
> childNodes
= new ArrayList
<TaxonNode
>(node
.getChildNodes());
415 for(TaxonNode childNode
: childNodes
){
416 this.addChildNode(childNode
, null, null);
424 * Removes the child node from this node. Sets the parent and the classification of the child
430 protected boolean removeChildNode(TaxonNode childNode
){
431 boolean result
= true;
433 if(childNode
== null){
434 throw new IllegalArgumentException("TaxonNode may not be null");
436 int index
= childNodes
.indexOf(childNode
);
446 * Removes the child node placed at the given (index + 1) position
447 * from the list of {@link #getChildNodes() children} of <i>this</i> taxon node.
448 * Sets the parent and the classification of the child
450 * If the given index is out of bounds no child will be removed.
452 * @param index the integer indicating the position of the taxon node to
454 * @see #getChildNodes()
455 * @see #addChildNode(TaxonNode, Reference, String)
456 * @see #addChildNode(TaxonNode, int, Reference, String)
457 * @see #deleteChildNode(TaxonNode)
459 public void removeChild(int index
){
461 TaxonNode child
= childNodes
.get(index
);
462 child
= HibernateProxyHelper
.deproxy(child
, TaxonNode
.class); //strange that this is required, but otherwise child.getParent() returns null for some lazy-loaded items.
466 TaxonNode parent
= HibernateProxyHelper
.deproxy(child
.getParent(), TaxonNode
.class);
467 TaxonNode thisNode
= HibernateProxyHelper
.deproxy(this, TaxonNode
.class);
468 if(parent
!= null && parent
!= thisNode
){
469 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());
470 }else if (parent
== null){
471 throw new IllegalStateException("Parent of child is null in TaxonNode.removeChild(int). This should not happen.");
473 childNodes
.remove(index
);
474 child
.setClassification(null);
477 //TODO workaround (see sortIndex doc)
478 this.countChildren
= childNodes
.size();
479 child
.setParent(null);
481 updateSortIndex(childNodes
, index
);
482 child
.setSortIndex(null);
488 * Remove this taxonNode From its taxonomic parent
490 * @return true on success
492 public boolean delete(){
494 return classification
.deleteChildNode(this);
496 return getParent().deleteChildNode(this);
501 * Remove this taxonNode From its taxonomic parent
503 * @return true on success
505 public boolean delete(boolean deleteChildren
){
507 return classification
.deleteChildNode(this, deleteChildren
);
509 return getParent().deleteChildNode(this, deleteChildren
);
514 @Deprecated //for CDM lib internal use only, may be removed in future versions
515 public int treeId() {
516 if (this.classification
== null){
517 logger
.warn("TaxonNode has no classification. This should not happen."); //#3840
520 return this.classification
.getId();
526 * Sets the parent of this taxon node to the given parent. Cleans up references to
527 * old parents and sets the classification to the new parents classification
532 protected void setParentTreeNode(TaxonNode parent
, int index
){
533 // remove ourselves from the old parent
534 TaxonNode formerParent
= this.getParent();
536 if (formerParent
!= null){
537 //special case, child already exists for same parent
538 //FIXME document / check for correctness
539 if (formerParent
.equals(parent
)){
540 int currentIndex
= formerParent
.getChildNodes().indexOf(this);
541 if (currentIndex
!= -1 && currentIndex
< index
){
546 //remove from old parent
547 formerParent
.removeChildNode(this);
550 // set the new parent
553 // set the classification to the parents classification
554 Classification classification
= parent
.getClassification();
555 //FIXME also set the tree index here for performance reasons
556 setClassificationRecursively(classification
);
558 // add this node to the parent child nodes
559 List
<TaxonNode
> parentChildren
= parent
.getChildNodes();
560 if (parentChildren
.contains(this)){
562 if (parentChildren
.indexOf(this) < index
){
565 parentChildren
.remove(this);
566 parentChildren
.add(index
, this);
568 parentChildren
.add(index
, this);
573 //TODO workaround (see sortIndex doc)
574 updateSortIndex(parentChildren
, index
);
576 if (! this.getSortIndex().equals(index
)){
577 logger
.warn("index and sortindex are not equal");
580 // update the children count
581 parent
.setCountChildren(parent
.getChildNodes().size());
585 * As long as the sort index is not correctly handled through hibernate this is a workaround method
586 * to update the sort index manually
587 * @param parentChildren
590 private void updateSortIndex(List
<TaxonNode
> children
, int index
) {
591 for(int i
= index
; i
< children
.size(); i
++){
592 TaxonNode child
= children
.get(i
);
594 // child = CdmBase.deproxy(child, TaxonNode.class); //deproxy not needed as long as setSortIndex is protected or public #4200
595 child
.setSortIndex(i
);
597 String message
= "A node in a taxon tree must never be null but is (ParentId: %d; sort index: %d; index: %d; i: %d)";
598 throw new IllegalStateException(String
.format(message
, getId(), sortIndex
, index
, i
));
607 * Returns a set containing this node and all nodes that are descendants of this node
612 protected Set
<TaxonNode
> getDescendants(){
613 Set
<TaxonNode
> nodeSet
= new HashSet
<TaxonNode
>();
617 for(TaxonNode childNode
: getChildNodes()){
618 nodeSet
.addAll(childNode
.getDescendants());
625 * Returns a set containing a clone of this node and of all nodes that are descendants of this node
629 protected TaxonNode
cloneDescendants(){
631 TaxonNode clone
= (TaxonNode
)this.clone();
632 TaxonNode childClone
;
634 for(TaxonNode childNode
: getChildNodes()){
635 childClone
= (TaxonNode
) childNode
.clone();
636 for (TaxonNode childChild
:childNode
.getChildNodes()){
637 childClone
.addChildNode(childChild
.cloneDescendants(), childChild
.getReference(), childChild
.getMicroReference());
639 clone
.addChildNode(childClone
, childNode
.getReference(), childNode
.getMicroReference());
640 //childClone.addChildNode(childNode.cloneDescendants());
650 protected Set
<TaxonNode
> getAncestors(){
651 Set
<TaxonNode
> nodeSet
= new HashSet
<TaxonNode
>();
653 if(this.getParent() != null){
654 TaxonNode parent
= CdmBase
.deproxy(this.getParent(), TaxonNode
.class);
655 nodeSet
.addAll(parent
.getAncestors());
662 * Whether this TaxonNode is a direct child of the classification TreeNode
666 public boolean isTopmostNode(){
667 boolean parentCheck
= false;
668 boolean classificationCheck
= false;
670 if(getParent() != null) {
671 if(getParent().getTaxon() == null) {
677 // FIXME This should work but doesn't, due to missing sort indexes, can be removed after fixing #4200, #4098
678 if (classification
!= null){
679 classificationCheck
= classification
.getRootNode().getChildNodes().contains(this);
681 classificationCheck
= false;
684 // The following is just for logging purposes for the missing sort indexes problem
686 if(parentCheck
!= classificationCheck
) {
687 logger
.warn("isTopmost node check " + parentCheck
+ " not same as classificationCheck : " + classificationCheck
+ " for taxon node ");
688 if(this.getParent() != null) {
689 logger
.warn("-- with parent uuid " + this.getParent().getUuid().toString());
690 logger
.warn("-- with parent id " + this.getParent().getId());
691 for(TaxonNode node
: this.getParent().getChildNodes()) {
693 logger
.warn("-- child node is null");
694 } else if (node
.getTaxon() == null) {
695 logger
.warn("-- child node taxon is null");
698 logger
.warn("-- parent child count" + this.getParent().getChildNodes().size());
706 * Whether this TaxonNode is a descendant of the given TaxonNode
708 * Caution: use this method with care on big branches. -> performance and memory hungry
710 * Protip: Try solving your problem with the isAscendant method which traverses the tree in the
711 * other direction (up). It will always result in a rather small set of consecutive parents beeing
714 * TODO implement more efficiently without generating the set of descendants first
716 * @param possibleParent
717 * @return true if this is a descendant
720 public boolean isDescendant(TaxonNode possibleParent
){
721 return possibleParent
== null ?
false : possibleParent
.isAncestor(this);
725 * Whether this TaxonNode is an ascendant of the given TaxonNode
727 * @param possibleChild
728 * @return true if there are ascendants
731 public boolean isAncestor(TaxonNode possibleChild
){
732 return possibleChild
== null ?
false : possibleChild
.getAncestors().contains(this);
736 * Whether this taxon has child nodes
738 * @return true if the taxonNode has childNodes
742 public boolean hasChildNodes(){
743 return childNodes
.size() > 0;
746 //*********************** CLONE ********************************************************/
748 * Clones <i>this</i> taxon node. This is a shortcut that enables to create
749 * a new instance that differs only slightly from <i>this</i> taxon node by
750 * modifying only some of the attributes.<BR><BR>
751 * The child nodes are not copied.<BR>
752 * The taxon and parent are the same as for the original taxon node. <BR>
754 * @see eu.etaxonomy.cdm.model.media.IdentifiableEntity#clone()
755 * @see java.lang.Object#clone()
758 public Object
clone() {
761 result
= (TaxonNode
)super.clone();
762 result
.getTaxon().addTaxonNode(result
);
763 result
.childNodes
= new ArrayList
<TaxonNode
>();
764 result
.countChildren
= 0;
767 }catch (CloneNotSupportedException e
) {
768 logger
.warn("Object does not implement cloneable");
774 public boolean hasTaxon() {
775 return (taxon
!= null);