Project

General

Profile

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

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

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

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

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

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

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

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

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

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

    
149
    //see https://dev.e-taxonomy.eu/redmine/issues/3722
150
    //see https://dev.e-taxonomy.eu/redmine/issues/4200
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
	}
226
    /**
227
     * SortIndex shall be handled only internally, therefore not public.
228
     * However, as javaassist only supports protected methods it needs to be protected, not private.
229
     * Alternatively we could use deproxy on every call of this method (see commented code)
230
     * @param i
231
     * @return
232
     * @deprecated for internal use only
233
     */
234
     @Deprecated
235
    protected void setSortIndex(Integer i) {
236
//    	 CdmBase.deproxy(this, TaxonNode.class).sortIndex = i;  //alternative solution for private, DON'T remove
237
    	 sortIndex = i;
238
	}
239

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

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

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

    
272
    public boolean isUnplaced() {return hasStatus(TaxonNodeStatus.UNPLACED);}
273

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

    
277
    public boolean isExcluded() {return hasStatus(TaxonNodeStatus.EXCLUDED);}
278

    
279
//************************* SOURCE *********************/
280

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

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

    
310

    
311

    
312
//************************************************************/
313

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

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

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

    
354
// *************** Status Note ***************
355

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

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

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

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

    
425
// ****************** Agent Relations ****************************/
426

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

    
443
//********************
444

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

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

    
472
//************************ METHODS **************************/
473

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

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

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

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

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

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

    
531

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

    
557
        child.setParentTreeNode(this, index);
558

    
559
        child.setSource(source);
560

    
561

    
562
        return child;
563
    }
564

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

    
587

    
588

    
589

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

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

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

    
630
        return result;
631
    }
632

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

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

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

    
677
        if (child != null){
678

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

    
689
            //update sortindex
690
            //TODO workaround (see sortIndex doc)
691
            this.countChildren = childNodes.size();
692
            child.setParent(null);
693
            child.setTreeIndex(null);
694
            updateSortIndex(index);
695
            child.setSortIndex(null);
696
        }
697
    }
698

    
699

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

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

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

    
737

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

    
759
        	//remove from old parent
760
            formerParent.removeChildNode(this);
761
        }
762

    
763
        // set the new parent
764
        setParent(parent);
765

    
766
        // set the classification to the parents classification
767

    
768
        Classification classification = parent.getClassification();
769
        //FIXME also set the tree index here for performance reasons
770
        classification = CdmBase.deproxy(classification);
771
        setClassificationRecursively(classification);
772
        // add this node to the parent's child nodes
773
        parent = CdmBase.deproxy(parent);
774
        List<TaxonNode> parentChildren = parent.getChildNodes();
775
       //TODO: Only as a workaround. We have to find out why merge creates null entries.
776

    
777
//        HHH_9751_Util.removeAllNull(parentChildren);
778
//        parent.updateSortIndex(0);
779
        if (index > parent.getChildNodes().size()){
780
            index = parent.getChildNodes().size();
781
        }
782
        if (parentChildren.contains(this)){
783
            //avoid duplicates
784
            if (parentChildren.indexOf(this) < index){
785
                index = index-1;
786
            }
787
            parentChildren.remove(this);
788
            parentChildren.add(index, this);
789
        }else{
790
            parentChildren.add(index, this);
791
        }
792

    
793

    
794
        //sortIndex
795
        //TODO workaround (see sortIndex doc)
796
       // this.getParent().removeNullValueFromChildren();
797
        this.getParent().updateSortIndex(index);
798
        //only for debugging
799
        if (this.getSortIndex() == null){
800
            logger.warn("sortindex is null. This should not happen.");
801
        }else if (! this.getSortIndex().equals(index)){
802
        	logger.warn("index and sortindex are not equal: " +  this.getSortIndex() + ";" + index);
803
        }
804

    
805
        // update the children count
