Project

General

Profile

Download (37.8 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.HHH_9751_Util;
55
import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
56
import eu.etaxonomy.cdm.jaxb.MultilanguageTextAdapter;
57
import eu.etaxonomy.cdm.model.agent.TeamOrPersonBase;
58
import eu.etaxonomy.cdm.model.common.CdmBase;
59
import eu.etaxonomy.cdm.model.common.ITreeNode;
60
import eu.etaxonomy.cdm.model.common.Language;
61
import eu.etaxonomy.cdm.model.common.LanguageString;
62
import eu.etaxonomy.cdm.model.common.MultilanguageText;
63
import eu.etaxonomy.cdm.model.common.SingleSourcedEntityBase;
64
import eu.etaxonomy.cdm.model.name.Rank;
65
import eu.etaxonomy.cdm.model.name.TaxonName;
66
import eu.etaxonomy.cdm.model.reference.NamedSource;
67
import eu.etaxonomy.cdm.model.reference.Reference;
68
import eu.etaxonomy.cdm.model.term.DefinedTerm;
69
import eu.etaxonomy.cdm.validation.Level3;
70
import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustBeLowerRankThanParent;
71
import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustDeriveNameFromParent;
72
import eu.etaxonomy.cdm.validation.annotation.ChildTaxaMustNotSkipRanks;
73

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

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

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

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

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

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

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

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

    
153
    @XmlElement(name = "countChildren")
154
    private int countChildren;
155

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

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

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

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

    
192
// ******************** CONSTRUCTOR **********************************************/
193

    
194
    protected TaxonNode(){super();}
195

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

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

    
220
// ************************* GETTER / SETTER *******************************/
221

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

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

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

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

    
274
    public boolean isUnplaced() {return hasStatus(TaxonNodeStatus.UNPLACED);}
275

    
276
    //#8281 indicates a preliminary placement
277
    public boolean isDoubtful() {return hasStatus(TaxonNodeStatus.DOUBTFUL);}
278

    
279
    public boolean isExcluded() {return hasStatus(TaxonNodeStatus.EXCLUDED);}
280

    
281
//************************* SOURCE *********************/
282

    
283
//    @Override
284
//    @Transient
285
//    public String getMicroReference() {
286
//        return source == null ? null : this.source.getCitationMicroReference();
287
//    }
288
//    public void setMicroReference(String microReference) {
289
//        this.getSource(true).setCitationMicroReference(StringUtils.isBlank(microReference)? null : microReference);
290
//        checkNullSource();
291
//    }
292
//
293
//    @Override
294
//    @Transient
295
//    public Reference getReference() {
296
//        return (this.source == null) ? null : source.getCitation();
297
//    }
298
//    public void setReference(Reference reference) {
299
//        getSource(true).setCitation(reference);
300
//        checkNullSource();
301
//    }
302

    
303
//    @Override
304
//    public DescriptionElementSource getSource() {
305
//        return source;
306
//    }
307
//    @Override
308
//    public void setSource(DescriptionElementSource source) {
309
//        this.source = source;
310
//    }
311

    
312

    
313

    
314
//************************************************************/
315

    
316
    //countChildren
317
    public int getCountChildren() {
318
        return countChildren;
319
    }
320
    /**
321
     * @deprecated for internal use only
322
     * @param countChildren
323
     */
324
    @Deprecated
325
    protected void setCountChildren(int countChildren) {
326
        this.countChildren = countChildren;
327
    }
328

    
329
    //parent
330
    @Override
331
    public TaxonNode getParent(){
332
        return parent;
333
    }
334
    /**
335
     * Sets the parent of this taxon node.<BR>
336
     *
337
     * In most cases you would want to call setParentTreeNode(ITreeNode) which
338
     * handles updating of the bidirectional relationship
339
     *
340
     * @see setParentTreeNode(ITreeNode)
341
     * @param parent
342
     *
343
     */
344
    protected void setParent(TaxonNode parent) {
345
        this.parent = parent;
346
//        this.treeIndex = parent.treeIndex() +
347
    }
348

    
349
    public TaxonNodeStatus getStatus() {
350
        return status;
351
    }
352
    public void setStatus(TaxonNodeStatus status) {
353
        this.status = status;
354
    }
355

    
356
// *************** Status Note ***************
357

    
358
    /**
359
     * Returns the {@link MultilanguageText multi-language text} to add a note to the
360
     * status. The different {@link LanguageString language strings}
361
     * contained in the multi-language text should all have the same meaning.
362
     * @see #getStatusNote(Language)
363
     * @see #putStatusNote(Language, String)
364
     */
365
    public Map<Language,LanguageString> getStatusNote(){
366
        return this.statusNote;
367
    }
368

    
369
    /**
370
     * Returns the status note string in the given {@link Language language}
371
     *
372
     * @param language  the language in which the description string looked for is formulated
373
     * @see             #getStatusNote()
374
     * @see             #putStatusNote(Language, String)
375
     */
376
    public String getStatusNote(Language language){
377
        LanguageString languageString = statusNote.get(language);
378
        if (languageString == null){
379
            return null;
380
        }else{
381
            return languageString.getText();
382
        }
383
    }
384

    
385
    /**
386
     * Adds a translated {@link LanguageString text in a particular language}
387
     * to the {@link MultilanguageText multilanguage text} used to add a note to
388
     * the {@link #getStatus() status}.
389
     *
390
     * @param statusNote   the language string adding a note to the status
391
     *                      in a particular language
392
     * @see                 #getStatusNote()
393
     * @see                 #putStatusNote(String, Language)
394
     */
395
    public void putStatusNote(LanguageString statusNote){
396
        this.statusNote.put(statusNote.getLanguage(), statusNote);
397
    }
398
    /**
399
     * Creates a {@link LanguageString language string} based on the given text string
400
     * and the given {@link Language language} and adds it to the {@link MultilanguageText
401
     * multi-language text} used to annotate the status.
402
     *
403
     * @param text      the string annotating the status
404
     *                  in a particular language
405
     * @param language  the language in which the text string is formulated
406
     * @see             #getStatusNote()
407
     * @see             #putStatusNote(LanguageString)
408
     * @see             #removeStatusNote(Language)
409
     */
410
    public void putStatusNote(Language language, String text){
411
        this.statusNote.put(language, LanguageString.NewInstance(text, language));
412
    }
413

    
414
    /**
415
     * Removes from the {@link MultilanguageText multilanguage text} used to annotate
416
     * the status the one {@link LanguageString language string}
417
     * with the given {@link Language language}.
418
     *
419
     * @param  lang the language in which the language string to be removed
420
     *       has been formulated
421
     * @see         #getStatusNote()
422
     */
423
    public void removeStatusNote(Language lang){
424
        this.statusNote.remove(lang);
425
    }
426

    
427
// ****************** Agent Relations ****************************/
428

    
429
    public Set<TaxonNodeAgentRelation> getAgentRelations() {
430
        return this.agentRelations;
431
    }
432
    public TaxonNodeAgentRelation addAgentRelation(DefinedTerm type, TeamOrPersonBase<?> agent){
433
        TaxonNodeAgentRelation result = TaxonNodeAgentRelation.NewInstance(this, agent, type);
434
        return result;
435
    }
436
    public void addAgentRelation(TaxonNodeAgentRelation agentRelation) {
437
        agentRelation.setTaxonNode(this);
438
        this.agentRelations.add(agentRelation);
439
    }
440
    public void removeAgentRelation(TaxonNodeAgentRelation agentRelation) {
441
        agentRelation.setTaxonNode(this);
442
        agentRelations.remove(agentRelation);
443
    }
444

    
445
//********************
446

    
447
    //synonymToBeused
448
    public Synonym getSynonymToBeUsed() {
449
        return synonymToBeUsed;
450
    }
451
    public void setSynonymToBeUsed(Synonym synonymToBeUsed) {
452
        this.synonymToBeUsed = synonymToBeUsed;
453
    }
454

    
455
    //treeindex
456
    @Override
457
    public String treeIndex() {
458
        return treeIndex;
459
    }
460
    @Override
461
    @Deprecated //for CDM lib internal use only, may be removed in future versions
462
    public void setTreeIndex(String treeIndex) {
463
        this.treeIndex = treeIndex;
464
    }
465
    @Override
466
    public String treeIndexLike() {
467
        return treeIndex + "%";
468
    }
469
    @Override
470
    public String treeIndexWc() {
471
        return treeIndex + "*";
472
    }
473

    
474
//************************ METHODS **************************/
475

    
476
   @Override
477
    public TaxonNode addChildTaxon(Taxon taxon, Reference citation, String microCitation) {
478
        return addChildTaxon(taxon, this.childNodes.size(), citation, microCitation);
479
    }
480

    
481
   @Override
482
   public TaxonNode addChildTaxon(Taxon taxon, NamedSource source) {
483
       return addChildTaxon(taxon, this.childNodes.size(), source);
484
   }
485

    
486
    @Override
487
    public TaxonNode addChildTaxon(Taxon taxon, int index, Reference citation, String microCitation) {
488
        return addChildTaxon(taxon, index, NamedSource.NewPrimarySourceInstance(citation, microCitation));
489
    }
490

    
491
    @Override
492
    public TaxonNode addChildTaxon(Taxon taxon, int index, NamedSource source) {
493
        Classification classification = CdmBase.deproxy(this.getClassification());
494
        taxon = HibernateProxyHelper.deproxy(taxon, Taxon.class);
495
        if (classification.isTaxonInTree(taxon)){
496
            throw new IllegalArgumentException(String.format("Taxon may not be in a classification twice: %s", taxon.getTitleCache()));
497
       }
498
       return addChildNode(new TaxonNode(taxon), index, source);
499
    }
500

    
501
    /**
502
     * Moves a taxon node to a new parent. Descendents of the node are moved as well
503
     *
504
     * @param childNode the taxon node to be moved to the new parent
505
     * @return the child node in the state of having a new parent
506
     */
507
    @Override
508
    public TaxonNode addChildNode(TaxonNode childNode, Reference reference, String microReference){
509
        addChildNode(childNode, childNodes.size(), reference, microReference);
510
        return childNode;
511
    }
512

    
513
    /**
514
     * Inserts the given taxon node in the list of children of <i>this</i> taxon node
515
     * at the given (index + 1) position. If the given index is out of bounds
516
     * an exception will arise.<BR>
517
     * Due to bidirectionality this method must also assign <i>this</i> taxon node
518
     * as the parent of the given child.
519
     *
520
     * @param	child	the taxon node to be added
521
     * @param	index	the integer indicating the position at which the child
522
     * 					should be added
523
     * @see				#getChildNodes()
524
     * @see				#addChildNode(TaxonNode, Reference, String, Synonym)
525
     * @see				#deleteChildNode(TaxonNode)
526
     * @see				#deleteChildNode(int)
527
     */
528
    @Override
529
    public TaxonNode addChildNode(TaxonNode child, int index, Reference reference, String microReference){
530
        return addChildNode(child, index, NamedSource.NewPrimarySourceInstance(reference, microReference));
531
    }
532

    
533

    
534
    /**
535
     * Inserts the given taxon node in the list of children of <i>this</i> taxon node
536
     * at the given (index + 1) position. If the given index is out of bounds
537
     * an exception will arise.<BR>
538
     * Due to bidirectionality this method must also assign <i>this</i> taxon node
539
     * as the parent of the given child.
540
     *
541
     * @param   child   the taxon node to be added
542
     * @param   index   the integer indicating the position at which the child
543
     *                  should be added
544
     * @see             #getChildNodes()
545
     * @see             #addChildNode(TaxonNode, Reference, String, Synonym)
546
     * @see             #deleteChildNode(TaxonNode)
547
     * @see             #deleteChildNode(int)
548
     */
549
    @Override
550
    public TaxonNode addChildNode(TaxonNode child, int index, NamedSource source){
551
        if (index < 0 || index > childNodes.size() + 1){
552
            throw new IndexOutOfBoundsException("Wrong index: " + index);
553
        }
554
           // check if this node is a descendant of the childNode
555
        if(child.getParent() != this && child.isAncestor(this)){
556
            throw new IllegalStateException("New parent node is a descendant of the node to be moved.");
557
        }
558

    
559
        child.setParentTreeNode(this, index);
560

    
561
        child.setSource(source);
562

    
563
        return child;
564
    }
565

    
566
    /**
567
     * Sets this nodes classification. Updates classification of child nodes recursively.
568
     *
569
     * If the former and the actual tree are equal() this method does nothing.
570
     *
571
     * @throws IllegalArgumentException if newClassifciation is null
572
     *
573
     * @param newClassification
574
     */
575
    @Transient
576
    private void setClassificationRecursively(Classification newClassification) {
577
        if (newClassification == null){
578
        	throw new IllegalArgumentException("New Classification must not be 'null' when setting new classification.");
579
        }
580
    	if(! newClassification.equals(this.getClassification())){
581
            this.setClassification(newClassification);
582
            for(TaxonNode childNode : this.getChildNodes()){
583
                childNode.setClassificationRecursively(newClassification);
584
            }
585
        }
586
    }
587

    
588

    
589

    
590

    
591
    @Override
592
    public boolean deleteChildNode(TaxonNode node) {
593
        boolean result = removeChildNode(node);
594
        Taxon taxon = deproxy(node.getTaxon());
595
        node = deproxy(node);
596
        node.setTaxon(null);
597

    
598
        ArrayList<TaxonNode> childNodes = new ArrayList<>(node.getChildNodes());
599
        for(TaxonNode childNode : childNodes){
600
            HibernateProxyHelper.deproxy(childNode, TaxonNode.class);
601
            node.deleteChildNode(childNode);
602
        }
603
        taxon.removeTaxonNode(node);
604
        return result;
605
    }
606

    
607
    /**
608
     * Deletes the child node and also removes children of childnode
609
     * recursively if delete children is <code>true</code>
610
     * @param node
611
     * @param deleteChildren
612
     * @return
613
     */
614
    public boolean deleteChildNode(TaxonNode node, boolean deleteChildren) {
615
        boolean result = removeChildNode(node);
616
        Taxon taxon = node.getTaxon();
617
        node.setTaxon(null);
618
        taxon.removeTaxonNode(node);
619
        if (deleteChildren){
620
            ArrayList<TaxonNode> childNodes = new ArrayList<>(node.getChildNodes());
621
            for(TaxonNode childNode : childNodes){
622
                node.deleteChildNode(childNode, deleteChildren);
623
            }
624
        } else{
625
        	ArrayList<TaxonNode> childNodes = new ArrayList<>(node.getChildNodes());
626
            for(TaxonNode childNode : childNodes){
627
             this.addChildNode(childNode, null, null);
628
            }
629
        }
630

    
631
        return result;
632
    }
633

    
634
    /**
635
     * Removes the child node from this node. Sets the parent and the classification of the child
636
     * node to null
637
     *
638
     * @param childNode
639
     * @return
640
     */
641
    protected boolean removeChildNode(TaxonNode childNode){
642
        boolean result = true;
643
        //removeNullValueFromChildren();
644
        if(childNode == null){
645
            throw new IllegalArgumentException("TaxonNode may not be null");
646
        }
647
        int index = childNodes.indexOf(childNode);
648
        if (index >= 0){
649
            removeChild(index);
650
        } else {
651
            result = false;
652
        }
653
        return result;
654
    }
655

    
656
    /**
657
     * Removes the child node placed at the given (index + 1) position
658
     * from the list of {@link #getChildNodes() children} of <i>this</i> taxon node.
659
     * Sets the parent and the classification of the child
660
     * node to null.
661
     * If the given index is out of bounds no child will be removed.<BR>
662
     * NOTE: this is more for inner use. It does not remove the node from the taxon!!
663
     * Use deleteChildNode(TaxonNode) instead
664
     *
665
     * @param  index	the integer indicating the position of the taxon node to
666
     * 					be removed
667
     * @see     		#getChildNodes()
668
     * @see				#addChildNode(TaxonNode, Reference, String)
669
     * @see				#addChildNode(TaxonNode, int, Reference, String)
670
     * @see				#deleteChildNode(TaxonNode)
671
     */
672
    public void removeChild(int index){
673
        //TODO: Only as a workaround. We have to find out why merge creates null entries.
674

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

    
678
        if (child != null){
679

    
680
            TaxonNode parent = HibernateProxyHelper.deproxy(child.getParent());
681
            TaxonNode thisNode = HibernateProxyHelper.deproxy(this);
682
            if(parent != null && parent != thisNode){
683
                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());
684
            }else if (parent == null){
685
                throw new IllegalStateException("Parent of child is null in TaxonNode.removeChild(int). This should not happen.");
686
            }
687
            childNodes.remove(index);
688
            child.setClassification(null);
689

    
690
            this.countChildren = childNodes.size();
691
            child.setParent(null);
692
            child.setTreeIndex(null);
693
            child.setSortIndex(null);
694
        }
695
    }
