Project

General

Profile

Download (35.2 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.OneToMany;
25
import javax.persistence.OrderColumn;
26
import javax.persistence.Table;
27
import javax.persistence.Transient;
28
import javax.xml.bind.annotation.XmlAccessType;
29
import javax.xml.bind.annotation.XmlAccessorType;
30
import javax.xml.bind.annotation.XmlAttribute;
31
import javax.xml.bind.annotation.XmlElement;
32
import javax.xml.bind.annotation.XmlElementWrapper;
33
import javax.xml.bind.annotation.XmlIDREF;
34
import javax.xml.bind.annotation.XmlRootElement;
35
import javax.xml.bind.annotation.XmlSchemaType;
36
import javax.xml.bind.annotation.XmlType;
37
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
38

    
39
import org.apache.logging.log4j.LogManager;
40
import org.apache.logging.log4j.Logger;
41
import org.hibernate.annotations.Cascade;
42
import org.hibernate.annotations.CascadeType;
43
import org.hibernate.annotations.Parameter;
44
import org.hibernate.annotations.Type;
45
import org.hibernate.envers.Audited;
46
import org.hibernate.search.annotations.Analyze;
47
import org.hibernate.search.annotations.ContainedIn;
48
import org.hibernate.search.annotations.Field;
49
import org.hibernate.search.annotations.Index;
50
import org.hibernate.search.annotations.IndexedEmbedded;
51
import org.hibernate.search.annotations.Store;
52

    
53
import eu.etaxonomy.cdm.common.CdmUtils;
54
import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
55
import eu.etaxonomy.cdm.jaxb.MultilanguageTextAdapter;
56
import eu.etaxonomy.cdm.model.agent.TeamOrPersonBase;
57
import eu.etaxonomy.cdm.model.common.CdmBase;
58
import eu.etaxonomy.cdm.model.common.ITreeNode;
59
import eu.etaxonomy.cdm.model.common.Language;
60
import eu.etaxonomy.cdm.model.common.LanguageString;
61
import eu.etaxonomy.cdm.model.common.MultilanguageText;
62
import eu.etaxonomy.cdm.model.common.SingleSourcedEntityBase;
63
import eu.etaxonomy.cdm.model.name.Rank;
64
import eu.etaxonomy.cdm.model.name.TaxonName;
65
import eu.etaxonomy.cdm.model.reference.NamedSource;
66
import eu.etaxonomy.cdm.model.reference.Reference;
67
import eu.etaxonomy.cdm.model.term.DefinedTerm;
68
import eu.etaxonomy.cdm.validation.Level3;
69
import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustBeLowerRankThanParent;
70
import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustDeriveNameFromParent;
71
import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustNotSkipRanks;
72

    
73
/**
74
 * @author a.mueller
75
 * @since 31.03.2009
76
 */
77
@XmlAccessorType(XmlAccessType.FIELD)
78
@XmlType(name = "TaxonNode", propOrder = {
79
    "classification",
80
    "taxon",
81
    "parent",
82
    "treeIndex",
83
    "childNodes",
84
    "sortIndex",
85
    "countChildren",
86
    "agentRelations",
87
    "synonymToBeUsed",
88
    "status",
89
    "statusNote"
90
})
91
@XmlRootElement(name = "TaxonNode")
92
@Entity
93
//@Indexed disabled to reduce clutter in indexes, since this type is not used by any search
94
//@Indexed(index = "eu.etaxonomy.cdm.model.taxon.TaxonNode")
95
@Audited
96
@Table(name="TaxonNode", indexes = { @javax.persistence.Index(name = "taxonNodeTreeIndex", columnList = "treeIndex") })
97
@ChildTaxaMustBeLowerRankThanParent(groups = Level3.class)
98
@ChildTaxaMustNotSkipRanks(groups = Level3.class)
99
@ChildTaxaMustDeriveNameFromParent(groups = Level3.class)
100
public class TaxonNode
101
            extends SingleSourcedEntityBase
102
            implements ITaxonTreeNode, ITreeNode<TaxonNode>{
103

    
104
    private static final long serialVersionUID = -4743289894926587693L;
105
    private static final Logger logger = LogManager.getLogger(TaxonNode.class);
106

    
107
    @XmlElement(name = "taxon")
108
    @XmlIDREF
109
    @XmlSchemaType(name = "IDREF")
110
    @ManyToOne(fetch = FetchType.LAZY)
111
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
112
    @ContainedIn
113
    private Taxon taxon;
114

    
115
    @XmlElement(name = "parent")
116
    @XmlIDREF
117
    @XmlSchemaType(name = "IDREF")
118
    @ManyToOne(fetch = FetchType.LAZY)
119
//    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
120
    private TaxonNode parent;
121

    
122
    @XmlElement(name = "treeIndex")
123
    @Column(length=255)
124
    @Field(store = Store.YES, index = Index.YES, analyze = Analyze.NO)
125
    private String treeIndex;
126

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

    
136
    @XmlElementWrapper(name = "childNodes")
137
    @XmlElement(name = "childNode")
138
    @XmlIDREF
139
    @XmlSchemaType(name = "IDREF")
140
    //see https://dev.e-taxonomy.eu/redmine/issues/3722
141
    //see https://dev.e-taxonomy.eu/redmine/issues/4200
142
    //see https://dev.e-taxonomy.eu/redmine/issues/8127
143
    //see https://dev.e-taxonomy.eu/redmine/issues/10067
144
    @OrderColumn(name="sortIndex", nullable=true)
145
    @OneToMany(mappedBy="parent", fetch=FetchType.LAZY)
146
    //do not cascade
147
    private List<TaxonNode> childNodes = new ArrayList<>();
148

    
149
    //TODO remove
150
    //see https://dev.e-taxonomy.eu/redmine/issues/3722
151
    //see https://dev.e-taxonomy.eu/redmine/issues/4200
152
    @Transient
153
    private Integer sortIndex = -1;
154

    
155
    @XmlElement(name = "countChildren")
156
    private int countChildren;
157

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

    
166
    /**
167
     * The {@link TaxonNodeStatus status} of this taxon node.
168
     */
169
    @XmlAttribute(name ="TaxonNodeStatus")
170
    @Column(name="status", length=10)
171
    @Type(type = "eu.etaxonomy.cdm.hibernate.EnumUserType",
172
        parameters = {@Parameter(name  = "enumClass", value = "eu.etaxonomy.cdm.model.taxon.TaxonNodeStatus")}
173
    )
174
    @Audited
175
    private TaxonNodeStatus status;
176

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

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

    
194
// ******************** CONSTRUCTOR **********************************************/
195

    
196
    //for hibernate use only, *packet* private required by bytebuddy
197
    @Deprecated
198
    TaxonNode(){}
199

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

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

    
224
// ************************* GETTER / SETTER *******************************/
225

    
226
    public Taxon getTaxon() {
227
        return taxon;
228
    }
229
    public void setTaxon(Taxon taxon) {
230
        this.taxon = taxon;
231
        if (taxon != null){
232
            taxon.addTaxonNode(this);
233
        }
234
    }
235

    
236
    @Override
237
    public List<TaxonNode> getChildNodes() {
238
        return childNodes;
239
    }
240
	protected void setChildNodes(List<TaxonNode> childNodes) {
241
		this.childNodes = childNodes;
242
	}
243

    
244
    public Classification getClassification() {
245
        return classification;
246
    }
247
    /**
248
     * THIS METHOD SHOULD NOT BE CALLED!
249
     * invisible part of the bidirectional relationship, for public use Classification.addRoot() or TaxonNode.addChild()
250
     * @param classification
251
     * @deprecated for internal use only
252
     */
253
    @Deprecated
254
    protected void setClassification(Classification classification) {
255
        this.classification = classification;
256
    }
257

    
258
    public boolean isUnplaced() {return hasStatus(TaxonNodeStatus.UNPLACED);}
259

    
260
    //#8281 indicates a preliminary placement
261
    public boolean isDoubtful() {return hasStatus(TaxonNodeStatus.DOUBTFUL);}
262

    
263
    public boolean isExcluded() {return hasStatus(TaxonNodeStatus.EXCLUDED);}
264

    
265
//************************************************************/
266

    
267
    /**
268
     * The computed order index of this node being a child in the parents
269
     * childnode list.
270
     */
271
    @Transient
272
    public Integer getSortIndex() {
273
        return getParent() == null ? null : getParent().getChildNodes().indexOf(CdmBase.deproxy(this));
274
    }
275

    
276
    //countChildren
277
    public int getCountChildren() {
278
        return countChildren;
279
    }
280
    /**
281
     * @deprecated for internal use only
282
     * @param countChildren
283
     */
284
    @Deprecated
285
    protected void setCountChildren(int countChildren) {
286
        this.countChildren = countChildren;
287
    }
288

    
289
    //parent
290
    @Override
291
    public TaxonNode getParent(){
292
        return parent;
293
    }
294
    /**
295
     * Sets the parent of this taxon node.<BR>
296
     *
297
     * In most cases you would want to call setParentTreeNode(ITreeNode) which
298
     * handles updating of the bidirectional relationship
299
     *
300
     * @see setParentTreeNode(ITreeNode)
301
     * @param parent
302
     *
303
     */
304
    protected void setParent(TaxonNode parent) {
305
        this.parent = parent;
306
//        this.treeIndex = parent.treeIndex() +
307
    }
308

    
309
    public TaxonNodeStatus getStatus() {
310
        return status;
311
    }
312
    public void setStatus(TaxonNodeStatus status) {
313
        this.status = status;
314
    }
315

    
316
// *************** Status Note ***************
317

    
318
    /**
319
     * Returns the {@link MultilanguageText multi-language text} to add a note to the
320
     * status. The different {@link LanguageString language strings}
321
     * contained in the multi-language text should all have the same meaning.
322
     * @see #getStatusNote(Language)
323
     * @see #putStatusNote(Language, String)
324
     */
325
    public Map<Language,LanguageString> getStatusNote(){
326
        return this.statusNote;
327
    }
328

    
329
    /**
330
     * Returns the status note string in the given {@link Language language}
331
     *
332
     * @param language  the language in which the description string looked for is formulated
333
     * @see             #getStatusNote()
334
     * @see             #putStatusNote(Language, String)
335
     */
336
    public String getStatusNote(Language language){
337
        LanguageString languageString = statusNote.get(language);
338
        if (languageString == null){
339
            return null;
340
        }else{
341
            return languageString.getText();
342
        }
343
    }
344

    
345
    /**
346
     * Adds a translated {@link LanguageString text in a particular language}
347
     * to the {@link MultilanguageText multilanguage text} used to add a note to
348
     * the {@link #getStatus() status}.
349
     *
350
     * @param statusNote   the language string adding a note to the status
351
     *                      in a particular language
352
     * @see                 #getStatusNote()
353
     * @see                 #putStatusNote(String, Language)
354
     */
355
    public void putStatusNote(LanguageString statusNote){
356
        this.statusNote.put(statusNote.getLanguage(), statusNote);
357
    }
358
    /**
359
     * Creates a {@link LanguageString language string} based on the given text string
360
     * and the given {@link Language language} and adds it to the {@link MultilanguageText
361
     * multi-language text} used to annotate the status.
362
     *
363
     * @param text      the string annotating the status
364
     *                  in a particular language
365
     * @param language  the language in which the text string is formulated
366
     * @see             #getStatusNote()
367
     * @see             #putStatusNote(LanguageString)
368
     * @see             #removeStatusNote(Language)
369
     */
370
    public void putStatusNote(Language language, String text){
371
        this.statusNote.put(language, LanguageString.NewInstance(text, language));
372
    }
373

    
374
    /**
375
     * Removes from the {@link MultilanguageText multilanguage text} used to annotate
376
     * the status the one {@link LanguageString language string}
377
     * with the given {@link Language language}.
378
     *
379
     * @param  lang the language in which the language string to be removed
380
     *       has been formulated
381
     * @see         #getStatusNote()
382
     */
383
    public void removeStatusNote(Language lang){
384
        this.statusNote.remove(lang);
385
    }
386

    
387
// ****************** Agent Relations ****************************/
388

    
389
    public Set<TaxonNodeAgentRelation> getAgentRelations() {
390
        return this.agentRelations;
391
    }
392
    public TaxonNodeAgentRelation addAgentRelation(DefinedTerm type, TeamOrPersonBase<?> agent){
393
        TaxonNodeAgentRelation result = TaxonNodeAgentRelation.NewInstance(this, agent, type);
394
        return result;
395
    }
396
    public void addAgentRelation(TaxonNodeAgentRelation agentRelation) {
397
        agentRelation.setTaxonNode(this);
398
        this.agentRelations.add(agentRelation);
399
    }
400
    public void removeAgentRelation(TaxonNodeAgentRelation agentRelation) {
401
        agentRelation.setTaxonNode(this);
402
        agentRelations.remove(agentRelation);
403
    }
404

    
405
//********************
406

    
407
    //synonymToBeused
408
    public Synonym getSynonymToBeUsed() {
409
        return synonymToBeUsed;
410
    }
411
    public void setSynonymToBeUsed(Synonym synonymToBeUsed) {
412
        this.synonymToBeUsed = synonymToBeUsed;
413
    }
414

    
415
    //treeindex
416
    @Override
417
    public String treeIndex() {
418
        return treeIndex;
419
    }
420
    @Override
421
    @Deprecated //for CDM lib internal use only, may be removed in future versions
422
    public void setTreeIndex(String treeIndex) {
423
        this.treeIndex = treeIndex;
424
    }
425
    @Override
426
    public String treeIndexLike() {
427
        return treeIndex + "%";
428
    }
429
    @Override
430
    public String treeIndexWc() {
431
        return treeIndex + "*";
432
    }
433

    
434
//************************ METHODS **************************/
435

    
436
   @Override
437
    public TaxonNode addChildTaxon(Taxon taxon, Reference citation, String microCitation) {
438
        return addChildTaxon(taxon, this.childNodes.size(), citation, microCitation);
439
    }
440

    
441
   @Override
442
   public TaxonNode addChildTaxon(Taxon taxon, NamedSource source) {
443
       return addChildTaxon(taxon, this.childNodes.size(), source);
444
   }
445

    
446
    @Override
447
    public TaxonNode addChildTaxon(Taxon taxon, int index, Reference citation, String microCitation) {
448
        return addChildTaxon(taxon, index, NamedSource.NewPrimarySourceInstance(citation, microCitation));
449
    }
450

    
451
    @Override
452
    public TaxonNode addChildTaxon(Taxon taxon, int index, NamedSource source) {
453
        Classification classification = CdmBase.deproxy(this.getClassification());
454
        taxon = HibernateProxyHelper.deproxy(taxon, Taxon.class);
455
        if (classification.isTaxonInTree(taxon)){
456
            throw new IllegalArgumentException(String.format("Taxon may not be in a classification twice: %s", taxon.getTitleCache()));
457
       }
458
       return addChildNode(new TaxonNode(taxon), index, source);
459
    }
460

    
461
    /**
462
     * Moves a taxon node to a new parent. Descendents of the node are moved as well
463
     *
464
     * @param childNode the taxon node to be moved to the new parent
465
     * @return the child node in the state of having a new parent
466
     */
467
    @Override
468
    public TaxonNode addChildNode(TaxonNode childNode, Reference reference, String microReference){
469
        addChildNode(childNode, childNodes.size(), reference, microReference);
470
        return childNode;
471
    }
472

    
473
    /**
474
     * Inserts the given taxon node in the list of children of <i>this</i> taxon node
475
     * at the given (index + 1) position. If the given index is out of bounds
476
     * an exception will arise.<BR>
477
     * Due to bidirectionality this method must also assign <i>this</i> taxon node
478
     * as the parent of the given child.
479
     *
480
     * @param	child	the taxon node to be added
481
     * @param	index	the integer indicating the position at which the child
482
     * 					should be added
483
     * @see				#getChildNodes()
484
     * @see				#addChildNode(TaxonNode, Reference, String, Synonym)
485
     * @see				#deleteChildNode(TaxonNode)
486
     * @see				#deleteChildNode(int)
487
     */
488
    @Override
489
    public TaxonNode addChildNode(TaxonNode child, int index, Reference reference, String microReference){
490
        return addChildNode(child, index, NamedSource.NewPrimarySourceInstance(reference, microReference));
491
    }
492

    
493

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

    
519
        child.setParentTreeNode(this, index);
520

    
521
        child.setSource(source);
522

    
523
        return child;
524
    }
525

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

    
548

    
549

    
550

    
551
    @Override
552
    public boolean deleteChildNode(TaxonNode node) {
553
        boolean result = removeChildNode(node);
554
        Taxon taxon = deproxy(node.getTaxon());
555
        node = deproxy(node);
556
        node.setTaxon(null);
557

    
558
        ArrayList<TaxonNode> childNodes = new ArrayList<>(node.getChildNodes());
559
        for(TaxonNode childNode : childNodes){
560
            HibernateProxyHelper.deproxy(childNode, TaxonNode.class);
561
            node.deleteChildNode(childNode);
562
        }
563
        taxon.removeTaxonNode(node);
564
        return result;
565
    }
566

    
567
    /**
568
     * Deletes the child node and also removes children of childnode
569
     * recursively if delete children is <code>true</code>
570
     * @param node
571
     * @param deleteChildren
572
     * @return
573
     */
574
    public boolean deleteChildNode(TaxonNode node, boolean deleteChildren) {
575
        boolean result = removeChildNode(node);
576
        Taxon taxon = node.getTaxon();
577
        node.setTaxon(null);
578
        taxon.removeTaxonNode(node);
579
        if (deleteChildren){
580
            ArrayList<TaxonNode> childNodes = new ArrayList<>(node.getChildNodes());
581
            for(TaxonNode childNode : childNodes){
582
                node.deleteChildNode(childNode, deleteChildren);
583
            }
584
        } else{
585
        	ArrayList<TaxonNode> childNodes = new ArrayList<>(node.getChildNodes());
586
            for(TaxonNode childNode : childNodes){
587
             this.addChildNode(childNode, null, null);
588
            }
589
        }
590

    
591
        return result;
592
    }
593

    
594
    /**
595
     * Removes the child node from this node. Sets the parent and the classification of the child
596
     * node to null
597
     *
598
     * @param childNode
599
     * @return
600
     */
601
    protected boolean removeChildNode(TaxonNode childNode){
602
        boolean result = true;
603
        //removeNullValueFromChildren();
604
        if(childNode == null){
605
            throw new IllegalArgumentException("TaxonNode may not be null");
606
        }
607
        int index = childNodes.indexOf(childNode);
608
        if (index >= 0){
609
            removeChild(index);
610
        } else {
611
            result = false;
612
        }
613
        return result;
614
    }
615

    
616
    /**
617
     * Removes the child node placed at the given (index + 1) position
618
     * from the list of {@link #getChildNodes() children} of <i>this</i> taxon node.
619
     * Sets the parent and the classification of the child
620
     * node to null.
621
     * If the given index is out of bounds no child will be removed.<BR>
622
     * NOTE: this is more for inner use. It does not remove the node from the taxon!!
623
     * Use deleteChildNode(TaxonNode) instead
624
     *
625
     * @param  index	the integer indicating the position of the taxon node to
626
     * 					be removed
627
     * @see     		#getChildNodes()
628
     * @see				#addChildNode(TaxonNode, Reference, String)
629
     * @see				#addChildNode(TaxonNode, int, Reference, String)
630
     * @see				#deleteChildNode(TaxonNode)
631
     */
632
    public void removeChild(int index){
633
        //TODO: Only as a workaround. We have to find out why merge creates null entries.
634

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

    
638
        if (child != null){
639

    
640
            TaxonNode parent = HibernateProxyHelper.deproxy(child.getParent());
641
            TaxonNode thisNode = HibernateProxyHelper.deproxy(this);
642
            if(parent != null && parent != thisNode){
643
                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());
644
            }else if (parent == null){
645
                throw new IllegalStateException("Parent of child is null in TaxonNode.removeChild(int). This should not happen.");
646
            }
647
            childNodes.remove(index);
648
            child.setClassification(null);
649

    
650
            this.countChildren = childNodes.size();
651
            child.setParent(null);
652
            child.setTreeIndex(null);
653
        }
654
    }
