Project

General

Profile

Download (38.5 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
//    //the source for this placement
154
//    @XmlElement(name = "source")
155
//    @XmlIDREF
156
//    @XmlSchemaType(name = "IDREF")
157
//    @OneToOne(fetch = FetchType.LAZY, orphanRemoval=true)
158
//    @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE, CascadeType.DELETE, CascadeType.PERSIST})
159
//    private DescriptionElementSource source;
160

    
161
    @XmlElement(name = "countChildren")
162
    private int countChildren;
163

    
164
    @XmlElementWrapper(name = "agentRelations")
165
    @XmlElement(name = "agentRelation")
166
    @XmlIDREF
167
    @XmlSchemaType(name = "IDREF")
168
    @OneToMany(mappedBy="taxonNode", fetch=FetchType.LAZY)
169
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
170
    private Set<TaxonNodeAgentRelation> agentRelations = new HashSet<>();
171

    
172
    /**
173
     * The {@link TaxonNodeStatus status} of this taxon node.
174
     */
175
    @XmlAttribute(name ="TaxonNodeStatus")
176
    @Column(name="status", length=10)
177
    @Type(type = "eu.etaxonomy.cdm.hibernate.EnumUserType",
178
        parameters = {@Parameter(name  = "enumClass", value = "eu.etaxonomy.cdm.model.taxon.TaxonNodeStatus")}
179
    )
180
    @Audited
181
    private TaxonNodeStatus status;
182

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

    
191
//	private Taxon originalConcept;
192
//	//or
193
    @XmlElement(name = "synonymToBeUsed")
194
    @XmlIDREF
195
    @XmlSchemaType(name = "IDREF")
196
    @ManyToOne(fetch = FetchType.LAZY)
197
    @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
198
    private Synonym synonymToBeUsed;
199

    
200
// ******************** CONSTRUCTOR **********************************************/
201

    
202
    protected TaxonNode(){super();}
203

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

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

    
228
// ************************* GETTER / SETTER *******************************/
229

    
230
    @Transient
231
    public Integer getSortIndex() {
232
        return sortIndex;
233
	}
234
    /**
235
     * SortIndex shall be handled only internally, therefore not public.
236
     * However, as javaassist only supports protected methods it needs to be protected, not private.
237
     * Alternatively we could use deproxy on every call of this method (see commented code)
238
     * @param i
239
     * @return
240
     * @deprecated for internal use only
241
     */
242
     @Deprecated
243
    protected void setSortIndex(Integer i) {
244
//    	 CdmBase.deproxy(this, TaxonNode.class).sortIndex = i;  //alternative solution for private, DON'T remove
245
    	 sortIndex = i;
246
	}
247

    
248
    public Taxon getTaxon() {
249
        return taxon;
250
    }
251
    public void setTaxon(Taxon taxon) {
252
        this.taxon = taxon;
253
        if (taxon != null){
254
            taxon.addTaxonNode(this);
255
        }
256
    }
257

    
258
    @Override
259
    public List<TaxonNode> getChildNodes() {
260
        return childNodes;
261
    }
262
	protected void setChildNodes(List<TaxonNode> childNodes) {
263
		this.childNodes = childNodes;
264
	}
265

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

    
280
    public boolean isUnplaced() {return hasStatus(TaxonNodeStatus.UNPLACED);}
281

    
282
    //#8281 indicates a preliminary placement
283
    public boolean isDoubtful() {return hasStatus(TaxonNodeStatus.DOUBTFUL);}
284

    
285
    public boolean isExcluded() {return hasStatus(TaxonNodeStatus.EXCLUDED);}
286

    
287
//************************* SOURCE *********************/
288

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

    
309
//    @Override
310
//    public DescriptionElementSource getSource() {
311
//        return source;
312
//    }
313
//    @Override
314
//    public void setSource(DescriptionElementSource source) {
315
//        this.source = source;
316
//    }
317

    
318

    
319

    
320
//************************************************************/
321

    
322
    //countChildren
323
    public int getCountChildren() {
324
        return countChildren;
325
    }
326
    /**
327
     * @deprecated for internal use only
328
     * @param countChildren
329
     */
330
    @Deprecated
331
    protected void setCountChildren(int countChildren) {
332
        this.countChildren = countChildren;
333
    }
334

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

    
355
    public TaxonNodeStatus getStatus() {
356
        return status;
357
    }
358
    public void setStatus(TaxonNodeStatus status) {
359
        this.status = status;
360
    }
361

    
362
// *************** Status Note ***************
363

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

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

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

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

    
433
// ****************** Agent Relations ****************************/
434

    
435
    public Set<TaxonNodeAgentRelation> getAgentRelations() {
436
        return this.agentRelations;
437
    }
438
    public TaxonNodeAgentRelation addAgentRelation(DefinedTerm type, TeamOrPersonBase<?> agent){
439
        TaxonNodeAgentRelation result = TaxonNodeAgentRelation.NewInstance(this, agent, type);
440
        return result;
441
    }
442
    public void addAgentRelation(TaxonNodeAgentRelation agentRelation) {
443
        agentRelation.setTaxonNode(this);
444
        this.agentRelations.add(agentRelation);
445
    }
446
    public void removeAgentRelation(TaxonNodeAgentRelation agentRelation) {
447
        agentRelation.setTaxonNode(this);
448
        agentRelations.remove(agentRelation);
449
    }
450

    
451
//********************
452

    
453
    //synonymToBeused
454
    public Synonym getSynonymToBeUsed() {
455
        return synonymToBeUsed;
456
    }
457
    public void setSynonymToBeUsed(Synonym synonymToBeUsed) {
458
        this.synonymToBeUsed = synonymToBeUsed;
459
    }
460

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

    
480
//************************ METHODS **************************/
481

    
482
   @Override
483
    public TaxonNode addChildTaxon(Taxon taxon, Reference citation, String microCitation) {
484
        return addChildTaxon(taxon, this.childNodes.size(), citation, microCitation);
485
    }
486

    
487
   @Override
488
   public TaxonNode addChildTaxon(Taxon taxon, NamedSource source) {
489
       return addChildTaxon(taxon, this.childNodes.size(), source);
490
   }
491

    
492
    @Override
493
    public TaxonNode addChildTaxon(Taxon taxon, int index, Reference citation, String microCitation) {
494
        return addChildTaxon(taxon, index, NamedSource.NewPrimarySourceInstance(citation, microCitation));
495
    }
496

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

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

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

    
539

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

    
565
        child.setParentTreeNode(this, index);
566

    
567
        child.setSource(source);
568

    
569

    
570
        return child;
571
    }