696

    
697
    /**
698
     * Remove this taxonNode From its taxonomic parent
699
     *
700
     * @return true on success
701
     */
702
    public boolean delete(){
703
        if(isTopmostNode()){
704
            return classification.deleteChildNode(this);
705
        }else{
706
            return getParent().deleteChildNode(this);
707
        }
708
    }
709

    
710
    /**
711
     * Remove this taxonNode From its taxonomic parent
712
     *
713
     * @return true on success
714
     */
715
    public boolean delete(boolean deleteChildren){
716
        if(isTopmostNode()){
717
            return classification.deleteChildNode(this, deleteChildren);
718
        }else{
719
            return getParent().deleteChildNode(this, deleteChildren);
720
        }
721
    }
722

    
723
    @Override
724
    @Deprecated //for CDM lib internal use only, may be removed in future versions
725
    public int treeId() {
726
        if (this.classification == null){
727
        	logger.warn("TaxonNode has no classification. This should not happen.");  //#3840
728
        	return -1;
729
        }else{
730
        	return this.classification.getId();
731
        }
732
    }
733

    
734

    
735
    /**
736
     * Sets the parent of this taxon node to the given parent. Cleans up references to
737
     * old parents and sets the classification to the new parents classification
738
     */
739
    @Transient
740
    protected void setParentTreeNode(TaxonNode parent, int index){
741
        // remove ourselves from the old parent
742
        TaxonNode formerParent = this.getParent();
743
        formerParent = CdmBase.deproxy(formerParent);
744
        if (formerParent != null){
745
        	//special case, child already exists for same parent
746
            //FIXME document / check for correctness
747
            if (formerParent.equals(parent)){
748
                int currentIndex = formerParent.getChildNodes().indexOf(this);
749
                if (currentIndex != -1 && currentIndex < index){
750
                    index--;
751
                }
752
        	}
753

    
754
        	//remove from old parent
755
            formerParent.removeChildNode(this);
756
        }
757

    
758
        // set the new parent
759
        setParent(parent);
760

    
761
        // set the classification to the parents classification
762

    
763
        Classification classification = parent.getClassification();
764
        //FIXME also set the tree index here for performance reasons
765
        classification = CdmBase.deproxy(classification);
766
        setClassificationRecursively(classification);
767
        // add this node to the parent's child nodes
768
        parent = CdmBase.deproxy(parent);
769
        List<TaxonNode> parentChildren = parent.getChildNodes();
770

    
771
        if (index > parent.getChildNodes().size()){
772
            index = parent.getChildNodes().size();
773
        }
774
        if (parentChildren.contains(this)){
775
            //avoid duplicates
776
            if (parentChildren.indexOf(this) < index){
777
                index = index-1;
778
            }
779
            parentChildren.remove(this);
780
            parentChildren.add(index, this);
781
        }else{
782
            parentChildren.add(index, this);
783
        }
784

    
785
//        //only for debugging
786
//        if (this.getSortIndex() == null){
787
//            logger.warn("sortindex is null. This should not happen.");
788
//        }else if (! this.getSortIndex().equals(index)){
789
//        	logger.warn("index and sortindex are not equal: " +  this.getSortIndex() + ";" + index);
790
//        }
791

    
792
        // update the children count
793
        parent.setCountChildren(parent.getChildNodes().size());
794
    }
