Project

General

Profile

Download (35 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
    "countChildren",
85
    "agentRelations",
86
    "synonymToBeUsed",
87
    "status",
88
    "statusNote"
89
})
90
@XmlRootElement(name = "TaxonNode")
91
@Entity
92
//@Indexed disabled to reduce clutter in indexes, since this type is not used by any search
93
//@Indexed(index = "eu.etaxonomy.cdm.model.taxon.TaxonNode")
94
@Audited
95
@Table(name="TaxonNode", indexes = { @javax.persistence.Index(name = "taxonNodeTreeIndex", columnList = "treeIndex") })
96
@ChildTaxaMustBeLowerRankThanParent(groups = Level3.class)
97
@ChildTaxaMustNotSkipRanks(groups = Level3.class)
98
@ChildTaxaMustDeriveNameFromParent(groups = Level3.class)
99
public class TaxonNode
100
            extends SingleSourcedEntityBase
101
            implements ITaxonTreeNode, ITreeNode<TaxonNode>{
102

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

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

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

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

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

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

    
148
    @XmlElement(name = "countChildren")
149
    private int countChildren;
150

    
151
    @XmlElementWrapper(name = "agentRelations")
152
    @XmlElement(name = "agentRelation")
153
    @XmlIDREF
154
    @XmlSchemaType(name = "IDREF")
155
    @OneToMany(mappedBy="taxonNode", fetch=FetchType.LAZY)
156
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
157
    private Set<TaxonNodeAgentRelation> agentRelations = new HashSet<>();
158

    
159
    /**
160
     * The {@link TaxonNodeStatus status} of this taxon node.
161
     */
162
    @XmlAttribute(name ="TaxonNodeStatus")
163
    @Column(name="status", length=10)
164
    @Type(type = "eu.etaxonomy.cdm.hibernate.EnumUserType",
165
        parameters = {@Parameter(name  = "enumClass", value = "eu.etaxonomy.cdm.model.taxon.TaxonNodeStatus")}
166
    )
167
    @Audited
168
    private TaxonNodeStatus status;
169

    
170
    @XmlElement(name = "statusNote")
171
    @XmlJavaTypeAdapter(MultilanguageTextAdapter.class)
172
    @OneToMany(fetch = FetchType.LAZY, orphanRemoval=true)
173
//    @MapKeyJoinColumn(name="statusNote_mapkey_id")
174
    @JoinTable(name = "TaxonNode_StatusNote")  //to make possible to add also unplacedNote
175
    @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE, CascadeType.DELETE})
176
    private Map<Language,LanguageString> statusNote = new HashMap<>();
177

    
178
//	private Taxon originalConcept;
179
//	//or
180
    @XmlElement(name = "synonymToBeUsed")
181
    @XmlIDREF
182
    @XmlSchemaType(name = "IDREF")
183
    @ManyToOne(fetch = FetchType.LAZY)
184
    @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
185
    private Synonym synonymToBeUsed;
186

    
187
// ******************** CONSTRUCTOR **********************************************/
188

    
189
    //for hibernate use only, *packet* private required by bytebuddy
190
    @Deprecated
191
    TaxonNode(){}
192

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

    
207
    /**
208
     * To create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}
209
     * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}
210
     *
211
     * @param taxon
212
     */
213
    protected TaxonNode(Taxon taxon){
214
        setTaxon(taxon);
215
    }
216

    
217
// ************************* GETTER / SETTER *******************************/
218

    
219
    public Taxon getTaxon() {
220
        return taxon;
221
    }
222
    public void setTaxon(Taxon taxon) {
223
        this.taxon = taxon;
224
        if (taxon != null){
225
            taxon.addTaxonNode(this);
226
        }
227
    }
228

    
229
    @Override
230
    public List<TaxonNode> getChildNodes() {
231
        return childNodes;
232
    }
233
	protected void setChildNodes(List<TaxonNode> childNodes) {
234
		this.childNodes = childNodes;
235
	}
236

    
237
    public Classification getClassification() {
238
        return classification;
239
    }
240
    /**
241
     * THIS METHOD SHOULD NOT BE CALLED!
242
     * invisible part of the bidirectional relationship, for public use Classification.addRoot() or TaxonNode.addChild()
243
     * @param classification
244
     * @deprecated for internal use only
245
     */
246
    @Deprecated
247
    protected void setClassification(Classification classification) {
248
        this.classification = classification;
249
    }
250

    
251
    public boolean isUnplaced() {return hasStatus(TaxonNodeStatus.UNPLACED);}
252

    
253
    //#8281 indicates a preliminary placement
254
    public boolean isDoubtful() {return hasStatus(TaxonNodeStatus.DOUBTFUL);}
