Project

General

Profile

Download (34.6 KB) Statistics
| Branch: | Tag: | Revision:
1
/**
2
* Copyright (C) 2007 EDIT
3
* European Distributed Institute of Taxonomy
4
* http://www.e-taxonomy.eu
5
*
6
* The contents of this file are subject to the Mozilla Public License Version 1.1
7
* See LICENSE.TXT at the top of this package for the full license terms.
8
*/
9

    
10
package eu.etaxonomy.cdm.model.taxon;
11

    
12
import java.util.ArrayList;
13
import java.util.HashMap;
14
import java.util.HashSet;
15
import java.util.List;
16
import java.util.Map;
17
import java.util.Set;
18

    
19
import javax.persistence.Column;
20
import javax.persistence.Entity;
21
import javax.persistence.FetchType;
22
import javax.persistence.JoinTable;
23
import javax.persistence.ManyToOne;
24
import javax.persistence.MapKeyJoinColumn;
25
import javax.persistence.OneToMany;
26
import javax.persistence.OrderBy;
27
import javax.persistence.OrderColumn;
28
import javax.persistence.Transient;
29
import javax.xml.bind.annotation.XmlAccessType;
30
import javax.xml.bind.annotation.XmlAccessorType;
31
import javax.xml.bind.annotation.XmlAttribute;
32
import javax.xml.bind.annotation.XmlElement;
33
import javax.xml.bind.annotation.XmlElementWrapper;
34
import javax.xml.bind.annotation.XmlIDREF;
35
import javax.xml.bind.annotation.XmlRootElement;
36
import javax.xml.bind.annotation.XmlSchemaType;
37
import javax.xml.bind.annotation.XmlType;
38
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
39

    
40
import org.apache.log4j.Logger;
41
import org.hibernate.LazyInitializationException;
42
import org.hibernate.annotations.Cascade;
43
import org.hibernate.annotations.CascadeType;
44
import org.hibernate.annotations.Index;
45
import org.hibernate.annotations.Table;
46
import org.hibernate.envers.Audited;
47
import org.hibernate.search.annotations.ContainedIn;
48
import org.hibernate.search.annotations.IndexedEmbedded;
49

    
50
import eu.etaxonomy.cdm.hibernate.HHH_9751_Util;
51
import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
52
import eu.etaxonomy.cdm.jaxb.MultilanguageTextAdapter;
53
import eu.etaxonomy.cdm.model.agent.TeamOrPersonBase;
54
import eu.etaxonomy.cdm.model.common.AnnotatableEntity;
55
import eu.etaxonomy.cdm.model.common.CdmBase;
56
import eu.etaxonomy.cdm.model.common.DefinedTerm;
57
import eu.etaxonomy.cdm.model.common.ITreeNode;
58
import eu.etaxonomy.cdm.model.common.Language;
59
import eu.etaxonomy.cdm.model.common.LanguageString;
60
import eu.etaxonomy.cdm.model.common.MultilanguageText;
61
import eu.etaxonomy.cdm.model.name.Rank;
62
import eu.etaxonomy.cdm.model.name.TaxonNameBase;
63
import eu.etaxonomy.cdm.model.reference.Reference;
64
import eu.etaxonomy.cdm.validation.Level3;
65
import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustBeLowerRankThanParent;
66
import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustDeriveNameFromParent;
67
import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustNotSkipRanks;
68

    
69
/**
70
 * @author a.mueller
71
 * @created 31.03.2009
72
 */