795

    
796
	/**
797
	 * As long as the sort index is not correctly handled through hibernate this is a workaround method
798
	 * to update the sort index manually
799
	 * @param parentChildren
800
	 * @param index
801
	 */
802
	private void updateSortIndex_old(int index) {
803
	    if (this.hasChildNodes()){
804
    	    List<TaxonNode> children = this.getChildNodes();
805
    	    HHH_9751_Util.removeAllNull(children);
806
    	    for(int i = index; i < children.size(); i++){
807
            	TaxonNode child = children.get(i);
808
            	if (child != null){
809
    //        		child = CdmBase.deproxy(child, TaxonNode.class);  //deproxy not needed as long as setSortIndex is protected or public #4200
810
            		child.setSortIndex(i);
811
            	}else{
812
            		String message = "A node in a taxon tree must never be null but is (ParentId: %d; sort index: %d; index: %d; i: %d)";
813
            		throw new IllegalStateException(String.format(message, getId(), sortIndex, index, i));
814
            	}
815
            }
816
	    }
817
	}
818

    
819
    /**
820
     * Returns a set containing this node and all nodes that are descendants of this node.
821
     */
822
	@Transient
823
    protected Set<TaxonNode> getDescendants(){
824
        Set<TaxonNode> nodeSet = new HashSet<>();
825

    
826
        nodeSet.add(this);
827

    
828
        for(TaxonNode childNode : getChildNodes()){
829
            nodeSet.addAll(childNode.getDescendants());
830
        }
831

    
832
        return nodeSet;
833
    }
