Project

General

Profile

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

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

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

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

    
113
    private static final long serialVersionUID = -4743289894926587693L;
114
    private static final Logger logger = Logger.getLogger(TaxonNode.class);
115

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

    
124
    @XmlElement(name = "parent")
125
    @XmlIDREF
126
    @XmlSchemaType(name = "IDREF")
127
    @ManyToOne(fetch = FetchType.LAZY)
128
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
129
    private TaxonNode parent;
130

    
131
    @XmlElement(name = "treeIndex")
132
    @Column(length=255)
133
    @Field(store = Store.YES, index = Index.YES, analyze = Analyze.NO)
134
    private String treeIndex;
135

    
136
    @XmlElement(name = "classification")
137
    @XmlIDREF
138
    @XmlSchemaType(name = "IDREF")
139
    @ManyToOne(fetch = FetchType.LAZY)
140
    @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
141
//	TODO @NotNull // avoids creating a UNIQUE key for this field
142
    @IndexedEmbedded(includeEmbeddedObjectId=true)
143
    private Classification classification;
144

    
145
    @XmlElementWrapper(name = "childNodes")
146
    @XmlElement(name = "childNode")
147
    @XmlIDREF
148
    @XmlSchemaType(name = "IDREF")
149
    //see https://dev.e-taxonomy.eu/trac/ticket/3722
150
    @OrderColumn(name="sortIndex")
151
    @OrderBy("sortIndex")
152
    @OneToMany(mappedBy="parent", fetch=FetchType.LAZY)
153
    //@Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
154
    private List<TaxonNode> childNodes = new ArrayList<>();
155

    
156
    //see https://dev.e-taxonomy.eu/trac/ticket/3722
157
    //see https://dev.e-taxonomy.eu/trac/ticket/4200
158
    private Integer sortIndex = -1;
159

    
160
    //the source for this placement
161
    @XmlElement(name = "source")
162
    @XmlIDREF
163
    @XmlSchemaType(name = "IDREF")
164
    @OneToOne(fetch = FetchType.LAZY, orphanRemoval=true)
165
    @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE, CascadeType.DELETE, CascadeType.PERSIST})
166
    private DescriptionElementSource source;
167

    
168
//    @XmlElement(name = "reference")
169
//    @XmlIDREF
170
//    @XmlSchemaType(name = "IDREF")
171
    @XmlTransient
172
    @ManyToOne(fetch = FetchType.LAZY)
173
    @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
174
    private Reference referenceForParentChildRelation;
175

    
176
//    @XmlElement(name = "microReference")
177
    @XmlTransient
178
    private String microReferenceForParentChildRelation;
179

    
180
    @XmlElement(name = "countChildren")
181
    private int countChildren;
182

    
183
    @XmlElementWrapper(name = "agentRelations")
184
    @XmlElement(name = "agentRelation")
185
    @XmlIDREF
186
    @XmlSchemaType(name = "IDREF")
187
    @OneToMany(mappedBy="taxonNode", fetch=FetchType.LAZY)
188
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
189
    private Set<TaxonNodeAgentRelation> agentRelations = new HashSet<>();
190

    
191
    /**
192
     * The {@link TaxonNodeStatus status} of this taxon node.
193
     */
194
    @XmlAttribute(name ="TaxonNodeStatus")
195
    @Column(name="status", length=10)
196
    @Type(type = "eu.etaxonomy.cdm.hibernate.EnumUserType",
197
        parameters = {@Parameter(name  = "enumClass", value = "eu.etaxonomy.cdm.model.taxon.TaxonNodeStatus")}
198
    )
199
    @Audited
200
    private TaxonNodeStatus status;
201

    
202
    @XmlElement(name = "statusNote")
203
    @XmlJavaTypeAdapter(MultilanguageTextAdapter.class)
204
    @OneToMany(fetch = FetchType.LAZY, orphanRemoval=true)
205
//    @MapKeyJoinColumn(name="statusNote_mapkey_id")
206
    @JoinTable(name = "TaxonNode_StatusNote")  //to make possible to add also unplacedNote
207
    @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE, CascadeType.DELETE})
208
    private Map<Language,LanguageString> statusNote = new HashMap<>();
209

    
210
//	private Taxon originalConcept;
211
//	//or
212
    @XmlElement(name = "synonymToBeUsed")
213
    @XmlIDREF
214
    @XmlSchemaType(name = "IDREF")
215
    @ManyToOne(fetch = FetchType.LAZY)
216
    @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
217
    private Synonym synonymToBeUsed;