655

    
656
    /**
657
     * Remove this taxonNode From its taxonomic parent
658
     *
659
     * @return true on success
660
     */
661
    public boolean delete(){
662
        if(isTopmostNode()){
663
            return classification.deleteChildNode(this);
664
        }else{
665
            return getParent().deleteChildNode(this);
666
        }
667
    }
668

    
669
    /**
670
     * Remove this taxonNode From its taxonomic parent
671
     *
672
     * @return true on success
673
     */
674
    public boolean delete(boolean deleteChildren){
675
        if(isTopmostNode()){
676
            return classification.deleteChildNode(this, deleteChildren);
677
        }else{
678
            return getParent().deleteChildNode(this, deleteChildren);
679
        }
680
    }
681

    
682
    @Override
683
    @Deprecated //for CDM lib internal use only, may be removed in future versions
684
    public int treeId() {
685
        if (this.classification == null){
686
        	logger.warn("TaxonNode has no classification. This should not happen.");  //#3840
687
        	return -1;
688
        }else{
689
        	return this.classification.getId();
690
        }
691
    }
692

    
693

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

    
713
        	//remove from old parent
714
            formerParent.removeChildNode(this);
715
        }
716

    
717
        // set the new parent
718
        setParent(parent);
719

    
720
        // set the classification to the parents classification