834

    
835
    /**
836
     * Returns a set containing a clone of this node and of all nodes that are descendants of this node
837
     *
838
     * @return
839
     */
840
    protected TaxonNode cloneDescendants(){
841

    
842
        TaxonNode clone = this.clone();
843
        TaxonNode childClone;
844

    
845
        for(TaxonNode childNode : getChildNodes()){
846
            childClone = childNode.clone();
847
            for (TaxonNode childChild:childNode.getChildNodes()){
848
                childClone.addChildNode(childChild.cloneDescendants(), childChild.getReference(), childChild.getMicroReference());
849
            }
850
            clone.addChildNode(childClone, childNode.getReference(), childNode.getMicroReference());
851
            //childClone.addChildNode(childNode.cloneDescendants());
852
        }
853
        return clone;
854
    }
855

    
856
    /**
857
     * Returns all ancestor nodes of this node
858
     *
859
     * @return a set of all parent nodes
860
     */
861
    @Transient
862
    protected Set<TaxonNode> getAncestors(){
863
        Set<TaxonNode> nodeSet = new HashSet<>();
864
        if(this.getParent() != null){
865
        	TaxonNode parent =  CdmBase.deproxy(this.getParent());
866
        	nodeSet.add(parent);
867
            nodeSet.addAll(parent.getAncestors());
868
        }
869
        return nodeSet;
870
    }