255

    
256
    public boolean isExcluded() {return hasStatus(TaxonNodeStatus.EXCLUDED);}
257

    
258
//************************************************************/
259

    
260
    /**
261
     * The computed order index of this node being a child in the parents
262
     * childnode list.
263
     */
264
    @Transient
265
    public Integer getSortIndex() {
266
        return getParent() == null ? null : getParent().getChildNodes().indexOf(CdmBase.deproxy(this));
267
    }
268

    
269
    //countChildren
270
    public int getCountChildren() {
271
        return countChildren;
272
    }
273
    /**
274
     * @deprecated for internal use only
275
     * @param countChildren
276
     */
277
    @Deprecated
278
    protected void setCountChildren(int countChildren) {
279
        this.countChildren = countChildren;
280
    }
281

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

    
302
    public TaxonNodeStatus getStatus() {
303
        return status;
304
    }
305
    public void setStatus(TaxonNodeStatus status) {
306
        this.status = status;
307
    }
308

    
309
// *************** Status Note ***************
310

    
311
    /**
312
     * Returns the {@link MultilanguageText multi-language text} to add a note to the
313
     * status. The different {@link LanguageString language strings}
314
     * contained in the multi-language text should all have the same meaning.
315
     * @see #getStatusNote(Language)
316
     * @see #putStatusNote(Language, String)
317
     */
318
    public Map<Language,LanguageString> getStatusNote(){
319
        return this.statusNote;
320
    }
321

    
322
    /**
323
     * Returns the status note string in the given {@link Language language}
324
     *
325
     * @param language  the language in which the description string looked for is formulated
326
     * @see             #getStatusNote()
327
     * @see             #putStatusNote(Language, String)
328
     */
329
    public String getStatusNote(Language language){
330
        LanguageString languageString = statusNote.get(language);
331
        if (languageString == null){
332
            return null;
333
        }else{
334
            return languageString.getText();
335
        }
336
    }
337

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

    
367
    /**
368
     * Removes from the {@link MultilanguageText multilanguage text} used to annotate
369
     * the status the one {@link LanguageString language string}
370
     * with the given {@link Language language}.
371
     *
372
     * @param  lang the language in which the language string to be removed
373
     *       has been formulated
374
     * @see         #getStatusNote()
375
     */
376
    public void removeStatusNote(Language lang){
377
        this.statusNote.remove(lang);
378
    }
379

    
380
// ****************** Agent Relations ****************************/
381

    
382
    public Set<TaxonNodeAgentRelation> getAgentRelations() {
383
        return this.agentRelations;
384
    }
385
    public TaxonNodeAgentRelation addAgentRelation(DefinedTerm type, TeamOrPersonBase<?> agent){
386
        TaxonNodeAgentRelation result = TaxonNodeAgentRelation.NewInstance(this, agent, type);
387
        return result;
388
    }
389
    public void addAgentRelation(TaxonNodeAgentRelation agentRelation) {
390
        agentRelation.setTaxonNode(this);
391
        this.agentRelations.add(agentRelation);
392
    }
393
    public void removeAgentRelation(TaxonNodeAgentRelation agentRelation) {
394
        agentRelation.setTaxonNode(this);
395
        agentRelations.remove(agentRelation);
396
    }
397

    
398
//********************
399

    
400
    //synonymToBeused
401
    public Synonym getSynonymToBeUsed() {
402
        return synonymToBeUsed;
403
    }
404
    public void setSynonymToBeUsed(Synonym synonymToBeUsed) {
405
        this.synonymToBeUsed = synonymToBeUsed;
406
    }
407

    
408
    //treeindex
409
    @Override
410
    public String treeIndex() {
411
        return treeIndex;
412
    }
413
    @Override
414
    @Deprecated //for CDM lib internal use only, may be removed in future versions
415
    public void setTreeIndex(String treeIndex) {
416
        this.treeIndex = treeIndex;
417
    }
418
    @Override
419
    public String treeIndexLike() {
420
        return treeIndex + "%";
421
    }
422
    @Override
423
    public String treeIndexWc() {
424
        return treeIndex + "*";
425
    }
426

    
427
//************************ METHODS **************************/
428

    
429
   @Override
430
    public TaxonNode addChildTaxon(Taxon taxon, Reference citation, String microCitation) {
431
        return addChildTaxon(taxon, this.childNodes.size(), citation, microCitation);
432
    }
433

    
434
   @Override
435
   public TaxonNode addChildTaxon(Taxon taxon, NamedSource source) {
436
       return addChildTaxon(taxon, this.childNodes.size(), source);
437
   }
438

    
439
    @Override