721

    
722
        Classification classification = parent.getClassification();
723
        //FIXME also set the tree index here for performance reasons
724
        classification = CdmBase.deproxy(classification);
725
        setClassificationRecursively(classification);
726
        // add this node to the parent's child nodes
727
        parent = CdmBase.deproxy(parent);
728
        List<TaxonNode> parentChildren = parent.getChildNodes();
729

    
730
        if (index > parent.getChildNodes().size()){
731
            index = parent.getChildNodes().size();
732
        }
733
        if (parentChildren.contains(this)){
734
            //avoid duplicates
735
            if (parentChildren.indexOf(this) < index){
736
                index = index-1;
737
            }
738
            parentChildren.remove(this);
739
            parentChildren.add(index, this);
740
        }else{
741
            parentChildren.add(index, this);
742
        }
743

    
744
//        //only for debugging
745
//        if (this.getSortIndex() == null){
746
//            logger.warn("sortindex is null. This should not happen.");
747
//        }else 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
     * Returns a set containing this node and all nodes that are descendants of this node.
757
     */
758
	@Transient
759
    protected Set<TaxonNode> getDescendants(){
760
        Set<TaxonNode> nodeSet = new HashSet<>();
761

    
762
        nodeSet.add(this);
763

    
764
        for(TaxonNode childNode : getChildNodes()){
765
            nodeSet.addAll(childNode.getDescendants());
766
        }
767

    
768
        return nodeSet;
769
    }
