Project

General

Profile

Download (35.7 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.TaxonName;
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
 * @since 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
//        this.treeIndex = parent.treeIndex() +
328
    }
329

    
330
    // *************** Excluded Note ***************
331

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

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

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

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

    
401
// ****************** Agent Relations ****************************/
402

    
403

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

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

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

    
431
//********************
432

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

    
441

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

    
453

    
454

    
455
//************************ METHODS **************************/
456

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

    
462

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

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

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

    
510
        child.setParentTreeNode(this, index);
511

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

    
515
        return child;
516
    }
517

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

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

    
547

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

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

    
581
        return result;
582
    }
583

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

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

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

    
626
        if (child != null){
627

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

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

    
648

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

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

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

    
686

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

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

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

    
715
        // set the classification to the parents classification
716

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

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

    
742

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

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

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

    
779

    
780
    /**
781
     * Returns a set containing this node and all nodes that are descendants of this node
782
     *
783
     * @return
784
     */
785
	@Transient
786
    protected Set<TaxonNode> getDescendants(){
787
        Set<TaxonNode> nodeSet = new HashSet<>();
788

    
789
        nodeSet.add(this);
790

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

    
795
        return nodeSet;
796
    }
797

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

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

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

    
819
    /**
820
     * Returns all ancestor nodes of this node
821
     *
822
     * @return a set of all parent nodes
823
     */
824
    @Transient
825
    protected Set<TaxonNode> getAncestors(){
826
        Set<TaxonNode> nodeSet = new HashSet<>();
827
        if(this.getParent() != null){
828
        	TaxonNode parent =  CdmBase.deproxy(this.getParent());
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. If any of the ancestors
837
     * has no taxon or has a rank > the given rank <code>null</code> is returned.
838
     * If <code>this</code> taxon is already of given rank this taxon is returned.
839
     * @param rank the rank the ancestor should have
840
     * @return the first found instance of a parent taxon node with the given rank
841
     */
842
    @Transient
843
    public TaxonNode getAncestorOfRank(Rank rank){
844
        Taxon taxon = CdmBase.deproxy(this.getTaxon());
845
        if (taxon == null){
846
            return null;
847
        }
848
        TaxonName name = CdmBase.deproxy(taxon.getName());
849
        if (name != null && name.getRank() != null){
850
            if (name.getRank().isHigher(rank)){
851
                return null;
852
            }
853
            if (name.getRank().equals(rank)){
854
                return this;
855
            }
856
        }
857

    
858
        if(this.getParent() != null){
859
        	TaxonNode parent =  CdmBase.deproxy(this.getParent());
860
            return parent.getAncestorOfRank(rank);
861
        }
862
		return null;
863
    }
864

    
865
    /**
866
     * Returns the ancestor taxa, starting with the highest (e.g. kingdom)
867
     * @return
868
     */
869
    @Transient
870
    public List<Taxon> getAncestorTaxaList(){
871
        List<Taxon> result = new ArrayList<>();
872
        TaxonNode current = this;
873
        while (current != null){
874
            if (current.getTaxon() != null){
875
                result.add(0, current.getTaxon());
876
            }
877
            current = current.getParent();
878
        }
879
        return result;
880
    }
881

    
882
    /**
883
     * Returns the ancestor taxon nodes, that do have a taxon attached
884
     * (excludes the root node) starting with the highest
885
     *
886
     * @return
887
     */
888
    @Transient
889
    public List<TaxonNode> getAncestorList(){
890
        List<TaxonNode> result = new ArrayList<>();
891
        TaxonNode current = this.getParent();
892
        while (current != null){
893
            if (current.getTaxon() != null){
894
                result.add(0, current);
895
            }
896
            current = current.getParent();
897
        }
898
        return result;
899
    }
900

    
901

    
902
    /**
903
     * Whether this TaxonNode is a direct child of the classification TreeNode
904
     * @return
905
     */
906
    @Transient
907
    public boolean isTopmostNode(){
908
    	boolean parentCheck = false;
909
    	boolean classificationCheck = false;
910

    
911
    	if(getParent() != null) {
912
    		if(getParent().getTaxon() == null) {
913
    			parentCheck = true;
914
    		}
915
    	}
916

    
917
    	//TODO remove
918
    	// FIXME This should work but doesn't, due to missing sort indexes, can be removed after fixing #4200, #4098
919
    	if (classification != null){
920
    		classificationCheck = classification.getRootNode().getChildNodes().contains(this);
921
    	}else{
922
    		classificationCheck = false;
923
    	}
924

    
925
    	// The following is just for logging purposes for the missing sort indexes problem
926
    	// ticket #4098
927
    	if(parentCheck != classificationCheck) {
928
    		logger.warn("isTopmost node check " + parentCheck + " not same as classificationCheck : " + classificationCheck + " for taxon node ");
929
    		if(this.getParent() != null) {
930
    			logger.warn("-- with parent uuid " + this.getParent().getUuid().toString());
931
    			logger.warn("-- with parent id " + this.getParent().getId());
932
    			for(TaxonNode node : this.getParent().getChildNodes()) {
933
    				if(node == null) {
934
    					logger.warn("-- child node is null");
935
    				} else if (node.getTaxon() == null) {
936
    					logger.warn("-- child node taxon is null");
937
    				}
938
    			}
939
    			logger.warn("-- parent child count" + this.getParent().getChildNodes().size());
940
    		}
941
    	}
942

    
943
    	return parentCheck;
944
    }
945

    
946
    /**
947
     * Whether this TaxonNode is a descendant of the given TaxonNode
948
     *
949
     * Caution: use this method with care on big branches. -> performance and memory hungry
950
     *
951
     * Protip: Try solving your problem with the isAscendant method which traverses the tree in the
952
     * other direction (up). It will always result in a rather small set of consecutive parents beeing
953
     * generated.
954
     *
955
     * TODO implement more efficiently without generating the set of descendants first
956
     *
957
     * @param possibleParent
958
     * @return true if this is a descendant
959
     */
960
    @Transient
961
    public boolean isDescendant(TaxonNode possibleParent){
962
    	if (this.treeIndex() == null || possibleParent.treeIndex() == null) {
963
    		return false;
964
    	}
965
    	return possibleParent == null ? false : this.treeIndex().startsWith(possibleParent.treeIndex() );
966
    }
967

    
968
    /**
969
     * Whether this TaxonNode is an ascendant of the given TaxonNode
970
     *
971
     * @param possibleChild
972
     * @return true if there are ascendants
973
     */
974
    @Transient
975
    public boolean isAncestor(TaxonNode possibleChild){
976
    	if (this.treeIndex() == null || possibleChild.treeIndex() == null) {
977
    		return false;
978
    	}
979
       // return possibleChild == null ? false : possibleChild.getAncestors().contains(this);
980
        return possibleChild == null ? false : possibleChild.treeIndex().startsWith(this.treeIndex());
981
    }
982

    
983
    /**
984
     * Whether this taxon has child nodes
985
     *
986
     * @return true if the taxonNode has childNodes
987
     */
988
    @Transient
989
    @Override
990
    public boolean hasChildNodes(){
991
        return childNodes.size() > 0;
992
    }
993

    
994

    
995
    public boolean hasTaxon() {
996
        return (taxon!= null);
997
    }
998

    
999
    /**
1000
     * @return
1001
     */
1002
    @Transient
1003
    public Rank getNullSafeRank() {
1004
        return hasTaxon() ? getTaxon().getNullSafeRank() : null;
1005
    }
1006

    
1007
    public void removeNullValueFromChildren(){
1008
        try {
1009
            //HHH_9751_Util.removeAllNull(childNodes);
1010
            this.updateSortIndex(0);
1011
        } catch (LazyInitializationException e) {
1012
            logger.info("Cannot clean up uninitialized children without a session, skipping.");
1013
        }
1014
    }
1015

    
1016

    
1017

    
1018
//*********************** CLONE ********************************************************/
1019
    /**
1020
     * Clones <i>this</i> taxon node. This is a shortcut that enables to create
1021
     * a new instance that differs only slightly from <i>this</i> taxon node by
1022
     * modifying only some of the attributes.<BR><BR>
1023
     * The child nodes are not copied.<BR>
1024
     * The taxon and parent are the same as for the original taxon node. <BR>
1025
     *
1026
     * @see eu.etaxonomy.cdm.model.media.IdentifiableEntity#clone()
1027
     * @see java.lang.Object#clone()
1028
     */
1029
    @Override
1030
    public Object clone()  {
1031
        try{
1032
            TaxonNode result = (TaxonNode)super.clone();
1033
            result.getTaxon().addTaxonNode(result);
1034

    
1035
            //childNodes
1036
            result.childNodes = new ArrayList<>();
1037
            result.countChildren = 0;
1038

    
1039
            //agents
1040
            result.agentRelations = new HashSet<>();
1041
            for (TaxonNodeAgentRelation rel : this.agentRelations){
1042
                result.addAgentRelation((TaxonNodeAgentRelation)rel.clone());
1043
            }
1044

    
1045
            //excludedNote
1046
            result.excludedNote = new HashMap<>();
1047
            for(Language lang : this.excludedNote.keySet()){
1048
                result.excludedNote.put(lang, this.excludedNote.get(lang));
1049
            }
1050

    
1051

    
1052
            return result;
1053
        }catch (CloneNotSupportedException e) {
1054
            logger.warn("Object does not implement cloneable");
1055
            e.printStackTrace();
1056
            return null;
1057
        }
1058
    }
1059

    
1060
}
(13-13/21)