440
    public TaxonNode addChildTaxon(Taxon taxon, int index, Reference citation, String microCitation) {
441
        return addChildTaxon(taxon, index, NamedSource.NewPrimarySourceInstance(citation, microCitation));
442
    }
443

    
444
    @Override
445
    public TaxonNode addChildTaxon(Taxon taxon, int index, NamedSource source) {
446
        Classification classification = CdmBase.deproxy(this.getClassification());
447
        taxon = HibernateProxyHelper.deproxy(taxon, Taxon.class);
448
        if (classification.isTaxonInTree(taxon)){
449
            throw new IllegalArgumentException(String.format("Taxon may not be in a classification twice: %s", taxon.getTitleCache()));
450
       }
451
       return addChildNode(new TaxonNode(taxon), index, source);
452
    }
453

    
454
    /**
455
     * Moves a taxon node to a new parent. Descendents of the node are moved as well
456
     *
457
     * @param childNode the taxon node to be moved to the new parent
458
     * @return the child node in the state of having a new parent
459
     */
460
    @Override
461
    public TaxonNode addChildNode(TaxonNode childNode, Reference reference, String microReference){
462
        addChildNode(childNode, childNodes.size(), reference, microReference);
463
        return childNode;
464
    }
465

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

    
486

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

    
512
        child.setParentTreeNode(this, index);
513

    
514
        child.setSource(source);
515

    
516
        return child;
517
    }
518

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

    
541

    
542

    
543

    
544
    @Override
545
    public boolean deleteChildNode(TaxonNode node) {
546
        boolean result = removeChildNode(node);
547
        Taxon taxon = deproxy(node.getTaxon());
548
        node = deproxy(node);
549
        node.setTaxon(null);
550

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

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

    
584
        return result;
585
    }