871

    
872
    /**
873
     * Retrieves the first ancestor of the given rank. If any of the ancestors
874
     * has no taxon or has a rank > the given rank <code>null</code> is returned.
875
     * If <code>this</code> taxon is already of given rank this taxon is returned.
876
     * @param rank the rank the ancestor should have
877
     * @return the first found instance of a parent taxon node with the given rank
878
     */
879
    @Transient
880
    public TaxonNode getAncestorOfRank(Rank rank){
881
        Taxon taxon = CdmBase.deproxy(this.getTaxon());
882
        if (taxon == null){
883
            return null;
884
        }
885
        TaxonName name = CdmBase.deproxy(taxon.getName());
886
        if (name != null && name.getRank() != null){
887
            if (name.getRank().isHigher(rank)){
888
                return null;
889
            }
890
            if (name.getRank().equals(rank)){
891
                return this;
892
            }
893
        }
894

    
895
        if(this.getParent() != null){
896
        	TaxonNode parent =  CdmBase.deproxy(this.getParent());
897
            return parent.getAncestorOfRank(rank);
898
        }
899
		return null;
900
    }
901

    
902
    /**
903
     * Returns the ancestor taxa, starting with the highest (e.g. kingdom)
904
     * @return
905
     */
906
    @Transient
907
    public List<Taxon> getAncestorTaxaList(){
908
        List<Taxon> result = new ArrayList<>();
909
        TaxonNode current = this;
910
        while (current != null){
911
            if (current.getTaxon() != null){
912
                result.add(0, current.getTaxon());
913
            }
914
            current = current.getParent();
915
        }
916
        return result;
917
    }