73
@XmlAccessorType(XmlAccessType.FIELD)
74
@XmlType(name = "TaxonNode", propOrder = {
75
    "classification",
76
    "taxon",
77
    "parent",
78
    "treeIndex",
79
    "sortIndex",
80
    "childNodes",
81
    "referenceForParentChildRelation",
82
    "microReferenceForParentChildRelation",
83
    "countChildren",
84
    "agentRelations",
85
    "synonymToBeUsed",
86
    "excludedNote"
87
})
88
@XmlRootElement(name = "TaxonNode")
89
@Entity
90
//@Indexed disabled to reduce clutter in indexes, since this type is not used by any search
91
//@Indexed(index = "eu.etaxonomy.cdm.model.taxon.TaxonNode")
92
@Audited
93
@Table(appliesTo="TaxonNode", indexes = { @Index(name = "taxonNodeTreeIndex", columnNames = { "treeIndex" }) })
94
@ChildTaxaMustBeLowerRankThanParent(groups = Level3.class)
95
@ChildTaxaMustNotSkipRanks(groups = Level3.class)
96
@ChildTaxaMustDeriveNameFromParent(groups = Level3.class)
97
public class TaxonNode extends AnnotatableEntity implements ITaxonTreeNode, ITreeNode<TaxonNode>, Cloneable{
98
    private static final long serialVersionUID = -4743289894926587693L;
99
    private static final Logger logger = Logger.getLogger(TaxonNode.class);
100

    
101
    @XmlElement(name = "taxon")
102
    @XmlIDREF
103
    @XmlSchemaType(name = "IDREF")
104
    @ManyToOne(fetch = FetchType.LAZY)
105
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
106
    @ContainedIn
107
    private Taxon taxon;
108

    
109

    
110
    @XmlElement(name = "parent")
111
    @XmlIDREF
112
    @XmlSchemaType(name = "IDREF")
113
    @ManyToOne(fetch = FetchType.LAZY)
114
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
115
    private TaxonNode parent;
116

    
117

    
118
    @XmlElement(name = "treeIndex")
119
    @Column(length=255)
120
    private String treeIndex;
121

    
122

    
123
    @XmlElement(name = "classification")
124
    @XmlIDREF
125
    @XmlSchemaType(name = "IDREF")
126
    @ManyToOne(fetch = FetchType.LAZY)
127
    @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
128
//	TODO @NotNull // avoids creating a UNIQUE key for this field
129
    @IndexedEmbedded(includeEmbeddedObjectId=true)
130
    private Classification classification;
131

    
132
    @XmlElementWrapper(name = "childNodes")
133
    @XmlElement(name = "childNode")
134
    @XmlIDREF
135
    @XmlSchemaType(name = "IDREF")
136
    //see https://dev.e-taxonomy.eu/trac/ticket/3722
137
    @OrderColumn(name="sortIndex")
138
    @OrderBy("sortIndex")
139
    @OneToMany(mappedBy="parent", fetch=FetchType.LAZY)
140
    //@Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
141
    private List<TaxonNode> childNodes = new ArrayList<>();
142

    
143
    //see https://dev.e-taxonomy.eu/trac/ticket/3722
144
    //see https://dev.e-taxonomy.eu/trac/ticket/4200
145
    private Integer sortIndex = -1;
146

    
147
	@XmlElement(name = "reference")
148
    @XmlIDREF
149
    @XmlSchemaType(name = "IDREF")
150
    @ManyToOne(fetch = FetchType.LAZY)
151
    @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
152
    private Reference referenceForParentChildRelation;
153

    
154
    @XmlElement(name = "microReference")
155
    private String microReferenceForParentChildRelation;
156

    
157
    @XmlElement(name = "countChildren")
158
    private int countChildren;
159

    
160
    @XmlElementWrapper(name = "agentRelations")
161
    @XmlElement(name = "agentRelation")
162
    @XmlIDREF
163
    @XmlSchemaType(name = "IDREF")
164
    @OneToMany(mappedBy="taxonNode", fetch=FetchType.LAZY)
165
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
166
    private Set<TaxonNodeAgentRelation> agentRelations = new HashSet<>();
167

    
168
    @XmlAttribute(name= "unplaced")
169
    private boolean unplaced = false;
170
    public boolean isUnplaced() {return unplaced;}
171
    public void setUnplaced(boolean unplaced) {this.unplaced = unplaced;}
172

    
173
    @XmlAttribute(name= "excluded")
174
    private boolean excluded = false;
175
    public boolean isExcluded() {return excluded;}
176
    public void setExcluded(boolean excluded) {this.excluded = excluded;}
177

    
178
    @XmlElement(name = "excludedNote")
179
    @XmlJavaTypeAdapter(MultilanguageTextAdapter.class)
180
    @OneToMany(fetch = FetchType.LAZY, orphanRemoval=true)
181
    @MapKeyJoinColumn(name="excludedNote_mapkey_id")
182
    @JoinTable(name = "TaxonNode_ExcludedNote")  //to make possible to add also unplacedNote
183
    @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE, CascadeType.DELETE})
184
    private Map<Language,LanguageString> excludedNote = new HashMap<>();
185

    
186
//	private Taxon originalConcept;
187
//	//or
188
    @XmlElement(name = "synonymToBeUsed")
189
    @XmlIDREF
190
    @XmlSchemaType(name = "IDREF")
191
    @ManyToOne(fetch = FetchType.LAZY)
192
    @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
193
    private Synonym synonymToBeUsed;
194

    
195
// ******************** CONSTRUCTOR **********************************************/
196

    
197
    protected TaxonNode(){super();}
198

    
199
    /**
200
     * to create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}
201
     * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}
202
     * @param taxon
203
     * @param classification
204
     * @deprecated setting of classification is handled in the addTaxonNode() method,
205
     * use TaxonNode(taxon) instead
206
     */
207
    @Deprecated
208
    protected TaxonNode (Taxon taxon, Classification classification){
209
        this(taxon);
210
        setClassification(classification);
211
    }
212

    
213
    /**
214
     * to create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}
215
     * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}
216
     *
217
     * @param taxon
218
     */
219
    protected TaxonNode(Taxon taxon){
220
        setTaxon(taxon);
221
    }