806
        parent.setCountChildren(parent.getChildNodes().size());
807
    }
808

    
809
	/**
810
	 * As long as the sort index is not correctly handled through hibernate this is a workaround method
811
	 * to update the sort index manually
812
	 * @param parentChildren
813
	 * @param index
814
	 */
815
	private void updateSortIndex(int index) {
816
	    if (this.hasChildNodes()){
817
    	    List<TaxonNode> children = this.getChildNodes();
818
    	    HHH_9751_Util.removeAllNull(children);
819
    	    for(int i = index; i < children.size(); i++){
820
            	TaxonNode child = children.get(i);
821
            	if (child != null){
822
    //        		child = CdmBase.deproxy(child, TaxonNode.class);  //deproxy not needed as long as setSortIndex is protected or public #4200
823
            		child.setSortIndex(i);
824
            	}else{
825
            		String message = "A node in a taxon tree must never be null but is (ParentId: %d; sort index: %d; index: %d; i: %d)";
826
            		throw new IllegalStateException(String.format(message, getId(), sortIndex, index, i));
827
            	}
828
            }
829
	    }
830
	}
831

    
832
    /**
833
     * Returns a set containing this node and all nodes that are descendants of this node.
834
     */
835
	@Transient
836
    protected Set<TaxonNode> getDescendants(){
837
        Set<TaxonNode> nodeSet = new HashSet<>();
838

    
839
        nodeSet.add(this);
840

    
841
        for(TaxonNode childNode : getChildNodes()){
842
            nodeSet.addAll(childNode.getDescendants());
843
        }
844

    
845
        return nodeSet;
846
    }
847

    
848
    /**
849
     * Returns a set containing a clone of this node and of all nodes that are descendants of this node
850
     *
851
     * @return
852
     */
853
    protected TaxonNode cloneDescendants(){
854

    
855
        TaxonNode clone = this.clone();
856
        TaxonNode childClone;
857

    
858
        for(TaxonNode childNode : getChildNodes()){
859
            childClone = childNode.clone();
860
            for (TaxonNode childChild:childNode.getChildNodes()){
861
                childClone.addChildNode(childChild.cloneDescendants(), childChild.getReference(), childChild.getMicroReference());
862
            }
863
            clone.addChildNode(childClone, childNode.getReference(), childNode.getMicroReference());
864
            //childClone.addChildNode(childNode.cloneDescendants());
865
        }
866
        return clone;
867
    }
868

    
869
    /**
870
     * Returns all ancestor nodes of this node
871
     *
872
     * @return a set of all parent nodes
873
     */
874
    @Transient
875
    protected Set<TaxonNode> getAncestors(){
876
        Set<TaxonNode> nodeSet = new HashSet<>();
877
        if(this.getParent() != null){
878
        	TaxonNode parent =  CdmBase.deproxy(this.getParent());
879
        	nodeSet.add(parent);
880
            nodeSet.addAll(parent.getAncestors());
881
        }
882
        return nodeSet;
883
    }
884

    
885
    /**
886
     * Retrieves the first ancestor of the given rank. If any of the ancestors
887
     * has no taxon or has a rank > the given rank <code>null</code> is returned.
888
     * If <code>this</code> taxon is already of given rank this taxon is returned.
889
     * @param rank the rank the ancestor should have
890
     * @return the first found instance of a parent taxon node with the given rank
891
     */
892
    @Transient
893
    public TaxonNode getAncestorOfRank(Rank rank){
894
        Taxon taxon = CdmBase.deproxy(this.getTaxon());
895
        if (taxon == null){
896
            return null;
897
        }
898
        TaxonName name = CdmBase.deproxy(taxon.getName());
899
        if (name != null && name.getRank() != null){
900
            if (name.getRank().isHigher(rank)){
901
                return null;
902
            }
903
            if (name.getRank().equals(rank)){
904
                return this;
905
            }
906
        }
907

    
908
        if(this.getParent() != null){
909
        	TaxonNode parent =  CdmBase.deproxy(this.getParent());
910
            return parent.getAncestorOfRank(rank);
911
        }
912
		return null;
913
    }