572

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

    
595

    
596

    
597

    
598
    @Override
599
    public boolean deleteChildNode(TaxonNode node) {
600
        boolean result = removeChildNode(node);
601
        Taxon taxon = deproxy(node.getTaxon());
602
        node = deproxy(node);
603
        node.setTaxon(null);
604

    
605
        ArrayList<TaxonNode> childNodes = new ArrayList<>(node.getChildNodes());
606
        for(TaxonNode childNode : childNodes){
607
            HibernateProxyHelper.deproxy(childNode, TaxonNode.class);
608
            node.deleteChildNode(childNode);
609
        }
610
        taxon.removeTaxonNode(node);
611
        return result;
612
    }
613

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

    
638
        return result;
639
    }
640

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

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

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

    
685
        if (child != null){
686

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

    
697
            //update sortindex
698
            //TODO workaround (see sortIndex doc)
699
            this.countChildren = childNodes.size();
700
            child.setParent(null);
701
            child.setTreeIndex(null);
702
            updateSortIndex(index);
703
            child.setSortIndex(null);
704
        }
705
    }
706

    
707

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

    
721
    /**
722
     * Remove this taxonNode From its taxonomic parent
723
     *
724
     * @return true on success
725
     */
726
    public boolean delete(boolean deleteChildren){
727
        if(isTopmostNode()){
728
            return classification.deleteChildNode(this, deleteChildren);
729
        }else{
730
            return getParent().deleteChildNode(this, deleteChildren);
731
        }
732
    }
