#5679 catching and ignoring LIEs during the attempt to clean up NULL items in sets
[cdmlib.git] / cdmlib-model / src / main / java / eu / etaxonomy / cdm / model / taxon / TaxonNode.java
old mode 100644 (file)
new mode 100755 (executable)
index a953f4b..f7209ea
-// $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;
+        }
+    }
+
+}