918

    
919
    /**
920
     * Returns the ancestor taxon nodes, that do have a taxon attached
921
     * (excludes the root node) starting with the highest
922
     */
923
    @Transient
924
    public List<TaxonNode> getAncestorList(){
925
        List<TaxonNode> result = new ArrayList<>();
926
        TaxonNode current = this.getParent();
927
        while (current != null){
928
            if (current.getTaxon() != null){
929
                result.add(0, current);
930
            }
931
            current = current.getParent();
932
        }
933
        return result;
934
    }
935

    
936

    
937
    /**
938
     * Whether this TaxonNode is a direct child of the classification TreeNode
939
     */
940
    @Transient
941
    public boolean isTopmostNode(){
942
    	boolean parentCheck = false;
943
    	boolean classificationCheck = false;
944

    
945
    	if(getParent() != null) {
946
    		if(getParent().getTaxon() == null) {
947
    			parentCheck = true;
948
    		}
949
    	}
950

    
951
    	//TODO remove
952
    	// FIXME This should work but doesn't, due to missing sort indexes, can be removed after fixing #4200, #4098
953
    	if (classification != null){
954
    		classificationCheck = classification.getRootNode().getChildNodes().contains(this);
955
    	}else{
956
    		classificationCheck = false;
957
    	}
958

    
959
    	// The following is just for logging purposes for the missing sort indexes problem
960
    	// ticket #4098
961
    	if(parentCheck != classificationCheck) {
962
    		logger.warn("isTopmost node check " + parentCheck + " not same as classificationCheck : " + classificationCheck + " for taxon node ");
963
    		if(this.getParent() != null) {
964
    			logger.warn("-- with parent uuid " + this.getParent().getUuid().toString());
965
    			logger.warn("-- with parent id " + this.getParent().getId());
966
    			for(TaxonNode node : this.getParent().getChildNodes()) {
967
    				if(node == null) {
968
    					logger.warn("-- child node is null");
969
    				} else if (node.getTaxon() == null) {
970
    					logger.warn("-- child node taxon is null");
971
    				}
972
    			}
973
    			logger.warn("-- parent child count" + this.getParent().getChildNodes().size());
974
    		}
975
    	}
976

    
977
    	return parentCheck;
978
    }