222

    
223
// ************************* GETTER / SETTER *******************************/
224

    
225
    @Transient
226
    public Integer getSortIndex() {
227
        return sortIndex;
228
	}
229
    /**
230
     * SortIndex shall be handled only internally, therefore not public.
231
     * However, as javaassist only supports protected methods it needs to be protected, not private.
232
     * Alternatively we could use deproxy on every call of this method (see commented code)
233
     * @param i
234
     * @return
235
     * @deprecated for internal use only
236
     */
237
     @Deprecated
238
    protected void setSortIndex(Integer i) {
239
//    	 CdmBase.deproxy(this, TaxonNode.class).sortIndex = i;  //alternative solution for private, DON'T remove
240
    	 sortIndex = i;
241
	}
242

    
243

    
244
    public Taxon getTaxon() {
245
        return taxon;
246
    }
247
    protected void setTaxon(Taxon taxon) {
248
        this.taxon = taxon;
249
        if (taxon != null){
250
            taxon.addTaxonNode(this);
251
        }
252
    }
253

    
254

    
255
    @Override
256
    public List<TaxonNode> getChildNodes() {
257
        return childNodes;
258
    }
259
	protected void setChildNodes(List<TaxonNode> childNodes) {
260
		this.childNodes = childNodes;
261
	}
262

    
263

    
264
    public Classification getClassification() {
265
        return classification;
266
    }
267
    /**
268
     * THIS METHOD SHOULD NOT BE CALLED!
269
     * invisible part of the bidirectional relationship, for public use TaxonomicView.addRoot() or TaxonNode.addChild()
270
     * @param classification
271
     * @deprecated for internal use only
272
     */
273
    @Deprecated
274
    protected void setClassification(Classification classification) {
275
        this.classification = classification;
276
    }
277

    
278

    
279
    @Override
280
    public String getMicroReference() {
281
        return microReferenceForParentChildRelation;
282
    }
283
    public void setMicroReference(String microReference) {
284
        this.microReferenceForParentChildRelation = microReference;
285
    }
286

    
287

    
288
    @Override
289
    public Reference getReference() {
290
        return referenceForParentChildRelation;
291
    }
292
    public void setReference(Reference reference) {
293
        this.referenceForParentChildRelation = reference;
294
    }
295

    
296
    //countChildren
297
    public int getCountChildren() {
298
        return countChildren;
299
    }
300
    /**
301
     * @deprecated for internal use only
302
     * @param countChildren
303
     */
304
    @Deprecated
305
    protected void setCountChildren(int countChildren) {
306
        this.countChildren = countChildren;
307
    }
308

    
309

    
310
    //parent
311
    @Override
312
    public TaxonNode getParent(){
313
        return parent;
314
    }
315
    /**
316
     * Sets the parent of this taxon node.<BR>
317
     *
318
     * In most cases you would want to call setParentTreeNode(ITreeNode) which
319
     * handles updating of the bidirectional relationship
320
     *
321
     * @see setParentTreeNode(ITreeNode)
322
     * @param parent
323
     *
324
     */
325
    protected void setParent(TaxonNode parent) {
326
        this.parent = parent;
327
    }
328

    
329
    // *************** Excluded Note ***************
330

    
331
    /**
332
     * Returns the {@link MultilanguageText multi-language text} to add a note to the
333
     * excluded flag. The different {@link LanguageString language strings}
334
     * contained in the multi-language text should all have the same meaning.
335
     * @see #getExcludedNote()
336
     * @see #putExcludedNote(Language, String)
337
     */
338
    public Map<Language,LanguageString> getExcludedNote(){
339
        return this.excludedNote;
340
    }
341

    
342
    /**
343
     * Returns the excluded note string in the given {@link Language language}
344
     *
345
     * @param language  the language in which the description string looked for is formulated
346
     * @see             #getExcludedNote()
347
     * @see             #putExcludedNote(Language, String)
348
     */
349
    public String getExcludedNote(Language language){
350
        LanguageString languageString = excludedNote.get(language);
351
        if (languageString == null){
352
            return null;
353
        }else{
354
            return languageString.getText();
355
        }
356
    }
357

    
358
    /**
359
     * Adds a translated {@link LanguageString text in a particular language}
360
     * to the {@link MultilanguageText multilanguage text} used to add a note to
361
     * the {@link #isExcluded() excluded} flag.
362
     *
363
     * @param excludedNote   the language string adding a note to the excluded flag
364
     *                      in a particular language
365
     * @see                 #getExcludedNote()
366
     * @see                 #putExcludedNote(String, Language)
367
     */
368
    public void putExcludedNote(LanguageString excludedNote){
369
        this.excludedNote.put(excludedNote.getLanguage(), excludedNote);
370
    }