770

    
771
    /**
772
     * Returns a set containing a clone of this node and of all nodes that are descendants of this node
773
     *
774
     * @return
775
     */
776
    protected TaxonNode cloneDescendants(){
777

    
778
        TaxonNode clone = this.clone();
779
        TaxonNode childClone;
780

    
781
        for(TaxonNode childNode : getChildNodes()){
782
            childClone = childNode.clone();
783
            for (TaxonNode childChild:childNode.getChildNodes()){
784
                childClone.addChildNode(childChild.cloneDescendants(), childChild.getReference(), childChild.getMicroReference());
785
            }
786
            clone.addChildNode(childClone, childNode.getReference(), childNode.getMicroReference());
787
            //childClone.addChildNode(childNode.cloneDescendants());
788
        }
789
        return clone;
790
    }
791

    
792
    /**
793
     * Returns all ancestor nodes of this node
794
     *
795
     * @return a set of all parent nodes
796
     */
797
    @Transient
798
    protected Set<TaxonNode> getAncestors(){
799
        Set<TaxonNode> nodeSet = new HashSet<>();
800
        if(this.getParent() != null){
801
        	TaxonNode parent =  CdmBase.deproxy(this.getParent());
802
        	nodeSet.add(parent);
803
            nodeSet.addAll(parent.getAncestors());
804
        }
805
        return nodeSet;
806
    }