218

    
219
// ******************** CONSTRUCTOR **********************************************/
220

    
221
    protected TaxonNode(){super();}
222

    
223
    /**
224
     * to create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}
225
     * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}
226
     * @param taxon
227
     * @param classification
228
     * @deprecated setting of classification is handled in the addTaxonNode() method,
229
     * use TaxonNode(taxon) instead
230
     */
231
    @Deprecated
232
    protected TaxonNode (Taxon taxon, Classification classification){
233
        this(taxon);
234
        setClassification(classification);
235
    }
236

    
237
    /**
238
     * To create nodes either use {@link Classification#addChildTaxon(Taxon, Reference, String, Synonym)}
239
     * or {@link TaxonNode#addChildTaxon(Taxon, Reference, String, Synonym)}
240
     *
241
     * @param taxon
242
     */
243
    protected TaxonNode(Taxon taxon){
244
        setTaxon(taxon);
245
    }
246

    
247
// ************************* GETTER / SETTER *******************************/
248

    
249
    @Transient
250
    public Integer getSortIndex() {
251
        return sortIndex;
252
	}
253
    /**
254
     * SortIndex shall be handled only internally, therefore not public.
255
     * However, as javaassist only supports protected methods it needs to be protected, not private.
256
     * Alternatively we could use deproxy on every call of this method (see commented code)
257
     * @param i
258
     * @return
259
     * @deprecated for internal use only
260
     */
261
     @Deprecated
262
    protected void setSortIndex(Integer i) {
263
//    	 CdmBase.deproxy(this, TaxonNode.class).sortIndex = i;  //alternative solution for private, DON'T remove
264
    	 sortIndex = i;
265
	}
266

    
267
    public Taxon getTaxon() {
268
        return taxon;
269
    }
270
    public void setTaxon(Taxon taxon) {
271
        this.taxon = taxon;
272
        if (taxon != null){
273
            taxon.addTaxonNode(this);
274
        }
275
    }
276

    
277
    @Override
278
    public List<TaxonNode> getChildNodes() {
279
        return childNodes;
280
    }
281
	protected void setChildNodes(List<TaxonNode> childNodes) {
282
		this.childNodes = childNodes;
283
	}
284

    
285
    public Classification getClassification() {
286
        return classification;
287
    }
288
    /**
289
     * THIS METHOD SHOULD NOT BE CALLED!
290
     * invisible part of the bidirectional relationship, for public use Classification.addRoot() or TaxonNode.addChild()
291
     * @param classification
292
     * @deprecated for internal use only
293
     */
294
    @Deprecated
295
    protected void setClassification(Classification classification) {
296
        this.classification = classification;
297
    }
298

    
299
    public boolean isUnplaced() {return hasStatus(TaxonNodeStatus.UNPLACED);}
300

    
301
    //#8281 indicates a preliminary placement
302
    public boolean isDoubtful() {return hasStatus(TaxonNodeStatus.DOUBTFUL);}
303

    
304
    public boolean isExcluded() {return hasStatus(TaxonNodeStatus.EXCLUDED);}
305

    
306
//************************* SOURCE *********************/
307

    
308
    @Override
309
    @Transient
310
    public String getMicroReference() {
311
        return source == null ? null : this.source.getCitationMicroReference();
312
    }
313
    public void setMicroReference(String microReference) {
314
        this.getSource(true).setCitationMicroReference(StringUtils.isBlank(microReference)? null : microReference);
315
        checkNullSource();
316
    }
317

    
318
    @Override
319
    @Transient
320
    public Reference getReference() {
321
        return (this.source == null) ? null : source.getCitation();
322
    }
323
    public void setReference(Reference reference) {
324
        getSource(true).setCitation(reference);
325
        checkNullSource();
326
    }
327

    
328
    public DescriptionElementSource getSource() {
329
        return source;
330
    }
331
    public void setSource(DescriptionElementSource source) {
332
        this.source = source;
333
    }
334

    
335
    private void checkNullSource() {
336
        if (this.source != null && this.source.checkEmpty(true)){
337
            this.source = null;
338
        }
339
    }
340

    
341
    private DescriptionElementSource getSource(boolean createIfNotExist){
342
        if (this.source == null && createIfNotExist){
343
            this.source = DescriptionElementSource.NewInstance(OriginalSourceType.PrimaryTaxonomicSource);
344
        }
345
        return source;
346
    }
347

    
348
//************************************************************/
349

    
350
    //countChildren
351
    public int getCountChildren() {
352
        return countChildren;
353
    }
354
    /**
355
     * @deprecated for internal use only
356
     * @param countChildren
357
     */