371
    /**
372
     * Creates a {@link LanguageString language string} based on the given text string
373
     * and the given {@link Language language} and adds it to the {@link MultilanguageText
374
     * multi-language text} used to annotate the excluded flag.
375
     *
376
     * @param text      the string annotating the excluded flag
377
     *                  in a particular language
378
     * @param language  the language in which the text string is formulated
379
     * @see             #getExcludedNote()
380
     * @see             #putExcludedNote(LanguageString)
381
     * @see             #removeExcludedNote(Language)
382
     */
383
    public void putExcludedNote(Language language, String text){
384
        this.excludedNote.put(language, LanguageString.NewInstance(text, language));
385
    }
386

    
387
    /**
388
     * Removes from the {@link MultilanguageText multilanguage text} used to annotate
389
     * the excluded flag the one {@link LanguageString language string}
390
     * with the given {@link Language language}.
391
     *
392
     * @param  lang the language in which the language string to be removed
393
     *       has been formulated
394
     * @see         #getExcludedNote()
395
     */
396
    public void removeExcludedNote(Language lang){
397
        this.excludedNote.remove(lang);
398
    }
399

    
400
// ****************** Agent Relations ****************************/
401

    
402

    
403
    /**
404
     * @return
405
     */
406
    public Set<TaxonNodeAgentRelation> getAgentRelations() {
407
        return this.agentRelations;
408
    }
409

    
410
    public TaxonNodeAgentRelation addAgentRelation(DefinedTerm type, TeamOrPersonBase<?> agent){
411
        TaxonNodeAgentRelation result = TaxonNodeAgentRelation.NewInstance(this, agent, type);
412
        return result;
413
    }
414
    /**
415
     * @param nodeAgentRelation
416
     */
417
    protected void addAgentRelation(TaxonNodeAgentRelation agentRelation) {
418
        agentRelation.setTaxonNode(this);
419
        this.agentRelations.add(agentRelation);
420
    }
421

    
422
    /**
423
     * @param nodeAgentRelation
424
     */
425
    public void removeNodeAgent(TaxonNodeAgentRelation agentRelation) {
426
        agentRelation.setTaxonNode(this);
427
        agentRelations.remove(agentRelation);
428
    }
429

    
430
//********************
431

    
432
    //synonymToBeused
433
    public Synonym getSynonymToBeUsed() {
434
        return synonymToBeUsed;
435
    }
436
    public void setSynonymToBeUsed(Synonym synonymToBeUsed) {
437
        this.synonymToBeUsed = synonymToBeUsed;
438
    }
439

    
440

    
441
    //treeindex
442
    @Override
443
    public String treeIndex() {
444
        return treeIndex;
445
    }
446
    @Override
447
    @Deprecated //for CDM lib internal use only, may be removed in future versions
448
    public void setTreeIndex(String treeIndex) {
449
        this.treeIndex = treeIndex;
450
    }
451

    
452

    
453

    
454
//************************ METHODS **************************/
455

    
456
   @Override
457
    public TaxonNode addChildTaxon(Taxon taxon, Reference citation, String microCitation) {
458
        return addChildTaxon(taxon, this.childNodes.size(), citation, microCitation);
459
    }
460

    
461

    
462
    @Override
463
    public TaxonNode addChildTaxon(Taxon taxon, int index, Reference citation, String microCitation) {
464
        Classification classification = HibernateProxyHelper.deproxy(this.getClassification(), Classification.class);
465
        taxon = HibernateProxyHelper.deproxy(taxon, Taxon.class);
466
        if (classification.isTaxonInTree(taxon)){
467
            throw new IllegalArgumentException(String.format("Taxon may not be in a classification twice: %s", taxon.getTitleCache()));
468
       }
469
       return addChildNode(new TaxonNode(taxon), index, citation, microCitation);
470
    }
471

    
472
    /**
473
     * Moves a taxon node to a new parent. Descendents of the node are moved as well
474
     *
475
     * @param childNode the taxon node to be moved to the new parent
476
     * @return the child node in the state of having a new parent
477
     */
478
    @Override
479
    public TaxonNode addChildNode(TaxonNode childNode, Reference reference, String microReference){
480
        addChildNode(childNode, childNodes.size(), reference, microReference);
481
        return childNode;
482
    }
483

    
484
    /**
485
     * Inserts the given taxon node in the list of children of <i>this</i> taxon node
486
     * at the given (index + 1) position. If the given index is out of bounds
487
     * an exception will arise.<BR>
488
     * Due to bidirectionality this method must also assign <i>this</i> taxon node
489
     * as the parent of the given child.
490
     *
491
     * @param	child	the taxon node to be added
492
     * @param	index	the integer indicating the position at which the child
493
     * 					should be added
494
     * @see				#getChildNodes()
495
     * @see				#addChildNode(TaxonNode, Reference, String, Synonym)
496
     * @see				#deleteChildNode(TaxonNode)
497
     * @see				#deleteChildNode(int)
498
     */