807

    
808
    /**
809
     * Retrieves the first ancestor of the given rank. If any of the ancestors
810
     * has no taxon or has a rank > the given rank <code>null</code> is returned.
811
     * If <code>this</code> taxon is already of given rank this taxon is returned.
812
     * @param rank the rank the ancestor should have
813
     * @return the first found instance of a parent taxon node with the given rank
814
     */
815
    @Transient
816
    public TaxonNode getAncestorOfRank(Rank rank){
817
        Taxon taxon = CdmBase.deproxy(this.getTaxon());
818
        if (taxon == null){
819
            return null;
820
        }
821
        TaxonName name = CdmBase.deproxy(taxon.getName());
822
        if (name != null && name.getRank() != null){
823
            if (name.getRank().isHigher(rank)){
824
                return null;
825
            }
826
            if (name.getRank().equals(rank)){
827
                return this;
828
            }
829
        }
830

    
831
        if(this.getParent() != null){
832
        	TaxonNode parent =  CdmBase.deproxy(this.getParent());
833
            return parent.getAncestorOfRank(rank);
834
        }
835
		return null;
836
    }
837

    
838
    /**
839
     * Returns the ancestor taxa, starting with the highest (e.g. kingdom)
840
     * @return
841
     */
842
    @Transient
843
    public List<Taxon> getAncestorTaxaList(){
844
        List<Taxon> result = new ArrayList<>();
845
        TaxonNode current = this;
846
        while (current != null){
847
            if (current.getTaxon() != null){
848
                result.add(0, current.getTaxon());
849
            }
850
            current = current.getParent();
851
        }
852
        return result;
853
    }