914

    
915
    /**
916
     * Returns the ancestor taxa, starting with the highest (e.g. kingdom)
917
     * @return
918
     */
919
    @Transient
920
    public List<Taxon> getAncestorTaxaList(){
921
        List<Taxon> result = new ArrayList<>();
922
        TaxonNode current = this;
923
        while (current != null){
924
            if (current.getTaxon() != null){
925
                result.add(0, current.getTaxon());
926
            }
927
            current = current.getParent();
928
        }
929
        return result;
930
    }
931

    
932
    /**
933
     * Returns the ancestor taxon nodes, that do have a taxon attached
934
     * (excludes the root node) starting with the highest
935
     */
936
    @Transient
937
    public List<TaxonNode> getAncestorList(){
938
        List<TaxonNode> result = new ArrayList<>();
939
        TaxonNode current = this.getParent();
940
        while (current != null){
941
            if (current.getTaxon() != null){
942
                result.add(0, current);
943
            }
944
            current = current.getParent();
945
        }
946
        return result;
947
    }
948

    
949

    
950
    /**
951
     * Whether this TaxonNode is a direct child of the classification TreeNode
952
     */
953
    @Transient
954
    public boolean isTopmostNode(){
955
    	boolean parentCheck = false;
956
    	boolean classificationCheck = false;
957

    
958
    	if(getParent() != null) {
959
    		if(getParent().getTaxon() == null) {
960
    			parentCheck = true;
961
    		}
962
    	}
963

    
964
    	//TODO remove
965
    	// FIXME This should work but doesn't, due to missing sort indexes, can be removed after fixing #4200, #4098
966
    	if (classification != null){
967
    		classificationCheck = classification.getRootNode().getChildNodes().contains(this);
968
    	}else{
969
    		classificationCheck = false;
970
    	}
971

    
972
    	// The following is just for logging purposes for the missing sort indexes problem
973
    	// ticket #4098
974
    	if(parentCheck != classificationCheck) {
975
    		logger.warn("isTopmost node check " + parentCheck + " not same as classificationCheck : " + classificationCheck + " for taxon node ");
976
    		if(this.getParent() != null) {
977
    			logger.warn("-- with parent uuid " + this.getParent().getUuid().toString());
978
    			logger.warn("-- with parent id " + this.getParent().getId());
979
    			for(TaxonNode node : this.getParent().getChildNodes()) {
980
    				if(node == null) {
981
    					logger.warn("-- child node is null");
982
    				} else if (node.getTaxon() == null) {
983
    					logger.warn("-- child node taxon is null");
984
    				}
985
    			}
986
    			logger.warn("-- parent child count" + this.getParent().getChildNodes().size());
987
    		}
988
    	}
989

    
990
    	return parentCheck;
991
    }
992

    
993
    /**
994
     * Whether this TaxonNode is a descendant of (or equal to) the given TaxonNode
995
     *
996
     * @param possibleParent
997
     * @return <code>true</code> if <b>this</b> is a descendant
998
     */
999
    @Transient
1000
    public boolean isDescendant(TaxonNode possibleParent){
1001
    	if (possibleParent == null || this.treeIndex() == null
1002
    	        || possibleParent.treeIndex() == null) {
1003
    		return false;
1004
    	}
1005
    	return this.treeIndex().startsWith(possibleParent.treeIndex() );
1006
    }
1007

    
1008
    /**
1009
     * Whether this TaxonNode is an ascendant of (or equal to) the given TaxonNode.
1010
     *
1011
     * @param possibleChild
1012
     * @return <code>true</code> if <b>this</b> is a ancestor of the given child parameter
1013
     */
1014
    @Transient