499
    @Override
500
    public TaxonNode addChildNode(TaxonNode child, int index, Reference reference, String microReference){
501
        if (index < 0 || index > childNodes.size() + 1){
502
            throw new IndexOutOfBoundsException("Wrong index: " + index);
503
        }
504
           // check if this node is a descendant of the childNode
505
        if(child.getParent() != this && child.isAncestor(this)){
506
            throw new IllegalAncestryException("New parent node is a descendant of the node to be moved.");
507
        }
508

    
509
        child.setParentTreeNode(this, index);
510

    
511
        child.setReference(reference);
512
        child.setMicroReference(microReference);
513

    
514
        return child;
515
    }
516

    
517
    /**
518
     * Sets this nodes classification. Updates classification of child nodes recursively.
519
     *
520
     * If the former and the actual tree are equal() this method does nothing.
521
     *
522
     * @throws IllegalArgumentException if newClassifciation is null
523
     *
524
     * @param newClassification
525
     */
526
    @Transient
527
    private void setClassificationRecursively(Classification newClassification) {
528
        if (newClassification == null){
529
        	throw new IllegalArgumentException("New Classification must not be 'null' when setting new classification.");
530
        }
531
    	if(! newClassification.equals(this.getClassification())){
532
            this.setClassification(newClassification);
533
            for(TaxonNode childNode : this.getChildNodes()){
534
                childNode.setClassificationRecursively(newClassification);
535
            }
536
        }
537
    }
538

    
539
    @Override
540
    public boolean deleteChildNode(TaxonNode node) {
541
        boolean result = removeChildNode(node);
542
        Taxon taxon = HibernateProxyHelper.deproxy(node.getTaxon(), Taxon.class);
543
        node = HibernateProxyHelper.deproxy(node, TaxonNode.class);
544
        node.setTaxon(null);
545

    
546

    
547
        ArrayList<TaxonNode> childNodes = new ArrayList<TaxonNode>(node.getChildNodes());
548
        for(TaxonNode childNode : childNodes){
549
            HibernateProxyHelper.deproxy(childNode, TaxonNode.class);
550
            node.deleteChildNode(childNode);
551
        }
552
        taxon.removeTaxonNode(node);
553
        return result;
554
    }
555

    
556
    /**
557
     * Deletes the child node and also removes children of childnode
558
     * recursively if delete children is <code>true</code>
559
     * @param node
560
     * @param deleteChildren
561
     * @return
562
     */
563
    public boolean deleteChildNode(TaxonNode node, boolean deleteChildren) {
564
        boolean result = removeChildNode(node);
565
        Taxon taxon = node.getTaxon();
566
        node.setTaxon(null);
567
        taxon.removeTaxonNode(node);
568
        if (deleteChildren){
569
            ArrayList<TaxonNode> childNodes = new ArrayList<TaxonNode>(node.getChildNodes());
570
            for(TaxonNode childNode : childNodes){
571
                node.deleteChildNode(childNode, deleteChildren);
572
            }
573
        } else{
574
        	ArrayList<TaxonNode> childNodes = new ArrayList<TaxonNode>(node.getChildNodes());
575
            for(TaxonNode childNode : childNodes){
576
             this.addChildNode(childNode, null, null);
577
            }
578
        }
579

    
580
        return result;
581
    }
582

    
583
    /**
584
     * Removes the child node from this node. Sets the parent and the classification of the child
585
     * node to null
586
     *
587
     * @param childNode
588
     * @return
589
     */
590
    protected boolean removeChildNode(TaxonNode childNode){
591
        boolean result = true;
592
        //removeNullValueFromChildren();
593
        if(childNode == null){
594
            throw new IllegalArgumentException("TaxonNode may not be null");
595
        }
596
        int index = childNodes.indexOf(childNode);
597
        if (index >= 0){
598
            removeChild(index);
599
        } else {
600
            result = false;
601
        }
602
        return result;
603
    }
604

    
605
    /**
606
     * Removes the child node placed at the given (index + 1) position
607
     * from the list of {@link #getChildNodes() children} of <i>this</i> taxon node.
608
     * Sets the parent and the classification of the child
609
     * node to null.
610
     * If the given index is out of bounds no child will be removed.
611
     *
612
     * @param  index	the integer indicating the position of the taxon node to
613
     * 					be removed
614
     * @see     		#getChildNodes()
615
     * @see				#addChildNode(TaxonNode, Reference, String)
616
     * @see				#addChildNode(TaxonNode, int, Reference, String)
617
     * @see				#deleteChildNode(TaxonNode)
618
     */