358
    @Deprecated
359
    protected void setCountChildren(int countChildren) {
360
        this.countChildren = countChildren;
361
    }
362

    
363
    //parent
364
    @Override
365
    public TaxonNode getParent(){
366
        return parent;
367
    }
368
    /**
369
     * Sets the parent of this taxon node.<BR>
370
     *
371
     * In most cases you would want to call setParentTreeNode(ITreeNode) which
372
     * handles updating of the bidirectional relationship
373
     *
374
     * @see setParentTreeNode(ITreeNode)
375
     * @param parent
376
     *
377
     */
378
    protected void setParent(TaxonNode parent) {
379
        this.parent = parent;
380
//        this.treeIndex = parent.treeIndex() +
381
    }
382

    
383
    public TaxonNodeStatus getStatus() {
384
        return status;
385
    }
386
    public void setStatus(TaxonNodeStatus status) {
387
        this.status = status;
388
    }
389

    
390
// *************** Status Note ***************
391

    
392
    /**
393
     * Returns the {@link MultilanguageText multi-language text} to add a note to the
394
     * status. The different {@link LanguageString language strings}
395
     * contained in the multi-language text should all have the same meaning.
396
     * @see #getStatusNote(Language)
397
     * @see #putStatusNote(Language, String)
398
     */
399
    public Map<Language,LanguageString> getStatusNote(){
400
        return this.statusNote;
401
    }
402

    
403
    /**
404
     * Returns the status note string in the given {@link Language language}
405
     *
406
     * @param language  the language in which the description string looked for is formulated
407
     * @see             #getStatusNote()
408
     * @see             #putStatusNote(Language, String)
409
     */
410
    public String getStatusNote(Language language){
411
        LanguageString languageString = statusNote.get(language);
412
        if (languageString == null){
413
            return null;
414
        }else{
415
            return languageString.getText();
416
        }
417
    }
418

    
419
    /**
420
     * Adds a translated {@link LanguageString text in a particular language}
421
     * to the {@link MultilanguageText multilanguage text} used to add a note to
422
     * the {@link #getStatus() status}.
423
     *
424
     * @param statusNote   the language string adding a note to the status
425
     *                      in a particular language
426
     * @see                 #getStatusNote()
427
     * @see                 #putStatusNote(String, Language)
428
     */
429
    public void putStatusNote(LanguageString statusNote){
430
        this.statusNote.put(statusNote.getLanguage(), statusNote);
431
    }
432
    /**
433
     * Creates a {@link LanguageString language string} based on the given text string
434
     * and the given {@link Language language} and adds it to the {@link MultilanguageText
435
     * multi-language text} used to annotate the status.
436
     *
437
     * @param text      the string annotating the status
438
     *                  in a particular language
439
     * @param language  the language in which the text string is formulated
440
     * @see             #getStatusNote()
441
     * @see             #putStatusNote(LanguageString)
442
     * @see             #removeStatusNote(Language)
443
     */
444
    public void putStatusNote(Language language, String text){
445
        this.statusNote.put(language, LanguageString.NewInstance(text, language));
446
    }
447

    
448
    /**
449
     * Removes from the {@link MultilanguageText multilanguage text} used to annotate
450
     * the status the one {@link LanguageString language string}
451
     * with the given {@link Language language}.
452
     *
453
     * @param  lang the language in which the language string to be removed
454
     *       has been formulated
455
     * @see         #getStatusNote()
456
     */
457
    public void removeStatusNote(Language lang){
458
        this.statusNote.remove(lang);
459
    }
460

    
461
// ****************** Agent Relations ****************************/
462

    
463
    public Set<TaxonNodeAgentRelation> getAgentRelations() {
464
        return this.agentRelations;
465
    }
466
    public TaxonNodeAgentRelation addAgentRelation(DefinedTerm type, TeamOrPersonBase<?> agent){
467
        TaxonNodeAgentRelation result = TaxonNodeAgentRelation.NewInstance(this, agent, type);
468
        return result;
469
    }
470
    public void addAgentRelation(TaxonNodeAgentRelation agentRelation) {
471
        agentRelation.setTaxonNode(this);
472
        this.agentRelations.add(agentRelation);
473
    }
474
    public void removeAgentRelation(TaxonNodeAgentRelation agentRelation) {
475
        agentRelation.setTaxonNode(this);
476
        agentRelations.remove(agentRelation);
477
    }
478

    
479
//********************
480

    
481
    //synonymToBeused
482
    public Synonym getSynonymToBeUsed() {
483
        return synonymToBeUsed;
484
    }