979

    
980
    /**
981
     * Whether this TaxonNode is a descendant of (or equal to) the given TaxonNode
982
     *
983
     * @param possibleParent
984
     * @return <code>true</code> if <b>this</b> is a descendant
985
     */
986
    @Transient
987
    public boolean isDescendant(TaxonNode possibleParent){
988
    	if (possibleParent == null || this.treeIndex() == null
989
    	        || possibleParent.treeIndex() == null) {
990
    		return false;
991
    	}
992
    	return this.treeIndex().startsWith(possibleParent.treeIndex() );
993
    }
994

    
995
    /**
996
     * Whether this TaxonNode is an ascendant of (or equal to) the given TaxonNode.
997
     *
998
     * @param possibleChild
999
     * @return <code>true</code> if <b>this</b> is a ancestor of the given child parameter
1000
     */
1001
    @Transient
1002
    public boolean isAncestor(TaxonNode possibleChild){
1003
    	if (possibleChild == null || this.treeIndex() == null || possibleChild.treeIndex() == null) {
1004
    		return false;
1005
    	}
1006
       // return possibleChild == null ? false : possibleChild.getAncestors().contains(this);
1007
        return  possibleChild.treeIndex().startsWith(this.treeIndex());
1008
    }
1009

    
1010
    /**
1011
     * Whether this taxon has child nodes
1012
     *
1013
     * @return true if the taxonNode has childNodes
1014
     */