619
    public void removeChild(int index){
620
        //TODO: Only as a workaround. We have to find out why merge creates null entries.
621

    
622
        TaxonNode child = childNodes.get(index);
623
        child = HibernateProxyHelper.deproxy(child, TaxonNode.class); //strange that this is required, but otherwise child.getParent() returns null for some lazy-loaded items.
624

    
625
        if (child != null){
626

    
627
            TaxonNode parent = HibernateProxyHelper.deproxy(child.getParent(), TaxonNode.class);
628
            TaxonNode thisNode = HibernateProxyHelper.deproxy(this, TaxonNode.class);
629
            if(parent != null && parent != thisNode){
630
                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());
631
            }else if (parent == null){
632
                throw new IllegalStateException("Parent of child is null in TaxonNode.removeChild(int). This should not happen.");
633
            }
634
            childNodes.remove(index);
635
            child.setClassification(null);
636

    
637
            //update sortindex
638
            //TODO workaround (see sortIndex doc)
639
            this.countChildren = childNodes.size();
640
            child.setParent(null);
641
            child.setTreeIndex(null);
642
            updateSortIndex(index);
643
            child.setSortIndex(null);
644
        }
645
    }
646

    
647

    
648
    /**
649
     * Remove this taxonNode From its taxonomic parent
650
     *
651
     * @return true on success
652
     */
653
    public boolean delete(){
654
        if(isTopmostNode()){
655
            return classification.deleteChildNode(this);
656
        }else{
657
            return getParent().deleteChildNode(this);
658
        }
659
    }
660

    
661
    /**
662
     * Remove this taxonNode From its taxonomic parent
663
     *
664
     * @return true on success
665
     */
666
    public boolean delete(boolean deleteChildren){
667
        if(isTopmostNode()){
668
            return classification.deleteChildNode(this, deleteChildren);
669
        }else{
670
            return getParent().deleteChildNode(this, deleteChildren);
671
        }
672
    }
673

    
674
    @Override
675
    @Deprecated //for CDM lib internal use only, may be removed in future versions
676
    public int treeId() {
677
        if (this.classification == null){
678
        	logger.warn("TaxonNode has no classification. This should not happen.");  //#3840
679
        	return -1;
680
        }else{
681
        	return this.classification.getId();
682
        }
683
    }
684

    
685

    
686
    /**
687
     * Sets the parent of this taxon node to the given parent. Cleans up references to
688
     * old parents and sets the classification to the new parents classification
689
     *
690
     * @param parent
691
     */
692
    @Transient
693
    protected void setParentTreeNode(TaxonNode parent, int index){
694
        // remove ourselves from the old parent
695
        TaxonNode formerParent = this.getParent();
696
        formerParent = HibernateProxyHelper.deproxy(formerParent, TaxonNode.class);
697
        if (formerParent != null){
698
        	//special case, child already exists for same parent
699
            //FIXME document / check for correctness
700
            if (formerParent.equals(parent)){
701
                int currentIndex = formerParent.getChildNodes().indexOf(this);
702
                if (currentIndex != -1 && currentIndex < index){
703
                    index--;
704
                }
705
        	}
706

    
707
        	//remove from old parent
708
            formerParent.removeChildNode(this);
709
        }
710

    
711
        // set the new parent
712
        setParent(parent);
713

    
714
        // set the classification to the parents classification
715

    
716
        Classification classification = parent.getClassification();
717
        //FIXME also set the tree index here for performance reasons
718
        classification = HibernateProxyHelper.deproxy(classification, Classification.class);
719
        setClassificationRecursively(classification);
720
        // add this node to the parent's child nodes
721
        parent = HibernateProxyHelper.deproxy(parent, TaxonNode.class);
722
        List<TaxonNode> parentChildren = parent.getChildNodes();
723
       //TODO: Only as a workaround. We have to find out why merge creates null entries.
724

    
725
//        HHH_9751_Util.removeAllNull(parentChildren);
726
//        parent.updateSortIndex(0);
727
        if (index > parent.getChildNodes().size()){
728
            index = parent.getChildNodes().size();
729
        }
730
        if (parentChildren.contains(this)){
731
            //avoid duplicates
732
            if (parentChildren.indexOf(this) < index){
733
                index = index -1;
734
            }
735
            parentChildren.remove(this);
736
            parentChildren.add(index, this);
737
        }else{
738
            parentChildren.add(index, this);
739
        }
740

    
741

    
742
        //sortIndex
743
        //TODO workaround (see sortIndex doc)
744
       // this.getParent().removeNullValueFromChildren();
745
        this.getParent().updateSortIndex(index);
746
        //only for debugging
747
        if (! this.getSortIndex().equals(index)){
748
        	logger.warn("index and sortindex are not equal: "+  this.getSortIndex() +";" + index);
749
        }
750

    
751
        // update the children count
752
        parent.setCountChildren(parent.getChildNodes().size());
753
    }
754

    
755
	/**
756
	 * As long as the sort index is not correctly handled through hibernate this is a workaround method
757
	 * to update the sort index manually
758
	 * @param parentChildren
759
	 * @param index
760
	 */