485
    public void setSynonymToBeUsed(Synonym synonymToBeUsed) {
486
        this.synonymToBeUsed = synonymToBeUsed;
487
    }
488

    
489
    //treeindex
490
    @Override
491
    public String treeIndex() {
492
        return treeIndex;
493
    }
494
    @Override
495
    @Deprecated //for CDM lib internal use only, may be removed in future versions
496
    public void setTreeIndex(String treeIndex) {
497
        this.treeIndex = treeIndex;
498
    }
499
    @Override
500
    public String treeIndexLike() {
501
        return treeIndex + "%";
502
    }
503
    @Override
504
    public String treeIndexWc() {
505
        return treeIndex + "*";
506
    }
507

    
508
//************************ METHODS **************************/
509

    
510
   @Override
511
    public TaxonNode addChildTaxon(Taxon taxon, Reference citation, String microCitation) {
512
        return addChildTaxon(taxon, this.childNodes.size(), citation, microCitation);
513
    }
514

    
515
    @Override
516
    public TaxonNode addChildTaxon(Taxon taxon, int index, Reference citation, String microCitation) {
517
        Classification classification = CdmBase.deproxy(this.getClassification());
518
        taxon = HibernateProxyHelper.deproxy(taxon, Taxon.class);
519
        if (classification.isTaxonInTree(taxon)){
520
            throw new IllegalArgumentException(String.format("Taxon may not be in a classification twice: %s", taxon.getTitleCache()));
521
       }
522
       return addChildNode(new TaxonNode(taxon), index, citation, microCitation);
523
    }
524

    
525
    /**
526
     * Moves a taxon node to a new parent. Descendents of the node are moved as well
527
     *
528
     * @param childNode the taxon node to be moved to the new parent
529
     * @return the child node in the state of having a new parent
530
     */
531
    @Override
532
    public TaxonNode addChildNode(TaxonNode childNode, Reference reference, String microReference){
533
        addChildNode(childNode, childNodes.size(), reference, microReference);
534
        return childNode;
535
    }
536

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

    
562
        child.setParentTreeNode(this, index);
563

    
564
        child.setReference(reference);
565
        child.setMicroReference(microReference);
566

    
567
        return child;
568
    }
569

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

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

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

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

    
632
        return result;
633
    }
634

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

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

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

    
679
        if (child != null){
680

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

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

    
701

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

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

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

    
739

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

    
761
        	//remove from old parent
762
            formerParent.removeChildNode(this);
763
        }
764

    
765
        // set the new parent
766
        setParent(parent);
767

    
768
        // set the classification to the parents classification
769

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

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

    
795

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

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

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

    
834

    
835
    /**
836
     * Returns a set containing this node and all nodes that are descendants of this node
837
     *
838
     * @return
839
     */
840
	@Transient
841
    protected Set<TaxonNode> getDescendants(){
842
        Set<TaxonNode> nodeSet = new HashSet<>();
843

    
844
        nodeSet.add(this);
845

    
846
        for(TaxonNode childNode : getChildNodes()){
847
            nodeSet.addAll(childNode.getDescendants());
848
        }
849

    
850
        return nodeSet;
851
    }
852

    
853
    /**
854
     * Returns a set containing a clone of this node and of all nodes that are descendants of this node
855
     *
856
     * @return
857
     */
858
    protected TaxonNode cloneDescendants(){
859

    
860
        TaxonNode clone = (TaxonNode)this.clone();
861
        TaxonNode childClone;
862

    
863
        for(TaxonNode childNode : getChildNodes()){
864
            childClone = (TaxonNode) childNode.clone();
865
            for (TaxonNode childChild:childNode.getChildNodes()){
866
                childClone.addChildNode(childChild.cloneDescendants(), childChild.getReference(), childChild.getMicroReference());
867
            }
868
            clone.addChildNode(childClone, childNode.getReference(), childNode.getMicroReference());
869
            //childClone.addChildNode(childNode.cloneDescendants());
870
        }
871
        return clone;
872
    }
873

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

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

    
913
        if(this.getParent() != null){
914
        	TaxonNode parent =  CdmBase.deproxy(this.getParent());
915
            return parent.getAncestorOfRank(rank);
916
        }
917
		return null;
918
    }
919

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

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

    
956

    
957
    /**
958
     * Whether this TaxonNode is a direct child of the classification TreeNode
959
     * @return
960
     */
961
    @Transient