586

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

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

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

    
631
        if (child != null){
632

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

    
643
            this.countChildren = childNodes.size();
644
            child.setParent(null);
645
            child.setTreeIndex(null);
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
    @Transient
692
    protected void setParentTreeNode(TaxonNode parent, int index){
693
        // remove ourselves from the old parent
694
        TaxonNode formerParent = this.getParent();
695
        formerParent = CdmBase.deproxy(formerParent);
696
        if (formerParent != null){
697
        	//special case, child already exists for same parent
698
            //FIXME document / check for correctness
699
            if (formerParent.equals(parent)){
700
                int currentIndex = formerParent.getChildNodes().indexOf(this);
701
                if (currentIndex != -1 && currentIndex < index){
702
                    index--;
703
                }
704
        	}
705

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

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

    
713
        // set the classification to the parents classification
714

    
715
        Classification classification = parent.getClassification();
716
        //FIXME also set the tree index here for performance reasons
717
        classification = CdmBase.deproxy(classification);
718
        setClassificationRecursively(classification);
719
        // add this node to the parent's child nodes
720
        parent = CdmBase.deproxy(parent);
721
        List<TaxonNode> parentChildren = parent.getChildNodes();
722

    
723
        if (index > parent.getChildNodes().size()){
724
            index = parent.getChildNodes().size();
725
        }
726
        if (parentChildren.contains(this)){
727
            //avoid duplicates
728
            if (parentChildren.indexOf(this) < index){
729
                index = index-1;
730
            }
731
            parentChildren.remove(this);
732
            parentChildren.add(index, this);
733
        }else{
734
            parentChildren.add(index, this);
735
        }
736

    
737
//        //only for debugging
738
//        if (this.getSortIndex() == null){
739
//            logger.warn("sortindex is null. This should not happen.");
740
//        }else if (! this.getSortIndex().equals(index)){
741
//        	logger.warn("index and sortindex are not equal: " +  this.getSortIndex() + ";" + index);
742
//        }
743

    
744
        // update the children count
745
        parent.setCountChildren(parent.getChildNodes().size());
746
    }
747

    
748
    /**
749
     * Returns a set containing this node and all nodes that are descendants of this node.
750
     */
751
	@Transient
752
    protected Set<TaxonNode> getDescendants(){
753
        Set<TaxonNode> nodeSet = new HashSet<>();
754

    
755
        nodeSet.add(this);
756

    
757
        for(TaxonNode childNode : getChildNodes()){
758
            nodeSet.addAll(childNode.getDescendants());
759
        }
760

    
761
        return nodeSet;
762
    }
763

    
764
    /**
765
     * Returns a set containing a clone of this node and of all nodes that are descendants of this node
766
     *
767
     * @return
768
     */
769
    protected TaxonNode cloneDescendants(){
770

    
771
        TaxonNode clone = this.clone();
772
        TaxonNode childClone;
773

    
774
        for(TaxonNode childNode : getChildNodes()){
775
            childClone = childNode.clone();
776
            for (TaxonNode childChild:childNode.getChildNodes()){
777
                childClone.addChildNode(childChild.cloneDescendants(), childChild.getReference(), childChild.getMicroReference());
778
            }
779
            clone.addChildNode(childClone, childNode.getReference(), childNode.getMicroReference());
780
            //childClone.addChildNode(childNode.cloneDescendants());
781
        }
782
        return clone;
783
    }
784

    
785
    /**
786
     * Returns all ancestor nodes of this node
787
     *
788
     * @return a set of all parent nodes
789
     */
790
    @Transient
791
    protected Set<TaxonNode> getAncestors(){
792
        Set<TaxonNode> nodeSet = new HashSet<>();
793
        if(this.getParent() != null){
794
        	TaxonNode parent =  CdmBase.deproxy(this.getParent());
795
        	nodeSet.add(parent);
796
            nodeSet.addAll(parent.getAncestors());
797
        }
798
        return nodeSet;
799
    }
800

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

    
824
        if(this.getParent() != null){
825
        	TaxonNode parent =  CdmBase.deproxy(this.getParent());
826
            return parent.getAncestorOfRank(rank);
827
        }
828
		return null;
829
    }
830

    
831
    /**
832
     * Returns the ancestor taxa, starting with the highest (e.g. kingdom)
833
     * @return
834
     */
835
    @Transient
836
    public List<Taxon> getAncestorTaxaList(){
837
        List<Taxon> result = new ArrayList<>();
838
        TaxonNode current = this;
839
        while (current != null){
840
            if (current.getTaxon() != null){
841
                result.add(0, current.getTaxon());
842
            }
843
            current = current.getParent();
844
        }
845
        return result;
846
    }
847

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

    
865

    
866
    /**
867
     * Whether this TaxonNode is a direct child of the classification TreeNode
868
     */
869
    @Transient
870
    public boolean isTopmostNode(){
871
    	boolean parentCheck = false;
872
    	boolean classificationCheck = false;
873

    
874
    	if(getParent() != null) {
875
    		if(getParent().getTaxon() == null) {
876
    			parentCheck = true;
877
    		}
878
    	}
879

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

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

    
906
    	return parentCheck;
907
    }
908

    
909
    /**
910
     * Whether this TaxonNode is a descendant of (or equal to) the given TaxonNode
911
     *
912
     * @param possibleParent
913
     * @return <code>true</code> if <b>this</b> is a descendant
914
     */
915
    @Transient
916
    public boolean isDescendant(TaxonNode possibleParent){
917
    	if (possibleParent == null || this.treeIndex() == null
918
    	        || possibleParent.treeIndex() == null) {
919
    		return false;
920
    	}
921
    	return this.treeIndex().startsWith(possibleParent.treeIndex() );
922
    }
923

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

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

    
950
    public boolean hasTaxon() {
951
        return (taxon!= null);
952
    }
953

    
954
    @Transient
955
    public Rank getNullSafeRank() {
956
        return hasTaxon() ? getTaxon().getNullSafeRank() : null;
957
    }
958

    
959
    @Transient
960
    public TaxonName getNullSafeName() {
961
        return getTaxon() == null? null: getTaxon().getName();
962
    }
963

    
964
    private boolean hasStatus(TaxonNodeStatus status) {
965
        return CdmUtils.nullSafeEqual(this.status, status);
966
    }
967

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

    
986
        try{
987
            TaxonNode result = (TaxonNode)super.clone();
988
            result.getTaxon().addTaxonNode(result);
989

    
990
            //childNodes
991
            result.childNodes = new ArrayList<>();
992
            result.countChildren = 0;
993

    
994
            //agents
995
            result.agentRelations = new HashSet<>();
996
            for (TaxonNodeAgentRelation rel : this.agentRelations){
997
                result.addAgentRelation(rel.clone());
998
            }
999

    
1000
            //statusNote
1001
            result.statusNote = new HashMap<>();
1002
            for(Language lang : this.statusNote.keySet()){
1003
                result.statusNote.put(lang, this.statusNote.get(lang));
1004
            }
1005

    
1006
            return result;
1007
        }catch (CloneNotSupportedException e) {
1008
            logger.warn("Object does not implement cloneable");
1009
            e.printStackTrace();
1010
            return null;
1011
        }
1012
    }
1013

    
1014
//To be removed once SingleSourcedEntity attributes have been renamed to reference and microReference
1015

    
1016
    @Override
1017
    @Transient
1018
    public Reference getReference() {
1019
        return getCitation();
1020
    }
1021

    
1022
    @Override
1023
    @Transient
1024
    public String getMicroReference() {
1025
        return getCitationMicroReference();
1026
    }
1027

    
1028
}
(11-11/20)