761
	private void updateSortIndex(int index) {
762
	    if (this.hasChildNodes()){
763
    	    List<TaxonNode> children = this.getChildNodes();
764
    	    HHH_9751_Util.removeAllNull(children);
765
    	    for(int i = index; i < children.size(); i++){
766
                	TaxonNode child = children.get(i);
767
                	if (child != null){
768
        //        		child = CdmBase.deproxy(child, TaxonNode.class);  //deproxy not needed as long as setSortIndex is protected or public #4200
769
                		child.setSortIndex(i);
770
                	}else{
771
                		String message = "A node in a taxon tree must never be null but is (ParentId: %d; sort index: %d; index: %d; i: %d)";
772
                		throw new IllegalStateException(String.format(message, getId(), sortIndex, index, i));
773
                	}
774
            }
775
	    }
776
	}
777

    
778

    
779

    
780

    
781
    /**
782
     * Returns a set containing this node and all nodes that are descendants of this node
783
     *
784
     * @return
785
     */
786

    
787
    protected Set<TaxonNode> getDescendants(){
788
        Set<TaxonNode> nodeSet = new HashSet<TaxonNode>();
789

    
790
        nodeSet.add(this);
791

    
792
        for(TaxonNode childNode : getChildNodes()){
793
            nodeSet.addAll(childNode.getDescendants());
794
        }
795

    
796
        return nodeSet;
797
    }
798

    
799
    /**
800
     * Returns a set containing a clone of this node and of all nodes that are descendants of this node
801
     *
802
     * @return
803
     */
804
    protected TaxonNode cloneDescendants(){
805

    
806
        TaxonNode clone = (TaxonNode)this.clone();
807
        TaxonNode childClone;
808

    
809
        for(TaxonNode childNode : getChildNodes()){
810
            childClone = (TaxonNode) childNode.clone();
811
            for (TaxonNode childChild:childNode.getChildNodes()){
812
                childClone.addChildNode(childChild.cloneDescendants(), childChild.getReference(), childChild.getMicroReference());
813
            }
814
            clone.addChildNode(childClone, childNode.getReference(), childNode.getMicroReference());
815
            //childClone.addChildNode(childNode.cloneDescendants());
816
        }
817
        return clone;
818
    }
819

    
820
    /**
821
     * Returns all ancestor nodes of this node
822
     *
823
     * @return a set of all parent nodes
824
     */
825
    protected Set<TaxonNode> getAncestors(){
826
        Set<TaxonNode> nodeSet = new HashSet<TaxonNode>();
827
        if(this.getParent() != null){
828
        	TaxonNode parent =  CdmBase.deproxy(this.getParent(), TaxonNode.class);
829
        	nodeSet.add(parent);
830
            nodeSet.addAll(parent.getAncestors());
831
        }
832
        return nodeSet;
833
    }
834

    
835
    /**
836
     * Retrieves the first ancestor of the given rank
837
     * @param rank the rank the ancestor should have
838
     * @return the first found instance of a parent taxon node with the given rank
839
     */
840
    public TaxonNode getAncestorOfRank(Rank rank){
841
        TaxonBase taxon = HibernateProxyHelper.deproxy(this.getTaxon(), Taxon.class);
842
        if (taxon == null){
843
            return null;
844
        }
845
        TaxonNameBase name = HibernateProxyHelper.deproxy(taxon.getName(), TaxonNameBase.class);
846
        if (name.getRank().isHigher(rank)){
847
        	return null;
848
        }
849
        if (name.getRank().equals(rank)){
850
        	return this;
851
        }
852

    
853
        if(this.getParent() != null){
854
        	TaxonNode parent =  CdmBase.deproxy(this.getParent(), TaxonNode.class);
855
            return parent.getAncestorOfRank(rank);
856
        }
857
		return null;
858

    
859
    }
860

    
861
    /**
862
     * Whether this TaxonNode is a direct child of the classification TreeNode
863
     * @return
864
     */
865
    @Transient
866
    public boolean isTopmostNode(){
867
    	boolean parentCheck = false;
868
    	boolean classificationCheck = false;
869

    
870
    	if(getParent() != null) {
871
    		if(getParent().getTaxon() == null) {
872
    			parentCheck = true;
873
    		}
874
    	}
875

    
876
    	//TODO remove
877
    	// FIXME This should work but doesn't, due to missing sort indexes, can be removed after fixing #4200, #4098
878
    	if (classification != null){
879
    		classificationCheck = classification.getRootNode().getChildNodes().contains(this);
880
    	}else{
881
    		classificationCheck = false;
882
    	}
883

    
884
    	// The following is just for logging purposes for the missing sort indexes problem
885
    	// ticket #4098
886
    	if(parentCheck != classificationCheck) {
887
    		logger.warn("isTopmost node check " + parentCheck + " not same as classificationCheck : " + classificationCheck + " for taxon node ");
888
    		if(this.getParent() != null) {
889
    			logger.warn("-- with parent uuid " + this.getParent().getUuid().toString());
890
    			logger.warn("-- with parent id " + this.getParent().getId());
891
    			for(TaxonNode node : this.getParent().getChildNodes()) {
892
    				if(node == null) {
893
    					logger.warn("-- child node is null");
894
    				} else if (node.getTaxon() == null) {
895
    					logger.warn("-- child node taxon is null");
896
    				}
897
    			}
898
    			logger.warn("-- parent child count" + this.getParent().getChildNodes().size());
899
    		}
900
    	}
901

    
902
    	return parentCheck;
903
    }