1015
    @Transient
1016
    @Override
1017
    public boolean hasChildNodes(){
1018
        return childNodes.size() > 0;
1019
    }
1020

    
1021
    public boolean hasTaxon() {
1022
        return (taxon!= null);
1023
    }
1024

    
1025
    @Transient
1026
    public Rank getNullSafeRank() {
1027
        return hasTaxon() ? getTaxon().getNullSafeRank() : null;
1028
    }
1029

    
1030
    @Transient
1031
    public TaxonName getNullSafeName() {
1032
        return getTaxon() == null? null: getTaxon().getName();
1033
    }
1034

    
1035
//    public void removeNullValueFromChildren(){
1036
//        try {
1037
//            //HHH_9751_Util.removeAllNull(childNodes);
1038
//            this.updateSortIndex(0);
1039
//        } catch (LazyInitializationException e) {
1040
//            logger.info("Cannot clean up uninitialized children without a session, skipping.");
1041
//        }
1042
//    }
1043

    
1044
    private boolean hasStatus(TaxonNodeStatus status) {
1045
        return CdmUtils.nullSafeEqual(this.status, status);
1046
    }
1047

    
1048
//*********************** CLONE ********************************************************/
1049
    /**
1050
     * Clones <i>this</i> taxon node. This is a shortcut that enables to create
1051
     * a new instance that differs only slightly from <i>this</i> taxon node by
1052
     * modifying only some of the attributes.<BR><BR>
1053
     * The child nodes are not copied.<BR>
1054
     * The taxon and parent are the same as for the original taxon node. <BR>
1055
     * <BR>
1056
     * Note: Cloning taxon nodes with cloning taxa (and children) is a complex
1057
     * issue which is better be handled in service layer logic. See according
1058
     * clone method in classification service
1059
     * or taxon node service there.
1060
     *
1061
     * @see java.lang.Object#clone()
1062
     */
1063
    @Override
1064
    public TaxonNode clone()  {
1065

    
1066
        try{
1067
            TaxonNode result = (TaxonNode)super.clone();
1068
            result.getTaxon().addTaxonNode(result);
1069

    
1070
            //childNodes
1071
            result.childNodes = new ArrayList<>();
1072
            result.countChildren = 0;
1073

    
1074
            //agents
1075
            result.agentRelations = new HashSet<>();
1076
            for (TaxonNodeAgentRelation rel : this.agentRelations){
1077
                result.addAgentRelation(rel.clone());
1078
            }
1079

    
1080
            //statusNote
1081
            result.statusNote = new HashMap<>();
1082
            for(Language lang : this.statusNote.keySet()){
1083
                result.statusNote.put(lang, this.statusNote.get(lang));
1084
            }
1085

    
1086
            return result;
1087
        }catch (CloneNotSupportedException e) {
1088
            logger.warn("Object does not implement cloneable");
1089
            e.printStackTrace();
1090
            return null;
1091
        }
1092
    }
1093

    
1094
//To be removed once SingleSourcedEntity attributes have been renamed to reference and microReference
1095

    
1096
    @Override
1097
    @Transient
1098
    public Reference getReference() {
1099
        return getCitation();
1100
    }
1101

    
1102
    @Override
1103
    @Transient
1104
    public String getMicroReference() {
1105
        return getCitationMicroReference();
1106
    }
1107

    
1108
}
(11-11/20)