-// $Id$\r
-/**\r
-* Copyright (C) 2007 EDIT\r
-* European Distributed Institute of Taxonomy\r
-* http://www.e-taxonomy.eu\r
-*\r
-* The contents of this file are subject to the Mozilla Public License Version 1.1\r
-* See LICENSE.TXT at the top of this package for the full license terms.\r
-*/\r
-\r
-package eu.etaxonomy.cdm.model.taxon;\r
-\r
-import java.util.ArrayList;\r
-import java.util.HashSet;\r
-import java.util.List;\r
-import java.util.Set;\r
-\r
-import javax.persistence.Entity;\r
-import javax.persistence.FetchType;\r
-import javax.persistence.ManyToOne;\r
-import javax.persistence.OneToMany;\r
-import javax.persistence.OrderBy;\r
-import javax.persistence.OrderColumn;\r
-import javax.persistence.Transient;\r
-import javax.validation.constraints.Size;\r
-import javax.xml.bind.annotation.XmlAccessType;\r
-import javax.xml.bind.annotation.XmlAccessorType;\r
-import javax.xml.bind.annotation.XmlElement;\r
-import javax.xml.bind.annotation.XmlElementWrapper;\r
-import javax.xml.bind.annotation.XmlIDREF;\r
-import javax.xml.bind.annotation.XmlRootElement;\r
-import javax.xml.bind.annotation.XmlSchemaType;\r
-import javax.xml.bind.annotation.XmlType;\r
-\r
-import org.apache.log4j.Logger;\r
-import org.hibernate.annotations.Cascade;\r
-import org.hibernate.annotations.CascadeType;\r
-import org.hibernate.annotations.Index;\r
-import org.hibernate.annotations.Table;\r
-import org.hibernate.envers.Audited;\r
-import org.hibernate.search.annotations.ContainedIn;\r
-import org.hibernate.search.annotations.Indexed;\r
-import org.hibernate.search.annotations.IndexedEmbedded;\r
-\r
-import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;\r
-import eu.etaxonomy.cdm.model.common.AnnotatableEntity;\r
-import eu.etaxonomy.cdm.model.common.CdmBase;\r
-import eu.etaxonomy.cdm.model.common.ITreeNode;\r
-import eu.etaxonomy.cdm.model.reference.Reference;\r
-\r
-/**\r
- * @author a.mueller\r
- * @created 31.03.2009\r
- */\r
-@XmlAccessorType(XmlAccessType.FIELD)\r
-@XmlType(name = "TaxonNode", propOrder = {\r
- "classification",\r
- "taxon",\r
- "parent",\r
- "treeIndex",\r
- "sortIndex",\r
- "childNodes",\r
- "referenceForParentChildRelation",\r
- "microReferenceForParentChildRelation",\r
- "countChildren",\r
- "synonymToBeUsed"\r
-})\r
-@XmlRootElement(name = "TaxonNode")\r
-@Entity\r
-@Indexed(index = "eu.etaxonomy.cdm.model.taxon.TaxonNode")\r
-@Audited\r
-@Table(appliesTo="TaxonNode", indexes = { @Index(name = "taxonNodeTreeIndex", columnNames = { "treeIndex" }) })\r
-public class TaxonNode extends AnnotatableEntity implements ITaxonTreeNode, ITreeNode<TaxonNode>, Cloneable{\r
- private static final long serialVersionUID = -4743289894926587693L;\r
- private static final Logger logger = Logger.getLogger(TaxonNode.class);\r
-\r
- @XmlElement(name = "taxon")\r
- @XmlIDREF\r
- @XmlSchemaType(name = "IDREF")\r
- @ManyToOne(fetch = FetchType.LAZY)\r
- @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})\r
- @ContainedIn\r
- private Taxon taxon;\r
-\r
-\r
- @XmlElement(name = "parent")\r
- @XmlIDREF\r
- @XmlSchemaType(name = "IDREF")\r
- @ManyToOne(fetch = FetchType.LAZY)\r
- @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})\r
- private TaxonNode parent;\r
-\r
-\r
- @XmlElement(name = "treeIndex")\r
- @Size(max=255)\r
- private String treeIndex;\r
-\r
-\r
- @XmlElement(name = "classification")\r
- @XmlIDREF\r
- @XmlSchemaType(name = "IDREF")\r
- @ManyToOne(fetch = FetchType.LAZY)\r
- @Cascade({CascadeType.SAVE_UPDATE})\r
-// TODO @NotNull // avoids creating a UNIQUE key for this field\r
- @IndexedEmbedded\r
- private Classification classification;\r
-\r
- @XmlElementWrapper(name = "childNodes")\r
- @XmlElement(name = "childNode")\r
- @XmlIDREF\r
- @XmlSchemaType(name = "IDREF")\r
- //see https://dev.e-taxonomy.eu/trac/ticket/3722\r
- @OrderColumn(name="sortIndex")\r
- @OrderBy("sortIndex")\r
- @OneToMany(mappedBy="parent", fetch=FetchType.LAZY)\r
- @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})\r
- private List<TaxonNode> childNodes = new ArrayList<TaxonNode>();\r
-\r
- //see https://dev.e-taxonomy.eu/trac/ticket/3722\r
- //see https://dev.e-taxonomy.eu/trac/ticket/4200\r
- private Integer sortIndex = -1;\r
-\r
- @XmlElement(name = "reference")\r
- @XmlIDREF\r
- @XmlSchemaType(name = "IDREF")\r
- @ManyToOne(fetch = FetchType.LAZY)\r
- @Cascade({CascadeType.SAVE_UPDATE})\r
- private Reference<?> referenceForParentChildRelation;\r
-\r
- @XmlElement(name = "microReference")\r
- private String microReferenceForParentChildRelation;\r
-\r
- @XmlElement(name = "countChildren")\r
- private int countChildren;\r
-\r
-// private Taxon originalConcept;\r
-// //or\r
- @XmlElement(name = "synonymToBeUsed")\r
- @XmlIDREF\r
- @XmlSchemaType(name = "IDREF")\r
- @ManyToOne(fetch = FetchType.LAZY)\r
- @Cascade({CascadeType.SAVE_UPDATE})\r
- private Synonym synonymToBeUsed;\r
-\r
-// ******************** CONSTRUCTOR **********************************************/\r
-\r
- protected TaxonNode(){super();}\r
-\r
- /**\r
- * to create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}\r
- * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}\r
- * @param taxon\r
- * @param classification\r
- * @deprecated setting of classification is handled in the addTaxonNode() method,\r
- * use TaxonNode(taxon) instead\r
- */\r
- @Deprecated\r
- protected TaxonNode (Taxon taxon, Classification classification){\r
- this(taxon);\r
- setClassification(classification);\r
- }\r
-\r
- /**\r
- * to create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}\r
- * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}\r
- *\r
- * @param taxon\r
- */\r
- protected TaxonNode(Taxon taxon){\r
- setTaxon(taxon);\r
- }\r
-\r
-// ************************* GETTER / SETTER *******************************/\r
- \r
- public Integer getSortIndex() {\r
- return sortIndex;\r
- }\r
- /**\r
- * SortIndex shall be handled only internally, therefore not public.\r
- * However, as javaassist only supports protected methods it needs to be protected, not private.\r
- * Alternatively we could use deproxy on every call of this method (see commented code)\r
- * @param i\r
- * @return\r
- * @deprecated for internal use only\r
- */\r
- protected void setSortIndex(Integer i) { \r
-// CdmBase.deproxy(this, TaxonNode.class).sortIndex = i; //alternative solution for private, DON'T remove\r
- sortIndex = i;\r
- }\r
- \r
-\r
- public Taxon getTaxon() {\r
- return taxon;\r
- }\r
- protected void setTaxon(Taxon taxon) {\r
- this.taxon = taxon;\r
- if (taxon != null){\r
- taxon.addTaxonNode(this);\r
- }\r
- }\r
- \r
-\r
- @Override\r
- public List<TaxonNode> getChildNodes() {\r
- return childNodes;\r
- }\r
- protected void setChildNodes(List<TaxonNode> childNodes) {\r
- this.childNodes = childNodes; \r
- }\r
-\r
- \r
- public Classification getClassification() {\r
- return classification;\r
- }\r
- /**\r
- * THIS METHOD SHOULD NOT BE CALLED!\r
- * invisible part of the bidirectional relationship, for public use TaxonomicView.addRoot() or TaxonNode.addChild()\r
- * @param classification\r
- * @deprecated for internal use only\r
- */\r
- protected void setClassification(Classification classification) {\r
- this.classification = classification;\r
- }\r
- \r
-\r
- @Override\r
- public String getMicroReference() {\r
- return microReferenceForParentChildRelation;\r
- }\r
- public void setMicroReference(String microReference) {\r
- this.microReferenceForParentChildRelation = microReference;\r
- }\r
- \r
-\r
- @Override\r
- public Reference getReference() {\r
- return referenceForParentChildRelation;\r
- }\r
- public void setReference(Reference reference) {\r
- this.referenceForParentChildRelation = reference;\r
- }\r
-\r
- //countChildren\r
- public int getCountChildren() {\r
- return countChildren;\r
- }\r
- /**\r
- * @deprecated for internal use only\r
- * @param countChildren\r
- */\r
- protected void setCountChildren(int countChildren) {\r
- this.countChildren = countChildren;\r
- }\r
- \r
-\r
- //parent\r
- @Override\r
- public TaxonNode getParent(){\r
- return parent;\r
- }\r
- /**\r
- * Sets the parent of this taxon node.<BR>\r
- *\r
- * In most cases you would want to call setParentTreeNode(ITreeNode) which\r
- * handles updating of the bidirectional relationship\r
- *\r
- * @see setParentTreeNode(ITreeNode)\r
- * @param parent\r
- *\r
- */\r
- protected void setParent(TaxonNode parent) {\r
- this.parent = parent;\r
- }\r
- \r
-\r
- //synonymToBeused\r
- public Synonym getSynonymToBeUsed() {\r
- return synonymToBeUsed;\r
- }\r
- public void setSynonymToBeUsed(Synonym synonymToBeUsed) {\r
- this.synonymToBeUsed = synonymToBeUsed;\r
- }\r
- \r
-\r
- //treeindex\r
- @Override\r
- public String treeIndex() {\r
- return treeIndex;\r
- }\r
- @Override\r
- @Deprecated //for CDM lib internal use only, may be removed in future versions\r
- public void setTreeIndex(String treeIndex) {\r
- this.treeIndex = treeIndex;\r
- }\r
- \r
-\r
-\r
-//************************ METHODS **************************/\r
-\r
- @Override\r
- public TaxonNode addChildTaxon(Taxon taxon, Reference citation, String microCitation) {\r
- return addChildTaxon(taxon, this.childNodes.size(), citation, microCitation);\r
- }\r
-\r
-\r
- @Override\r
- public TaxonNode addChildTaxon(Taxon taxon, int index, Reference citation, String microCitation) {\r
- if (this.getClassification().isTaxonInTree(taxon)){\r
- throw new IllegalArgumentException(String.format("Taxon may not be in a classification twice: %s", taxon.getTitleCache()));\r
- }\r
- return addChildNode(new TaxonNode(taxon), index, citation, microCitation);\r
- }\r
-\r
- /**\r
- * Moves a taxon node to a new parent. Descendents of the node are moved as well\r
- *\r
- * @param childNode the taxon node to be moved to the new parent\r
- * @return the child node in the state of having a new parent\r
- */\r
- @Override\r
- public TaxonNode addChildNode(TaxonNode childNode, Reference reference, String microReference){\r
- addChildNode(childNode, childNodes.size(), reference, microReference);\r
- return childNode;\r
- }\r
-\r
- /**\r
- * Inserts the given taxon node in the list of children of <i>this</i> taxon node\r
- * at the given (index + 1) position. If the given index is out of bounds\r
- * an exception will arise.<BR>\r
- * Due to bidirectionality this method must also assign <i>this</i> taxon node\r
- * as the parent of the given child.\r
- *\r
- * @param child the taxon node to be added\r
- * @param index the integer indicating the position at which the child\r
- * should be added\r
- * @see #getChildNodes()\r
- * @see #addChildNode(TaxonNode, Reference, String, Synonym)\r
- * @see #deleteChildNode(TaxonNode)\r
- * @see #deleteChildNode(int)\r
- */\r
- @Override\r
- public TaxonNode addChildNode(TaxonNode child, int index, Reference reference, String microReference){\r
- if (index < 0 || index > childNodes.size() + 1){\r
- throw new IndexOutOfBoundsException("Wrong index: " + index);\r
- }\r
- // check if this node is a descendant of the childNode\r
- if(child.getParent() != this && child.isAncestor(this)){\r
- throw new IllegalAncestryException("New parent node is a descendant of the node to be moved.");\r
- }\r
-\r
- child.setParentTreeNode(this, index); \r
- \r
- child.setReference(reference);\r
- child.setMicroReference(microReference);\r
-\r
- return child;\r
- }\r
-\r
- /**\r
- * Sets this nodes classification. Updates classification of child nodes recursively.\r
- *\r
- * If the former and the actual tree are equal() this method does nothing.\r
- * \r
- * @throws IllegalArgumentException if newClassifciation is null\r
- *\r
- * @param newClassification\r
- */\r
- @Transient\r
- private void setClassificationRecursively(Classification newClassification) {\r
- if (newClassification == null){\r
- throw new IllegalArgumentException("New Classification must not be 'null' when setting new classification.");\r
- }\r
- if(! newClassification.equals(this.getClassification())){\r
- this.setClassification(newClassification);\r
- for(TaxonNode childNode : this.getChildNodes()){\r
- childNode.setClassificationRecursively(newClassification);\r
- }\r
- }\r
- }\r
-\r
- @Override\r
- public boolean deleteChildNode(TaxonNode node) {\r
- boolean result = removeChildNode(node);\r
- Taxon taxon = node.getTaxon();\r
- node.setTaxon(null);\r
- taxon.removeTaxonNode(node);\r
-\r
- ArrayList<TaxonNode> childNodes = new ArrayList<TaxonNode>(node.getChildNodes());\r
- for(TaxonNode childNode : childNodes){\r
- node.deleteChildNode(childNode);\r
- }\r
-\r
- return result;\r
- }\r
-\r
- /**\r
- * Deletes the child node and also removes children of childnode \r
- * recursively if delete children is <code>true</code>\r
- * @param node\r
- * @param deleteChildren\r
- * @return\r
- */\r
- public boolean deleteChildNode(TaxonNode node, boolean deleteChildren) {\r
- boolean result = removeChildNode(node);\r
- Taxon taxon = node.getTaxon();\r
- node.setTaxon(null);\r
- taxon.removeTaxonNode(node);\r
- if (deleteChildren){\r
- ArrayList<TaxonNode> childNodes = new ArrayList<TaxonNode>(node.getChildNodes());\r
- for(TaxonNode childNode : childNodes){\r
- node.deleteChildNode(childNode, deleteChildren);\r
- }\r
- } else{\r
- ArrayList<TaxonNode> childNodes = new ArrayList<TaxonNode>(node.getChildNodes());\r
- for(TaxonNode childNode : childNodes){\r
- this.addChildNode(childNode, null, null);\r
- }\r
- }\r
-\r
- return result;\r
- }\r
-\r
- /**\r
- * Removes the child node from this node. Sets the parent and the classification of the child\r
- * node to null\r
- *\r
- * @param childNode\r
- * @return\r
- */\r
- protected boolean removeChildNode(TaxonNode childNode){\r
- boolean result = true;\r
-\r
- if(childNode == null){\r
- throw new IllegalArgumentException("TaxonNode may not be null");\r
- }\r
- int index = childNodes.indexOf(childNode);\r
- if (index >= 0){\r
- removeChild(index);\r
- } else {\r
- result = false;\r
- }\r
- return result;\r
- }\r
-\r
- /**\r
- * Removes the child node placed at the given (index + 1) position\r
- * from the list of {@link #getChildNodes() children} of <i>this</i> taxon node.\r
- * Sets the parent and the classification of the child\r
- * node to null.\r
- * If the given index is out of bounds no child will be removed.\r
- *\r
- * @param index the integer indicating the position of the taxon node to\r
- * be removed\r
- * @see #getChildNodes()\r
- * @see #addChildNode(TaxonNode, Reference, String)\r
- * @see #addChildNode(TaxonNode, int, Reference, String)\r
- * @see #deleteChildNode(TaxonNode)\r
- */\r
- public void removeChild(int index){\r
-\r
- TaxonNode child = childNodes.get(index);\r
- child = HibernateProxyHelper.deproxy(child, TaxonNode.class); //strange that this is required, but otherwise child.getParent() returns null for some lazy-loaded items.\r
-\r
- if (child != null){\r
-\r
- TaxonNode parent = HibernateProxyHelper.deproxy(child.getParent(), TaxonNode.class);\r
- TaxonNode thisNode = HibernateProxyHelper.deproxy(this, TaxonNode.class);\r
- if(parent != null && parent != thisNode){\r
- 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());\r
- }else if (parent == null){\r
- throw new IllegalStateException("Parent of child is null in TaxonNode.removeChild(int). This should not happen.");\r
- }\r
- childNodes.remove(index);\r
- child.setClassification(null);\r
- \r
- //update sortindex\r
- //TODO workaround (see sortIndex doc)\r
- this.countChildren = childNodes.size();\r
- child.setParent(null);\r
- \r
- updateSortIndex(childNodes, index);\r
- child.setSortIndex(null);\r
- }\r
- }\r
-\r
-\r
- /**\r
- * Remove this taxonNode From its taxonomic parent\r
- *\r
- * @return true on success\r
- */\r
- public boolean delete(){\r
- if(isTopmostNode()){\r
- return classification.deleteChildNode(this);\r
- }else{\r
- return getParent().deleteChildNode(this);\r
- }\r
- }\r
-\r
- /**\r
- * Remove this taxonNode From its taxonomic parent\r
- *\r
- * @return true on success\r
- */\r
- public boolean delete(boolean deleteChildren){\r
- if(isTopmostNode()){\r
- return classification.deleteChildNode(this, deleteChildren);\r
- }else{\r
- return getParent().deleteChildNode(this, deleteChildren);\r
- }\r
- }\r
-\r
- @Override\r
- @Deprecated //for CDM lib internal use only, may be removed in future versions\r
- public int treeId() {\r
- if (this.classification == null){\r
- logger.warn("TaxonNode has no classification. This should not happen."); //#3840\r
- return -1;\r
- }else{\r
- return this.classification.getId();\r
- }\r
- }\r
-\r
-\r
- /**\r
- * Sets the parent of this taxon node to the given parent. Cleans up references to\r
- * old parents and sets the classification to the new parents classification\r
- *\r
- * @param parent\r
- */\r
- @Transient\r
- protected void setParentTreeNode(TaxonNode parent, int index){\r
- // remove ourselves from the old parent\r
- TaxonNode formerParent = this.getParent();\r
-\r
- if (formerParent != null){\r
- //special case, child already exists for same parent\r
- //FIXME document / check for correctness\r
- if (formerParent.equals(parent)){\r
- int currentIndex = formerParent.getChildNodes().indexOf(this);\r
- if (currentIndex != -1 && currentIndex < index){\r
- index--;\r
- }\r
- }\r
- \r
- //remove from old parent\r
- formerParent.removeChildNode(this);\r
- }\r
- \r
- // set the new parent\r
- setParent(parent);\r
-\r
- // set the classification to the parents classification\r
- Classification classification = parent.getClassification();\r
- //FIXME also set the tree index here for performance reasons\r
- setClassificationRecursively(classification);\r
-\r
- // add this node to the parent child nodes\r
- List<TaxonNode> parentChildren = parent.getChildNodes();\r
- if (parentChildren.contains(this)){\r
- //avoid duplicates\r
- if (parentChildren.indexOf(this) < index){\r
- index = index -1;\r
- }\r
- parentChildren.remove(this);\r
- parentChildren.add(index, this);\r
- }else{\r
- parentChildren.add(index, this);\r
- }\r
-\r
- \r
- //sortIndex\r
- //TODO workaround (see sortIndex doc)\r
- updateSortIndex(parentChildren, index);\r
- //only for debugging\r
- if (! this.getSortIndex().equals(index)){\r
- logger.warn("index and sortindex are not equal");\r
- }\r
- \r
- // update the children count\r
- parent.setCountChildren(parent.getChildNodes().size());\r
- }\r
- \r
- /**\r
- * As long as the sort index is not correctly handled through hibernate this is a workaround method\r
- * to update the sort index manually\r
- * @param parentChildren\r
- * @param index\r
- */\r
- private void updateSortIndex(List<TaxonNode> children, int index) {\r
- for(int i = index; i < children.size(); i++){\r
- TaxonNode child = children.get(i);\r
- if (child != null){\r
-// child = CdmBase.deproxy(child, TaxonNode.class); //deproxy not needed as long as setSortIndex is protected or public #4200\r
- child.setSortIndex(i);\r
- }else{\r
- String message = "A node in a taxon tree must never be null but is (ParentId: %d; sort index: %d; index: %d; i: %d)";\r
- throw new IllegalStateException(String.format(message, getId(), sortIndex, index, i));\r
- }\r
- }\r
- }\r
-\r
-\r
-\r
-\r
- /**\r
- * Returns a set containing this node and all nodes that are descendants of this node\r
- *\r
- * @return\r
- */\r
- \r
- protected Set<TaxonNode> getDescendants(){\r
- Set<TaxonNode> nodeSet = new HashSet<TaxonNode>();\r
-\r
- nodeSet.add(this);\r
-\r
- for(TaxonNode childNode : getChildNodes()){\r
- nodeSet.addAll(childNode.getDescendants());\r
- }\r
-\r
- return nodeSet;\r
- }\r
-\r
- /**\r
- * Returns a set containing a clone of this node and of all nodes that are descendants of this node\r
- *\r
- * @return\r
- */\r
- protected TaxonNode cloneDescendants(){\r
-\r
- TaxonNode clone = (TaxonNode)this.clone();\r
- TaxonNode childClone;\r
-\r
- for(TaxonNode childNode : getChildNodes()){\r
- childClone = (TaxonNode) childNode.clone();\r
- for (TaxonNode childChild:childNode.getChildNodes()){\r
- childClone.addChildNode(childChild.cloneDescendants(), childChild.getReference(), childChild.getMicroReference());\r
- }\r
- clone.addChildNode(childClone, childNode.getReference(), childNode.getMicroReference());\r
- //childClone.addChildNode(childNode.cloneDescendants());\r
- }\r
- return clone;\r
- }\r
-\r
- /**\r
- * Returns a\r
- *\r
- * @return\r
- */\r
- protected Set<TaxonNode> getAncestors(){\r
- Set<TaxonNode> nodeSet = new HashSet<TaxonNode>();\r
- nodeSet.add(this);\r
- if(this.getParent() != null){\r
- TaxonNode parent = CdmBase.deproxy(this.getParent(), TaxonNode.class);\r
- nodeSet.addAll(parent.getAncestors());\r
- }\r
- return nodeSet;\r
- }\r
-\r
-\r
- /**\r
- * Whether this TaxonNode is a direct child of the classification TreeNode\r
- * @return\r
- */\r
- @Transient\r
- public boolean isTopmostNode(){\r
- boolean parentCheck = false;\r
- boolean classificationCheck = false;\r
-\r
- if(getParent() != null) {\r
- if(getParent().getTaxon() == null) {\r
- parentCheck = true;\r
- }\r
- }\r
-\r
- //TODO remove\r
- // FIXME This should work but doesn't, due to missing sort indexes, can be removed after fixing #4200, #4098\r
- if (classification != null){ \r
- classificationCheck = classification.getRootNode().getChildNodes().contains(this);\r
- }else{\r
- classificationCheck = false;\r
- }\r
-\r
- // The following is just for logging purposes for the missing sort indexes problem\r
- // ticket #4098\r
- if(parentCheck != classificationCheck) { \r
- logger.warn("isTopmost node check " + parentCheck + " not same as classificationCheck : " + classificationCheck + " for taxon node "); \r
- if(this.getParent() != null) {\r
- logger.warn("-- with parent uuid " + this.getParent().getUuid().toString());\r
- logger.warn("-- with parent id " + this.getParent().getId());\r
- for(TaxonNode node : this.getParent().getChildNodes()) {\r
- if(node == null) {\r
- logger.warn("-- child node is null");\r
- } else if (node.getTaxon() == null) {\r
- logger.warn("-- child node taxon is null");\r
- } \r
- }\r
- logger.warn("-- parent child count" + this.getParent().getChildNodes().size()); \r
- } \r
- }\r
-\r
- return parentCheck;\r
- }\r
-\r
- /**\r
- * Whether this TaxonNode is a descendant of the given TaxonNode\r
- *\r
- * Caution: use this method with care on big branches. -> performance and memory hungry\r
- *\r
- * Protip: Try solving your problem with the isAscendant method which traverses the tree in the\r
- * other direction (up). It will always result in a rather small set of consecutive parents beeing\r
- * generated.\r
- *\r
- * TODO implement more efficiently without generating the set of descendants first\r
- *\r
- * @param possibleParent\r
- * @return true if this is a descendant\r
- */\r
- @Transient\r
- public boolean isDescendant(TaxonNode possibleParent){\r
- return possibleParent == null ? false : possibleParent.isAncestor(this);\r
- }\r
-\r
- /**\r
- * Whether this TaxonNode is an ascendant of the given TaxonNode\r
- *\r
- * @param possibleChild\r
- * @return true if there are ascendants\r
- */\r
- @Transient\r
- public boolean isAncestor(TaxonNode possibleChild){\r
- return possibleChild == null ? false : possibleChild.getAncestors().contains(this);\r
- }\r
-\r
- /**\r
- * Whether this taxon has child nodes\r
- *\r
- * @return true if the taxonNode has childNodes\r
- */\r
- @Transient\r
- @Override\r
- public boolean hasChildNodes(){\r
- return childNodes.size() > 0;\r
- }\r
-\r
-//*********************** CLONE ********************************************************/\r
- /**\r
- * Clones <i>this</i> taxon node. This is a shortcut that enables to create\r
- * a new instance that differs only slightly from <i>this</i> taxon node by\r
- * modifying only some of the attributes.<BR><BR>\r
- * The child nodes are not copied.<BR>\r
- * The taxon and parent are the same as for the original taxon node. <BR>\r
- *\r
- * @see eu.etaxonomy.cdm.model.media.IdentifiableEntity#clone()\r
- * @see java.lang.Object#clone()\r
- */\r
- @Override\r
- public Object clone() {\r
- TaxonNode result;\r
- try{\r
- result = (TaxonNode)super.clone();\r
- result.getTaxon().addTaxonNode(result);\r
- result.childNodes = new ArrayList<TaxonNode>();\r
- result.countChildren = 0;\r
-\r
- return result;\r
- }catch (CloneNotSupportedException e) {\r
- logger.warn("Object does not implement cloneable");\r
- e.printStackTrace();\r
- return null;\r
- }\r
- }\r
-\r
- public boolean hasTaxon() {\r
- return (taxon!= null);\r
- }\r
-\r
-\r
-\r
-}\r
+// $Id$
+/**
+* Copyright (C) 2007 EDIT
+* European Distributed Institute of Taxonomy
+* http://www.e-taxonomy.eu
+*
+* The contents of this file are subject to the Mozilla Public License Version 1.1
+* See LICENSE.TXT at the top of this package for the full license terms.
+*/
+
+package eu.etaxonomy.cdm.model.taxon;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.ManyToOne;
+import javax.persistence.OneToMany;
+import javax.persistence.OrderBy;
+import javax.persistence.OrderColumn;
+import javax.persistence.Transient;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+import javax.xml.bind.annotation.XmlElement;
+import javax.xml.bind.annotation.XmlElementWrapper;
+import javax.xml.bind.annotation.XmlIDREF;
+import javax.xml.bind.annotation.XmlRootElement;
+import javax.xml.bind.annotation.XmlSchemaType;
+import javax.xml.bind.annotation.XmlType;
+
+import org.apache.log4j.Logger;
+import org.hibernate.LazyInitializationException;
+import org.hibernate.annotations.Cascade;
+import org.hibernate.annotations.CascadeType;
+import org.hibernate.annotations.Index;
+import org.hibernate.annotations.Table;
+import org.hibernate.envers.Audited;
+import org.hibernate.search.annotations.ContainedIn;
+import org.hibernate.search.annotations.IndexedEmbedded;
+
+import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
+import eu.etaxonomy.cdm.model.agent.TeamOrPersonBase;
+import eu.etaxonomy.cdm.model.common.AnnotatableEntity;
+import eu.etaxonomy.cdm.model.common.CdmBase;
+import eu.etaxonomy.cdm.model.common.DefinedTerm;
+import eu.etaxonomy.cdm.model.common.ITreeNode;
+import eu.etaxonomy.cdm.model.name.Rank;
+import eu.etaxonomy.cdm.model.name.TaxonNameBase;
+import eu.etaxonomy.cdm.model.reference.Reference;
+import eu.etaxonomy.cdm.validation.Level3;
+import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustBeLowerRankThanParent;
+import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustDeriveNameFromParent;
+import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustNotSkipRanks;
+
+/**
+ * @author a.mueller
+ * @created 31.03.2009
+ */
+@XmlAccessorType(XmlAccessType.FIELD)
+@XmlType(name = "TaxonNode", propOrder = {
+ "classification",
+ "taxon",
+ "parent",
+ "treeIndex",
+ "sortIndex",
+ "childNodes",
+ "referenceForParentChildRelation",
+ "microReferenceForParentChildRelation",
+ "countChildren",
+ "agentRelations",
+ "synonymToBeUsed"
+})
+@XmlRootElement(name = "TaxonNode")
+@Entity
+//@Indexed disabled to reduce clutter in indexes, since this type is not used by any search
+//@Indexed(index = "eu.etaxonomy.cdm.model.taxon.TaxonNode")
+@Audited
+@Table(appliesTo="TaxonNode", indexes = { @Index(name = "taxonNodeTreeIndex", columnNames = { "treeIndex" }) })
+@ChildTaxaMustBeLowerRankThanParent(groups = Level3.class)
+@ChildTaxaMustNotSkipRanks(groups = Level3.class)
+@ChildTaxaMustDeriveNameFromParent(groups = Level3.class)
+public class TaxonNode extends AnnotatableEntity implements ITaxonTreeNode, ITreeNode<TaxonNode>, Cloneable{
+ private static final long serialVersionUID = -4743289894926587693L;
+ private static final Logger logger = Logger.getLogger(TaxonNode.class);
+
+ @XmlElement(name = "taxon")
+ @XmlIDREF
+ @XmlSchemaType(name = "IDREF")
+ @ManyToOne(fetch = FetchType.LAZY)
+ @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
+ @ContainedIn
+ private Taxon taxon;
+
+
+ @XmlElement(name = "parent")
+ @XmlIDREF
+ @XmlSchemaType(name = "IDREF")
+ @ManyToOne(fetch = FetchType.LAZY)
+ @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
+ private TaxonNode parent;
+
+
+ @XmlElement(name = "treeIndex")
+ @Column(length=255)
+ private String treeIndex;
+
+
+ @XmlElement(name = "classification")
+ @XmlIDREF
+ @XmlSchemaType(name = "IDREF")
+ @ManyToOne(fetch = FetchType.LAZY)
+ @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
+// TODO @NotNull // avoids creating a UNIQUE key for this field
+ @IndexedEmbedded(includeEmbeddedObjectId=true)
+ private Classification classification;
+
+ @XmlElementWrapper(name = "childNodes")
+ @XmlElement(name = "childNode")
+ @XmlIDREF
+ @XmlSchemaType(name = "IDREF")
+ //see https://dev.e-taxonomy.eu/trac/ticket/3722
+ @OrderColumn(name="sortIndex")
+ @OrderBy("sortIndex")
+ @OneToMany(mappedBy="parent", fetch=FetchType.LAZY)
+ @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
+ private List<TaxonNode> childNodes = new ArrayList<TaxonNode>();
+
+ //see https://dev.e-taxonomy.eu/trac/ticket/3722
+ //see https://dev.e-taxonomy.eu/trac/ticket/4200
+ private Integer sortIndex = -1;
+
+ @XmlElement(name = "reference")
+ @XmlIDREF
+ @XmlSchemaType(name = "IDREF")
+ @ManyToOne(fetch = FetchType.LAZY)
+ @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
+ private Reference<?> referenceForParentChildRelation;
+
+ @XmlElement(name = "microReference")
+ private String microReferenceForParentChildRelation;
+
+ @XmlElement(name = "countChildren")
+ private int countChildren;
+
+ @XmlElementWrapper(name = "agentRelations")
+ @XmlElement(name = "agentRelation")
+ @XmlIDREF
+ @XmlSchemaType(name = "IDREF")
+ @OneToMany(mappedBy="taxonNode", fetch=FetchType.LAZY)
+ @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
+ private Set<TaxonNodeAgentRelation> agentRelations = new HashSet<TaxonNodeAgentRelation>();
+
+
+// private Taxon originalConcept;
+// //or
+ @XmlElement(name = "synonymToBeUsed")
+ @XmlIDREF
+ @XmlSchemaType(name = "IDREF")
+ @ManyToOne(fetch = FetchType.LAZY)
+ @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
+ private Synonym synonymToBeUsed;
+
+// ******************** CONSTRUCTOR **********************************************/
+
+ protected TaxonNode(){super();}
+
+ /**
+ * to create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}
+ * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}
+ * @param taxon
+ * @param classification
+ * @deprecated setting of classification is handled in the addTaxonNode() method,
+ * use TaxonNode(taxon) instead
+ */
+ @Deprecated
+ protected TaxonNode (Taxon taxon, Classification classification){
+ this(taxon);
+ setClassification(classification);
+ }
+
+ /**
+ * to create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}
+ * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}
+ *
+ * @param taxon
+ */
+ protected TaxonNode(Taxon taxon){
+ setTaxon(taxon);
+ }
+
+// ************************* GETTER / SETTER *******************************/
+
+ @Transient
+ public Integer getSortIndex() {
+ TaxonNode parent = HibernateProxyHelper.deproxy(this.parent, TaxonNode.class);
+ parent.removeNullValueFromChildren();
+
+ return sortIndex;
+ }
+ /**
+ * SortIndex shall be handled only internally, therefore not public.
+ * However, as javaassist only supports protected methods it needs to be protected, not private.
+ * Alternatively we could use deproxy on every call of this method (see commented code)
+ * @param i
+ * @return
+ * @deprecated for internal use only
+ */
+ @Deprecated
+ protected void setSortIndex(Integer i) {
+// CdmBase.deproxy(this, TaxonNode.class).sortIndex = i; //alternative solution for private, DON'T remove
+ sortIndex = i;
+ }
+
+
+ public Taxon getTaxon() {
+ return taxon;
+ }
+ protected void setTaxon(Taxon taxon) {
+ this.taxon = taxon;
+ if (taxon != null){
+ taxon.addTaxonNode(this);
+ }
+ }
+
+
+ @Override
+ public List<TaxonNode> getChildNodes() {
+
+ return childNodes;
+ }
+ protected void setChildNodes(List<TaxonNode> childNodes) {
+ this.childNodes = childNodes;
+ }
+
+
+ public Classification getClassification() {
+ return classification;
+ }
+ /**
+ * THIS METHOD SHOULD NOT BE CALLED!
+ * invisible part of the bidirectional relationship, for public use TaxonomicView.addRoot() or TaxonNode.addChild()
+ * @param classification
+ * @deprecated for internal use only
+ */
+ @Deprecated
+ protected void setClassification(Classification classification) {
+ this.classification = classification;
+ }
+
+
+ @Override
+ public String getMicroReference() {
+ return microReferenceForParentChildRelation;
+ }
+ public void setMicroReference(String microReference) {
+ this.microReferenceForParentChildRelation = microReference;
+ }
+
+
+ @Override
+ public Reference getReference() {
+ return referenceForParentChildRelation;
+ }
+ public void setReference(Reference reference) {
+ this.referenceForParentChildRelation = reference;
+ }
+
+ //countChildren
+ public int getCountChildren() {
+ return countChildren;
+ }
+ /**
+ * @deprecated for internal use only
+ * @param countChildren
+ */
+ @Deprecated
+ protected void setCountChildren(int countChildren) {
+ this.countChildren = countChildren;
+ }
+
+
+ //parent
+ @Override
+ public TaxonNode getParent(){
+ return parent;
+ }
+ /**
+ * Sets the parent of this taxon node.<BR>
+ *
+ * In most cases you would want to call setParentTreeNode(ITreeNode) which
+ * handles updating of the bidirectional relationship
+ *
+ * @see setParentTreeNode(ITreeNode)
+ * @param parent
+ *
+ */
+ protected void setParent(TaxonNode parent) {
+ this.parent = parent;
+ }
+
+// ****************** Agent Relations ****************************/
+
+
+ /**
+ * @return
+ */
+ public Set<TaxonNodeAgentRelation> getAgentRelations() {
+ return this.agentRelations;
+ }
+
+ public TaxonNodeAgentRelation addAgentRelation(DefinedTerm type, TeamOrPersonBase<?> agent){
+ TaxonNodeAgentRelation result = TaxonNodeAgentRelation.NewInstance(this, agent, type);
+ return result;
+ }
+ /**
+ * @param nodeAgentRelation
+ */
+ protected void addAgentRelation(TaxonNodeAgentRelation agentRelation) {
+ agentRelation.setTaxonNode(this);
+ this.agentRelations.add(agentRelation);
+ }
+
+ /**
+ * @param nodeAgentRelation
+ */
+ public void removeNodeAgent(TaxonNodeAgentRelation agentRelation) {
+ agentRelation.setTaxonNode(this);
+ agentRelations.remove(agentRelation);
+ }
+
+//********************
+
+ //synonymToBeused
+ public Synonym getSynonymToBeUsed() {
+ return synonymToBeUsed;
+ }
+ public void setSynonymToBeUsed(Synonym synonymToBeUsed) {
+ this.synonymToBeUsed = synonymToBeUsed;
+ }
+
+
+ //treeindex
+ @Override
+ public String treeIndex() {
+ return treeIndex;
+ }
+ @Override
+ @Deprecated //for CDM lib internal use only, may be removed in future versions
+ public void setTreeIndex(String treeIndex) {
+ this.treeIndex = treeIndex;
+ }
+
+
+
+//************************ METHODS **************************/
+
+ @Override
+ public TaxonNode addChildTaxon(Taxon taxon, Reference citation, String microCitation) {
+ return addChildTaxon(taxon, this.childNodes.size(), citation, microCitation);
+ }
+
+
+ @Override
+ public TaxonNode addChildTaxon(Taxon taxon, int index, Reference citation, String microCitation) {
+ Classification classification = HibernateProxyHelper.deproxy(this.getClassification(), Classification.class);
+ taxon = HibernateProxyHelper.deproxy(taxon, Taxon.class);
+ if (this.getClassification().isTaxonInTree(taxon)){
+ throw new IllegalArgumentException(String.format("Taxon may not be in a classification twice: %s", taxon.getTitleCache()));
+ }
+ return addChildNode(new TaxonNode(taxon), index, citation, microCitation);
+ }
+
+ /**
+ * Moves a taxon node to a new parent. Descendents of the node are moved as well
+ *
+ * @param childNode the taxon node to be moved to the new parent
+ * @return the child node in the state of having a new parent
+ */
+ @Override
+ public TaxonNode addChildNode(TaxonNode childNode, Reference reference, String microReference){
+ addChildNode(childNode, childNodes.size(), reference, microReference);
+ return childNode;
+ }
+
+ /**
+ * Inserts the given taxon node in the list of children of <i>this</i> taxon node
+ * at the given (index + 1) position. If the given index is out of bounds
+ * an exception will arise.<BR>
+ * Due to bidirectionality this method must also assign <i>this</i> taxon node
+ * as the parent of the given child.
+ *
+ * @param child the taxon node to be added
+ * @param index the integer indicating the position at which the child
+ * should be added
+ * @see #getChildNodes()
+ * @see #addChildNode(TaxonNode, Reference, String, Synonym)
+ * @see #deleteChildNode(TaxonNode)
+ * @see #deleteChildNode(int)
+ */
+ @Override
+ public TaxonNode addChildNode(TaxonNode child, int index, Reference reference, String microReference){
+ if (index < 0 || index > childNodes.size() + 1){
+ throw new IndexOutOfBoundsException("Wrong index: " + index);
+ }
+ // check if this node is a descendant of the childNode
+ if(child.getParent() != this && child.isAncestor(this)){
+ throw new IllegalAncestryException("New parent node is a descendant of the node to be moved.");
+ }
+
+ child.setParentTreeNode(this, index);
+
+ child.setReference(reference);
+ child.setMicroReference(microReference);
+
+ return child;
+ }
+
+ /**
+ * Sets this nodes classification. Updates classification of child nodes recursively.
+ *
+ * If the former and the actual tree are equal() this method does nothing.
+ *
+ * @throws IllegalArgumentException if newClassifciation is null
+ *
+ * @param newClassification
+ */
+ @Transient
+ private void setClassificationRecursively(Classification newClassification) {
+ if (newClassification == null){
+ throw new IllegalArgumentException("New Classification must not be 'null' when setting new classification.");
+ }
+ if(! newClassification.equals(this.getClassification())){
+ this.setClassification(newClassification);
+ for(TaxonNode childNode : this.getChildNodes()){
+ childNode.setClassificationRecursively(newClassification);
+ }
+ }
+ }
+
+ @Override
+ public boolean deleteChildNode(TaxonNode node) {
+ boolean result = removeChildNode(node);
+ Taxon taxon = node.getTaxon();
+ node.setTaxon(null);
+ taxon.removeTaxonNode(node);
+
+ ArrayList<TaxonNode> childNodes = new ArrayList<TaxonNode>(node.getChildNodes());
+ for(TaxonNode childNode : childNodes){
+ node.deleteChildNode(childNode);
+ }
+
+ return result;
+ }
+
+ /**
+ * Deletes the child node and also removes children of childnode
+ * recursively if delete children is <code>true</code>
+ * @param node
+ * @param deleteChildren
+ * @return
+ */
+ public boolean deleteChildNode(TaxonNode node, boolean deleteChildren) {
+ boolean result = removeChildNode(node);
+ Taxon taxon = node.getTaxon();
+ node.setTaxon(null);
+ taxon.removeTaxonNode(node);
+ if (deleteChildren){
+ ArrayList<TaxonNode> childNodes = new ArrayList<TaxonNode>(node.getChildNodes());
+ for(TaxonNode childNode : childNodes){
+ node.deleteChildNode(childNode, deleteChildren);
+ }
+ } else{
+ ArrayList<TaxonNode> childNodes = new ArrayList<TaxonNode>(node.getChildNodes());
+ for(TaxonNode childNode : childNodes){
+ this.addChildNode(childNode, null, null);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Removes the child node from this node. Sets the parent and the classification of the child
+ * node to null
+ *
+ * @param childNode
+ * @return
+ */
+ protected boolean removeChildNode(TaxonNode childNode){
+ boolean result = true;
+ removeNullValueFromChildren();
+ if(childNode == null){
+ throw new IllegalArgumentException("TaxonNode may not be null");
+ }
+ int index = childNodes.indexOf(childNode);
+ if (index >= 0){
+ removeChild(index);
+ } else {
+ result = false;
+ }
+ return result;
+ }
+
+ /**
+ * Removes the child node placed at the given (index + 1) position
+ * from the list of {@link #getChildNodes() children} of <i>this</i> taxon node.
+ * Sets the parent and the classification of the child
+ * node to null.
+ * If the given index is out of bounds no child will be removed.
+ *
+ * @param index the integer indicating the position of the taxon node to
+ * be removed
+ * @see #getChildNodes()
+ * @see #addChildNode(TaxonNode, Reference, String)
+ * @see #addChildNode(TaxonNode, int, Reference, String)
+ * @see #deleteChildNode(TaxonNode)
+ */
+ public void removeChild(int index){
+ //TODO: Only as a workaround. We have to find out why merge creates null entries.
+
+ TaxonNode child = childNodes.get(index);
+ child = HibernateProxyHelper.deproxy(child, TaxonNode.class); //strange that this is required, but otherwise child.getParent() returns null for some lazy-loaded items.
+
+ if (child != null){
+
+ TaxonNode parent = HibernateProxyHelper.deproxy(child.getParent(), TaxonNode.class);
+ TaxonNode thisNode = HibernateProxyHelper.deproxy(this, TaxonNode.class);
+ if(parent != null && parent != thisNode){
+ 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());
+ }else if (parent == null){
+ throw new IllegalStateException("Parent of child is null in TaxonNode.removeChild(int). This should not happen.");
+ }
+ childNodes.remove(index);
+ child.setClassification(null);
+
+ //update sortindex
+ //TODO workaround (see sortIndex doc)
+ this.countChildren = childNodes.size();
+ child.setParent(null);
+ child.setTreeIndex(null);
+ updateSortIndex(index);
+ child.setSortIndex(null);
+ }
+ }
+
+
+ /**
+ * Remove this taxonNode From its taxonomic parent
+ *
+ * @return true on success
+ */
+ public boolean delete(){
+ if(isTopmostNode()){
+ return classification.deleteChildNode(this);
+ }else{
+ return getParent().deleteChildNode(this);
+ }
+ }
+
+ /**
+ * Remove this taxonNode From its taxonomic parent
+ *
+ * @return true on success
+ */
+ public boolean delete(boolean deleteChildren){
+ if(isTopmostNode()){
+ return classification.deleteChildNode(this, deleteChildren);
+ }else{
+ return getParent().deleteChildNode(this, deleteChildren);
+ }
+ }
+
+ @Override
+ @Deprecated //for CDM lib internal use only, may be removed in future versions
+ public int treeId() {
+ if (this.classification == null){
+ logger.warn("TaxonNode has no classification. This should not happen."); //#3840
+ return -1;
+ }else{
+ return this.classification.getId();
+ }
+ }
+
+
+ /**
+ * Sets the parent of this taxon node to the given parent. Cleans up references to
+ * old parents and sets the classification to the new parents classification
+ *
+ * @param parent
+ */
+ @Transient
+ protected void setParentTreeNode(TaxonNode parent, int index){
+ // remove ourselves from the old parent
+ TaxonNode formerParent = this.getParent();
+ formerParent = HibernateProxyHelper.deproxy(formerParent, TaxonNode.class);
+ if (formerParent != null){
+ //special case, child already exists for same parent
+ //FIXME document / check for correctness
+ if (formerParent.equals(parent)){
+ int currentIndex = formerParent.getChildNodes().indexOf(this);
+ if (currentIndex != -1 && currentIndex < index){
+ index--;
+ }
+ }
+
+ //remove from old parent
+ formerParent.removeChildNode(this);
+ }
+
+ // set the new parent
+ setParent(parent);
+
+ // set the classification to the parents classification
+
+ Classification classification = parent.getClassification();
+ //FIXME also set the tree index here for performance reasons
+ classification = HibernateProxyHelper.deproxy(classification, Classification.class);
+ setClassificationRecursively(classification);
+
+ // add this node to the parent's child nodes
+ parent = HibernateProxyHelper.deproxy(parent, TaxonNode.class);
+ List<TaxonNode> parentChildren = parent.getChildNodes();
+ //TODO: Only as a workaround. We have to find out why merge creates null entries.
+
+ while (parentChildren.contains(null)){
+ parentChildren.remove(null);
+ }
+ parent.updateSortIndex(0);
+ if (index > parent.getChildNodes().size()){
+ index = parent.getChildNodes().size();
+ }
+ if (parentChildren.contains(this)){
+ //avoid duplicates
+ if (parentChildren.indexOf(this) < index){
+ index = index -1;
+ }
+ parentChildren.remove(this);
+ parentChildren.add(index, this);
+ }else{
+ parentChildren.add(index, this);
+ }
+
+
+ //sortIndex
+ //TODO workaround (see sortIndex doc)
+ this.getParent().updateSortIndex(index);
+ //only for debugging
+ if (! this.getSortIndex().equals(index)){
+ logger.warn("index and sortindex are not equal");
+ }
+
+ // update the children count
+ parent.setCountChildren(parent.getChildNodes().size());
+ }
+
+ /**
+ * As long as the sort index is not correctly handled through hibernate this is a workaround method
+ * to update the sort index manually
+ * @param parentChildren
+ * @param index
+ */
+ private void updateSortIndex(int index) {
+ List<TaxonNode> children = this.getChildNodes();
+
+
+ for(int i = index; i < children.size(); i++){
+ TaxonNode child = children.get(i);
+ if (child != null){
+// child = CdmBase.deproxy(child, TaxonNode.class); //deproxy not needed as long as setSortIndex is protected or public #4200
+ child.setSortIndex(i);
+ }else{
+ String message = "A node in a taxon tree must never be null but is (ParentId: %d; sort index: %d; index: %d; i: %d)";
+ throw new IllegalStateException(String.format(message, getId(), sortIndex, index, i));
+ }
+ }
+ }
+
+
+
+
+ /**
+ * Returns a set containing this node and all nodes that are descendants of this node
+ *
+ * @return
+ */
+
+ protected Set<TaxonNode> getDescendants(){
+ Set<TaxonNode> nodeSet = new HashSet<TaxonNode>();
+
+ nodeSet.add(this);
+
+ for(TaxonNode childNode : getChildNodes()){
+ nodeSet.addAll(childNode.getDescendants());
+ }
+
+ return nodeSet;
+ }
+
+ /**
+ * Returns a set containing a clone of this node and of all nodes that are descendants of this node
+ *
+ * @return
+ */
+ protected TaxonNode cloneDescendants(){
+
+ TaxonNode clone = (TaxonNode)this.clone();
+ TaxonNode childClone;
+
+ for(TaxonNode childNode : getChildNodes()){
+ childClone = (TaxonNode) childNode.clone();
+ for (TaxonNode childChild:childNode.getChildNodes()){
+ childClone.addChildNode(childChild.cloneDescendants(), childChild.getReference(), childChild.getMicroReference());
+ }
+ clone.addChildNode(childClone, childNode.getReference(), childNode.getMicroReference());
+ //childClone.addChildNode(childNode.cloneDescendants());
+ }
+ return clone;
+ }
+
+ /**
+ * Returns a
+ *
+ * @return
+ */
+ protected Set<TaxonNode> getAncestors(){
+ Set<TaxonNode> nodeSet = new HashSet<TaxonNode>();
+ // nodeSet.add(this);
+ if(this.getParent() != null){
+ TaxonNode parent = CdmBase.deproxy(this.getParent(), TaxonNode.class);
+ nodeSet.addAll(parent.getAncestors());
+ }
+ return nodeSet;
+ }
+ public TaxonNode getAncestorOfRank(Rank rank){
+ Set<TaxonNode> nodeSet = new HashSet<TaxonNode>();
+ // nodeSet.add(this);
+ TaxonBase taxon = CdmBase.deproxy(this.getTaxon(), Taxon.class);
+ TaxonNameBase name = CdmBase.deproxy(taxon.getName(), TaxonNameBase.class);
+ if (name.getRank().isHigher(rank)){
+ return null;
+ }
+ if (name.getRank().equals(rank)){
+ return this;
+ }
+ if(this.getParent() != null ){
+ TaxonNode parent = CdmBase.deproxy(this.getParent(), TaxonNode.class);
+ return parent.getAncestorOfRank(rank);
+ }
+ return null;
+
+ }
+
+ /**
+ * Whether this TaxonNode is a direct child of the classification TreeNode
+ * @return
+ */
+ @Transient
+ public boolean isTopmostNode(){
+ boolean parentCheck = false;
+ boolean classificationCheck = false;
+
+ if(getParent() != null) {
+ if(getParent().getTaxon() == null) {
+ parentCheck = true;
+ }
+ }
+
+ //TODO remove
+ // FIXME This should work but doesn't, due to missing sort indexes, can be removed after fixing #4200, #4098
+ if (classification != null){
+ classificationCheck = classification.getRootNode().getChildNodes().contains(this);
+ }else{
+ classificationCheck = false;
+ }
+
+ // The following is just for logging purposes for the missing sort indexes problem
+ // ticket #4098
+ if(parentCheck != classificationCheck) {
+ logger.warn("isTopmost node check " + parentCheck + " not same as classificationCheck : " + classificationCheck + " for taxon node ");
+ if(this.getParent() != null) {
+ logger.warn("-- with parent uuid " + this.getParent().getUuid().toString());
+ logger.warn("-- with parent id " + this.getParent().getId());
+ for(TaxonNode node : this.getParent().getChildNodes()) {
+ if(node == null) {
+ logger.warn("-- child node is null");
+ } else if (node.getTaxon() == null) {
+ logger.warn("-- child node taxon is null");
+ }
+ }
+ logger.warn("-- parent child count" + this.getParent().getChildNodes().size());
+ }
+ }
+
+ return parentCheck;
+ }
+
+ /**
+ * Whether this TaxonNode is a descendant of the given TaxonNode
+ *
+ * Caution: use this method with care on big branches. -> performance and memory hungry
+ *
+ * Protip: Try solving your problem with the isAscendant method which traverses the tree in the
+ * other direction (up). It will always result in a rather small set of consecutive parents beeing
+ * generated.
+ *
+ * TODO implement more efficiently without generating the set of descendants first
+ *
+ * @param possibleParent
+ * @return true if this is a descendant
+ */
+ @Transient
+ public boolean isDescendant(TaxonNode possibleParent){
+ if (this.treeIndex() == null || possibleParent.treeIndex() == null) {
+ return false;
+ }
+ return possibleParent == null ? false : this.treeIndex().startsWith(possibleParent.treeIndex() );
+ }
+
+ /**
+ * Whether this TaxonNode is an ascendant of the given TaxonNode
+ *
+ * @param possibleChild
+ * @return true if there are ascendants
+ */
+ @Transient
+ public boolean isAncestor(TaxonNode possibleChild){
+ if (this.treeIndex() == null || possibleChild.treeIndex() == null) {
+ return false;
+ }
+ // return possibleChild == null ? false : possibleChild.getAncestors().contains(this);
+ return possibleChild == null ? false : possibleChild.treeIndex().startsWith(this.treeIndex());
+ }
+
+ /**
+ * Whether this taxon has child nodes
+ *
+ * @return true if the taxonNode has childNodes
+ */
+ @Transient
+ @Override
+ public boolean hasChildNodes(){
+ return childNodes.size() > 0;
+ }
+
+
+ public boolean hasTaxon() {
+ return (taxon!= null);
+ }
+
+ /**
+ * @return
+ */
+ @Transient
+ public Rank getNullSafeRank() {
+ return hasTaxon() ? getTaxon().getNullSafeRank() : null;
+ }
+
+ private void removeNullValueFromChildren(){
+ try {
+ if (childNodes.contains(null)){
+ while(childNodes.contains(null)){
+ childNodes.remove(null);
+ }
+ }
+ this.updateSortIndex(0);
+ } catch (LazyInitializationException e) {
+ logger.info("Cannot clean up uninitialized children without a session, skipping.");
+ }
+ }
+
+
+
+//*********************** CLONE ********************************************************/
+ /**
+ * Clones <i>this</i> taxon node. This is a shortcut that enables to create
+ * a new instance that differs only slightly from <i>this</i> taxon node by
+ * modifying only some of the attributes.<BR><BR>
+ * The child nodes are not copied.<BR>
+ * The taxon and parent are the same as for the original taxon node. <BR>
+ *
+ * @see eu.etaxonomy.cdm.model.media.IdentifiableEntity#clone()
+ * @see java.lang.Object#clone()
+ */
+ @Override
+ public Object clone() {
+ try{
+ TaxonNode result = (TaxonNode)super.clone();
+ result.getTaxon().addTaxonNode(result);
+ result.childNodes = new ArrayList<TaxonNode>();
+ result.countChildren = 0;
+
+ //agents
+ result.agentRelations = new HashSet<TaxonNodeAgentRelation>();
+ for (TaxonNodeAgentRelation rel : this.agentRelations){
+ result.addAgentRelation((TaxonNodeAgentRelation)rel.clone());
+ }
+
+ return result;
+ }catch (CloneNotSupportedException e) {
+ logger.warn("Object does not implement cloneable");
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+}