962
    public boolean isTopmostNode(){
963
    	boolean parentCheck = false;
964
    	boolean classificationCheck = false;
965

    
966
    	if(getParent() != null) {
967
    		if(getParent().getTaxon() == null) {
968
    			parentCheck = true;
969
    		}
970
    	}
971

    
972
    	//TODO remove
973
    	// FIXME This should work but doesn't, due to missing sort indexes, can be removed after fixing #4200, #4098
974
    	if (classification != null){
975
    		classificationCheck = classification.getRootNode().getChildNodes().contains(this);
976
    	}else{
977
    		classificationCheck = false;
978
    	}
979

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

    
998
    	return parentCheck;
999
    }
1000

    
1001
    /**
1002
     * Whether this TaxonNode is a descendant of (or equal to) the given TaxonNode
1003
     *
1004
     * @param possibleParent
1005
     * @return <code>true</code> if <b>this</b> is a descendant
1006
     */
1007
    @Transient
1008
    public boolean isDescendant(TaxonNode possibleParent){
1009
    	if (possibleParent == null || this.treeIndex() == null
1010
    	        || possibleParent.treeIndex() == null) {
1011
    		return false;
1012
    	}
1013
    	return this.treeIndex().startsWith(possibleParent.treeIndex() );
1014
    }
1015

    
1016
    /**
1017
     * Whether this TaxonNode is an ascendant of (or equal to) the given TaxonNode.
1018
     *
1019
     *
1020
     * @param possibleChild
1021
     * @return <code>true</code> if <b>this</b> is a ancestor of the given child parameter
1022
     */
1023
    @Transient
1024
    public boolean isAncestor(TaxonNode possibleChild){
1025
    	if (possibleChild == null || this.treeIndex() == null || possibleChild.treeIndex() == null) {
1026
    		return false;
1027
    	}
1028
       // return possibleChild == null ? false : possibleChild.getAncestors().contains(this);
1029
        return  possibleChild.treeIndex().startsWith(this.treeIndex());
1030
    }
1031

    
1032
    /**
1033
     * Whether this taxon has child nodes
1034
     *
1035
     * @return true if the taxonNode has childNodes
1036
     */
1037
    @Transient
1038
    @Override
1039
    public boolean hasChildNodes(){
1040
        return childNodes.size() > 0;
1041
    }
1042

    
1043
    public boolean hasTaxon() {
1044
        return (taxon!= null);
1045
    }
1046

    
1047
    @Transient
1048
    public Rank getNullSafeRank() {
1049
        return hasTaxon() ? getTaxon().getNullSafeRank() : null;
1050
    }
1051

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

    
1061
    private void setStatus(TaxonNodeStatus status, boolean value) {
1062
        if (value){
1063
            this.status = status;
1064
        }else if (this.status != null && this.status.equals(status)){
1065
            this.status = null;
1066
        }
1067
    }
1068
    private boolean hasStatus(TaxonNodeStatus status) {
1069
        return CdmUtils.nullSafeEqual(this.status, status);
1070
    }
1071

    
1072
//*********************** CLONE ********************************************************/
1073
    /**
1074
     * Clones <i>this</i> taxon node. This is a shortcut that enables to create
1075
     * a new instance that differs only slightly from <i>this</i> taxon node by
1076
     * modifying only some of the attributes.<BR><BR>
1077
     * The child nodes are not copied.<BR>
1078
     * The taxon and parent are the same as for the original taxon node. <BR>
1079
     *
1080
     * @see eu.etaxonomy.cdm.model.media.IdentifiableEntity#clone()
1081
     * @see java.lang.Object#clone()
1082
     */
1083
    @Override
1084
    public Object clone()  {
1085
        try{
1086
            TaxonNode result = (TaxonNode)super.clone();
1087
            result.getTaxon().addTaxonNode(result);
1088

    
1089
            //childNodes
1090
            result.childNodes = new ArrayList<>();
1091
            result.countChildren = 0;
1092

    
1093
            //agents
1094
            result.agentRelations = new HashSet<>();
1095
            for (TaxonNodeAgentRelation rel : this.agentRelations){
1096
                result.addAgentRelation((TaxonNodeAgentRelation)rel.clone());
1097
            }
1098

    
1099
            //statusNote
1100
            result.statusNote = new HashMap<>();
1101
            for(Language lang : this.statusNote.keySet()){
1102
                result.statusNote.put(lang, this.statusNote.get(lang));
1103
            }
1104

    
1105

    
1106
            return result;
1107
        }catch (CloneNotSupportedException e) {
1108
            logger.warn("Object does not implement cloneable");
1109
            e.printStackTrace();
1110
            return null;
1111
        }
1112
    }
1113

    
1114
}
(13-13/21)