904

    
905
    /**
906
     * Whether this TaxonNode is a descendant of the given TaxonNode
907
     *
908
     * Caution: use this method with care on big branches. -> performance and memory hungry
909
     *
910
     * Protip: Try solving your problem with the isAscendant method which traverses the tree in the
911
     * other direction (up). It will always result in a rather small set of consecutive parents beeing
912
     * generated.
913
     *
914
     * TODO implement more efficiently without generating the set of descendants first
915
     *
916
     * @param possibleParent
917
     * @return true if this is a descendant
918
     */
919
    @Transient
920
    public boolean isDescendant(TaxonNode possibleParent){
921
    	if (this.treeIndex() == null || possibleParent.treeIndex() == null) {
922
    		return false;
923
    	}
924
    	return possibleParent == null ? false : this.treeIndex().startsWith(possibleParent.treeIndex() );
925
    }
926

    
927
    /**
928
     * Whether this TaxonNode is an ascendant of the given TaxonNode
929
     *
930
     * @param possibleChild
931
     * @return true if there are ascendants
932
     */
933
    @Transient
934
    public boolean isAncestor(TaxonNode possibleChild){
935
    	if (this.treeIndex() == null || possibleChild.treeIndex() == null) {
936
    		return false;
937
    	}
938
       // return possibleChild == null ? false : possibleChild.getAncestors().contains(this);
939
        return possibleChild == null ? false : possibleChild.treeIndex().startsWith(this.treeIndex());
940
    }
941

    
942
    /**
943
     * Whether this taxon has child nodes
944
     *
945
     * @return true if the taxonNode has childNodes
946
     */
947
    @Transient
948
    @Override
949
    public boolean hasChildNodes(){
950
        return childNodes.size() > 0;
951
    }
952

    
953

    
954
    public boolean hasTaxon() {
955
        return (taxon!= null);
956
    }
957

    
958
    /**
959
     * @return
960
     */
961
    @Transient
962
    public Rank getNullSafeRank() {
963
        return hasTaxon() ? getTaxon().getNullSafeRank() : null;
964
    }
965

    
966
    public void removeNullValueFromChildren(){
967
        try {
968
            //HHH_9751_Util.removeAllNull(childNodes);
969
            this.updateSortIndex(0);
970
        } catch (LazyInitializationException e) {
971
            logger.info("Cannot clean up uninitialized children without a session, skipping.");
972
        }
973
    }
974

    
975

    
976

    
977
//*********************** CLONE ********************************************************/
978
    /**
979
     * Clones <i>this</i> taxon node. This is a shortcut that enables to create
980
     * a new instance that differs only slightly from <i>this</i> taxon node by
981
     * modifying only some of the attributes.<BR><BR>
982
     * The child nodes are not copied.<BR>
983
     * The taxon and parent are the same as for the original taxon node. <BR>
984
     *
985
     * @see eu.etaxonomy.cdm.model.media.IdentifiableEntity#clone()
986
     * @see java.lang.Object#clone()
987
     */
988
    @Override
989
    public Object clone()  {
990
        try{
991
            TaxonNode result = (TaxonNode)super.clone();
992
            result.getTaxon().addTaxonNode(result);
993

    
994
            //childNodes
995
            result.childNodes = new ArrayList<>();
996
            result.countChildren = 0;
997

    
998
            //agents
999
            result.agentRelations = new HashSet<>();
1000
            for (TaxonNodeAgentRelation rel : this.agentRelations){
1001
                result.addAgentRelation((TaxonNodeAgentRelation)rel.clone());
1002
            }
1003

    
1004
            //excludedNote
1005
            result.excludedNote = new HashMap<>();
1006
            for(Language lang : this.excludedNote.keySet()){
1007
                result.excludedNote.put(lang, this.excludedNote.get(lang));
1008
            }
1009

    
1010

    
1011
            return result;
1012
        }catch (CloneNotSupportedException e) {
1013
            logger.warn("Object does not implement cloneable");
1014
            e.printStackTrace();
1015
            return null;
1016
        }
1017
    }
1018

    
1019
}
(13-13/20)