854

    
855
    /**
856
     * Returns the ancestor taxon nodes, that do have a taxon attached
857
     * (excludes the root node) starting with the highest
858
     */
859
    @Transient
860
    public List<TaxonNode> getAncestorList(){
861
        List<TaxonNode> result = new ArrayList<>();
862
        TaxonNode current = this.getParent();
863
        while (current != null){
864
            if (current.getTaxon() != null){
865
                result.add(0, current);
866
            }
867
            current = current.getParent();
868
        }
869
        return result;
870
    }
871

    
872

    
873
    /**
874
     * Whether this TaxonNode is a direct child of the classification TreeNode
875
     */
876
    @Transient
877
    public boolean isTopmostNode(){
878
    	boolean parentCheck = false;
879
    	boolean classificationCheck = false;
880

    
881
    	if(getParent() != null) {
882
    		if(getParent().getTaxon() == null) {
883
    			parentCheck = true;
884
    		}
885
    	}
886

    
887
    	//TODO remove
888
    	// FIXME This should work but doesn't, due to missing sort indexes, can be removed after fixing #4200, #4098
889
    	if (classification != null){
890
    		classificationCheck = classification.getRootNode().getChildNodes().contains(this);
891
    	}else{
892
    		classificationCheck = false;
893
    	}
894

    
895
    	// The following is just for logging purposes for the missing sort indexes problem
896
    	// ticket #4098
897
    	if(parentCheck != classificationCheck) {
898
    		logger.warn("isTopmost node check " + parentCheck + " not same as classificationCheck : " + classificationCheck + " for taxon node ");
899
    		if(this.getParent() != null) {
900
    			logger.warn("-- with parent uuid " + this.getParent().getUuid().toString());
901
    			logger.warn("-- with parent id " + this.getParent().getId());
902
    			for(TaxonNode node : this.getParent().getChildNodes()) {
903
    				if(node == null) {
904
    					logger.warn("-- child node is null");
905
    				} else if (node.getTaxon() == null) {
906
    					logger.warn("-- child node taxon is null");
907
    				}
908
    			}
909
    			logger.warn("-- parent child count" + this.getParent().getChildNodes().size());
910
    		}
911
    	}
912

    
913
    	return parentCheck;
914
    }