733

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

    
745

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

    
767
        	//remove from old parent
768
            formerParent.removeChildNode(this);
769
        }
770

    
771
        // set the new parent
772
        setParent(parent);
773

    
774
        // set the classification to the parents classification
775

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

    
785
//        HHH_9751_Util.removeAllNull(parentChildren);
786
//        parent.updateSortIndex(0);
787
        if (index > parent.getChildNodes().size()){
788
            index = parent.getChildNodes().size();
789
        }
790
        if (parentChildren.contains(this)){
791
            //avoid duplicates
792
            if (parentChildren.indexOf(this) < index){
793
                index = index-1;
794
            }
795
            parentChildren.remove(this);
796
            parentChildren.add(index, this);
797
        }else{
798
            parentChildren.add(index, this);
799
        }
800

    
801

    
802
        //sortIndex
803
        //TODO workaround (see sortIndex doc)
804
       // this.getParent().removeNullValueFromChildren();
805
        this.getParent().updateSortIndex(index);
806
        //only for debugging
807
        if (this.getSortIndex() == null){
808
            logger.warn("sortindex is null. This should not happen.");
809
        }else if (! this.getSortIndex().equals(index)){
810
        	logger.warn("index and sortindex are not equal: " +  this.getSortIndex() + ";" + index);
811
        }
812

    
813
        // update the children count
814
        parent.setCountChildren(parent.getChildNodes().size());
815
    }
816

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

    
840
    /**
841
     * Returns a set containing this node and all nodes that are descendants of this node.
842
     */
843
	@Transient
844
    protected Set<TaxonNode> getDescendants(){
845
        Set<TaxonNode> nodeSet = new HashSet<>();
846

    
847
        nodeSet.add(this);
848

    
849
        for(TaxonNode childNode : getChildNodes()){
850
            nodeSet.addAll(childNode.getDescendants());
851
        }
852

    
853
        return nodeSet;
854
    }
855

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

    
863
        TaxonNode clone = this.clone();
864
        TaxonNode childClone;
865

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

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

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

    
916
        if(this.getParent() != null){
917
        	TaxonNode parent =  CdmBase.deproxy(this.getParent());
918
            return parent.getAncestorOfRank(rank);
919
        }
920
		return null;
921
    }
922

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

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

    
957

    
958
    /**
959
     * Whether this TaxonNode is a direct child of the classification TreeNode
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
     * @param possibleChild
1020
     * @return <code>true</code> if <b>this</b> is a ancestor of the given child parameter
1021
     */
1022
    @Transient
1023
    public boolean isAncestor(TaxonNode possibleChild){
1024
    	if (possibleChild == null || this.treeIndex() == null || possibleChild.treeIndex() == null) {
1025
    		return false;
1026
    	}
1027
       // return possibleChild == null ? false : possibleChild.getAncestors().contains(this);
1028
        return  possibleChild.treeIndex().startsWith(this.treeIndex());
1029
    }
1030

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

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

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

    
1051
    @Transient
1052
    public TaxonName getNullSafeName() {
1053
        return getTaxon() == null? null: getTaxon().getName();
1054
    }
1055

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

    
1065
    private boolean hasStatus(TaxonNodeStatus status) {
1066
        return CdmUtils.nullSafeEqual(this.status, status);
1067
    }
1068

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

    
1087
        try{
1088
            TaxonNode result = (TaxonNode)super.clone();
1089
            result.getTaxon().addTaxonNode(result);
1090

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

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

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

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

    
1115
//To be removed once SingleSourcedEntity attributes have been renamed to reference and microReference
1116

    
1117
    @Override
1118
    @Transient
1119
    public Reference getReference() {
1120
        return getCitation();
1121
    }
1122

    
1123
    @Override
1124
    @Transient
1125
    public String getMicroReference() {
1126
        return getCitationMicroReference();
1127
    }
1128

    
1129
}
(12-12/19)