1015
    public boolean isAncestor(TaxonNode possibleChild){
1016
    	if (possibleChild == null || this.treeIndex() == null || possibleChild.treeIndex() == null) {
1017
    		return false;
1018
    	}
1019
       // return possibleChild == null ? false : possibleChild.getAncestors().contains(this);
1020
        return  possibleChild.treeIndex().startsWith(this.treeIndex());
1021
    }
1022

    
1023
    /**
1024
     * Whether this taxon has child nodes
1025
     *
1026
     * @return true if the taxonNode has childNodes
1027
     */
1028
    @Transient
1029
    @Override
1030
    public boolean hasChildNodes(){
1031
        return childNodes.size() > 0;
1032
    }
1033

    
1034
    public boolean hasTaxon() {
1035
        return (taxon!= null);
1036
    }
1037

    
1038
    @Transient
1039
    public Rank getNullSafeRank() {
1040
        return hasTaxon() ? getTaxon().getNullSafeRank() : null;
1041
    }
1042

    
1043
    @Transient
1044
    public TaxonName getNullSafeName() {
1045
        return getTaxon() == null? null: getTaxon().getName();
1046
    }
1047

    
1048
    public void removeNullValueFromChildren(){
1049
        try {
1050
            //HHH_9751_Util.removeAllNull(childNodes);
1051
            this.updateSortIndex(0);
1052
        } catch (LazyInitializationException e) {
1053
            logger.info("Cannot clean up uninitialized children without a session, skipping.");
1054
        }
1055
    }
1056

    
1057
    private boolean hasStatus(TaxonNodeStatus status) {
1058
        return CdmUtils.nullSafeEqual(this.status, status);
1059
    }
1060

    
1061
//*********************** CLONE ********************************************************/
1062
    /**
1063
     * Clones <i>this</i> taxon node. This is a shortcut that enables to create
1064
     * a new instance that differs only slightly from <i>this</i> taxon node by
1065
     * modifying only some of the attributes.<BR><BR>
1066
     * The child nodes are not copied.<BR>
1067
     * The taxon and parent are the same as for the original taxon node. <BR>
1068
     * <BR>
1069
     * Note: Cloning taxon nodes with cloning taxa (and children) is a complex
1070
     * issue which is better be handled in service layer logic. See according
1071
     * clone method in classification service
1072
     * or taxon node service there.
1073
     *
1074
     * @see java.lang.Object#clone()
1075
     */
1076
    @Override
1077
    public TaxonNode clone()  {
1078

    
1079
        try{
1080
            TaxonNode result = (TaxonNode)super.clone();
1081
            result.getTaxon().addTaxonNode(result);
1082

    
1083
            //childNodes
1084
            result.childNodes = new ArrayList<>();
1085
            result.countChildren = 0;
1086

    
1087
            //agents
1088
            result.agentRelations = new HashSet<>();
1089
            for (TaxonNodeAgentRelation rel : this.agentRelations){
1090
                result.addAgentRelation(rel.clone());
1091
            }
1092

    
1093
            //statusNote
1094
            result.statusNote = new HashMap<>();
1095
            for(Language lang : this.statusNote.keySet()){
1096
                result.statusNote.put(lang, this.statusNote.get(lang));
1097
            }
1098

    
1099
            return result;
1100
        }catch (CloneNotSupportedException e) {
1101
            logger.warn("Object does not implement cloneable");
1102
            e.printStackTrace();
1103
            return null;
1104
        }
1105
    }
1106

    
1107
//To be removed once SingleSourcedEntity attributes have been renamed to reference and microReference
1108

    
1109
    @Override
1110
    @Transient
1111
    public Reference getReference() {
1112
        return getCitation();
1113
    }
1114

    
1115
    @Override
1116
    @Transient
1117
    public String getMicroReference() {
1118
        return getCitationMicroReference();
1119
    }
1120

    
1121
}
(12-12/21)