915

    
916
    /**
917
     * Whether this TaxonNode is a descendant of (or equal to) the given TaxonNode
918
     *
919
     * @param possibleParent
920
     * @return <code>true</code> if <b>this</b> is a descendant
921
     */
922
    @Transient
923
    public boolean isDescendant(TaxonNode possibleParent){
924
    	if (possibleParent == null || this.treeIndex() == null
925
    	        || possibleParent.treeIndex() == null) {
926
    		return false;
927
    	}
928
    	return this.treeIndex().startsWith(possibleParent.treeIndex() );
929
    }
930

    
931
    /**
932
     * Whether this TaxonNode is an ascendant of (or equal to) the given TaxonNode.
933
     *
934
     * @param possibleChild
935
     * @return <code>true</code> if <b>this</b> is a ancestor of the given child parameter
936
     */
937
    @Transient
938
    public boolean isAncestor(TaxonNode possibleChild){
939
    	if (possibleChild == null || this.treeIndex() == null || possibleChild.treeIndex() == null) {
940
    		return false;
941
    	}
942
       // return possibleChild == null ? false : possibleChild.getAncestors().contains(this);
943
        return  possibleChild.treeIndex().startsWith(this.treeIndex());
944
    }
945

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

    
957
    public boolean hasTaxon() {
958
        return (taxon!= null);
959
    }
960

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

    
966
    @Transient
967
    public TaxonName getNullSafeName() {
968
        return getTaxon() == null? null: getTaxon().getName();
969
    }
970

    
971
    private boolean hasStatus(TaxonNodeStatus status) {
972
        return CdmUtils.nullSafeEqual(this.status, status);
973
    }
974

    
975
//*********************** CLONE ********************************************************/
976
    /**
977
     * Clones <i>this</i> taxon node. This is a shortcut that enables to create
978
     * a new instance that differs only slightly from <i>this</i> taxon node by
979
     * modifying only some of the attributes.<BR><BR>
980
     * The child nodes are not copied.<BR>
981
     * The taxon and parent are the same as for the original taxon node. <BR>
982
     * <BR>
983
     * Note: Cloning taxon nodes with cloning taxa (and children) is a complex
984
     * issue which is better be handled in service layer logic. See according
985
     * clone method in classification service
986
     * or taxon node service there.
987
     *
988
     * @see java.lang.Object#clone()
989
     */
990
    @Override
991
    public TaxonNode clone()  {
992

    
993
        try{
994
            TaxonNode result = (TaxonNode)super.clone();
995
            result.getTaxon().addTaxonNode(result);
996

    
997
            //childNodes
998
            result.childNodes = new ArrayList<>();
999
            result.countChildren = 0;
1000

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

    
1007
            //statusNote
1008
            result.statusNote = new HashMap<>();
1009
            for(Language lang : this.statusNote.keySet()){
1010
                result.statusNote.put(lang, this.statusNote.get(lang));
1011
            }
1012

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

    
1021
//To be removed once SingleSourcedEntity attributes have been renamed to reference and microReference
1022

    
1023
    @Override
1024
    @Transient
1025
    public Reference getReference() {
1026
        return getCitation();
1027
    }
1028

    
1029
    @Override
1030
    @Transient
1031
    public String getMicroReference() {
1032
        return getCitationMicroReference();
1033
    }
1034

    
1035
}
(11-11/20)