1
|
/**
|
2
|
* European Distributed Institute of Taxonomy
|
3
|
* http://www.e-taxonomy.eu
|
4
|
*
|
5
|
* The contents of this file are subject to the Mozilla Public License Version 1.1
|
6
|
* See LICENSE.TXT at the top of this package for the full license terms.
|
7
|
*/
|
8
|
|
9
|
package eu.etaxonomy.cdm.model.description;
|
10
|
|
11
|
import java.util.ArrayList;
|
12
|
import java.util.HashMap;
|
13
|
import java.util.List;
|
14
|
import java.util.Map;
|
15
|
import java.util.Map.Entry;
|
16
|
|
17
|
import javax.persistence.Entity;
|
18
|
import javax.persistence.FetchType;
|
19
|
import javax.persistence.JoinColumn;
|
20
|
import javax.persistence.ManyToOne;
|
21
|
import javax.persistence.MapKeyJoinColumn;
|
22
|
import javax.persistence.OneToMany;
|
23
|
import javax.persistence.OrderBy;
|
24
|
import javax.persistence.Transient;
|
25
|
import javax.xml.bind.annotation.XmlAccessType;
|
26
|
import javax.xml.bind.annotation.XmlAccessorType;
|
27
|
import javax.xml.bind.annotation.XmlElement;
|
28
|
import javax.xml.bind.annotation.XmlElementWrapper;
|
29
|
import javax.xml.bind.annotation.XmlIDREF;
|
30
|
import javax.xml.bind.annotation.XmlRootElement;
|
31
|
import javax.xml.bind.annotation.XmlSchemaType;
|
32
|
import javax.xml.bind.annotation.XmlType;
|
33
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
34
|
|
35
|
import org.apache.log4j.Logger;
|
36
|
import org.hibernate.LazyInitializationException;
|
37
|
import org.hibernate.annotations.Cascade;
|
38
|
import org.hibernate.annotations.CascadeType;
|
39
|
import org.hibernate.annotations.IndexColumn;
|
40
|
import org.hibernate.envers.Audited;
|
41
|
|
42
|
import eu.etaxonomy.cdm.jaxb.MultilanguageTextAdapter;
|
43
|
import eu.etaxonomy.cdm.model.common.IMultiLanguageTextHolder;
|
44
|
import eu.etaxonomy.cdm.model.common.Language;
|
45
|
import eu.etaxonomy.cdm.model.common.LanguageString;
|
46
|
import eu.etaxonomy.cdm.model.common.MultilanguageText;
|
47
|
import eu.etaxonomy.cdm.model.common.VersionableEntity;
|
48
|
import eu.etaxonomy.cdm.model.taxon.Taxon;
|
49
|
|
50
|
/**
|
51
|
* This class represents a node within a {@link PolytomousKey polytomous key}
|
52
|
* structure. The structure of such a key is a directed tree like acyclic graph
|
53
|
* of <code>PolytomousKeyNode</code>s.
|
54
|
* A <code>PolytomousKeyNode</code> represents both the node and the edges that lead
|
55
|
* to <code>this</code> node, therefore an extra class representing the edges
|
56
|
* does not exist.
|
57
|
* <BR>
|
58
|
* The attribute representing the edge leading from its parent node to <code>this</code>
|
59
|
* node is the {@link #getStatement() statement}, attributes leading to the child nodes
|
60
|
* are either the {@link #getQuestion() question} or the {@link #getFeature() feature}.
|
61
|
* While {@link #getStatement() statements} are required, {@link #getQuestion() questions} and
|
62
|
* {@link #getFeature() features} are optional and do typically not exist in classical keys.
|
63
|
* Both, {@link #getQuestion() questions} and {@link #getFeature() features}, will be "answered" by the
|
64
|
* {@link #getStatement() statements} of the child nodes, where {@link #getQuestion() questions}
|
65
|
* are usually free text used in manually created keys while {@link #getFeature() features} are
|
66
|
* typically used in automatically created keys based on structured descriptive data.
|
67
|
* Only one of them should be defined in a node. However, if both exist the {@link #getQuestion() question}
|
68
|
* should always be given <b>priority</b> over the {@link #getFeature() feature}.<br>
|
69
|
*
|
70
|
* Typically a node either links to its child nodes (subnodes) or represents a link
|
71
|
* to a {@link Taxon taxon}. The later, if taken as part of the tree, are usually
|
72
|
* the leaves of the represented tree like structure (taxonomically they are the
|
73
|
* end point of the decision process).<br>
|
74
|
*
|
75
|
* However, there are exceptions to this simple structure:
|
76
|
*
|
77
|
* <li>Subnodes and taxon link<br>
|
78
|
*
|
79
|
* In rare cases a node can have both, subnodes and a {@link #getTaxon() link to a taxon}.
|
80
|
* In this case the taxonomic determination process may be either terminated
|
81
|
* at the given {@link Taxon taxon} or can proceed with the children if a more accurate
|
82
|
* determination is wanted. This may be the case e.g. in a key that generally
|
83
|
* covers all taxa of rank species and at the same time allows identification of
|
84
|
* subspecies or varieties of these taxa.</li>
|
85
|
*
|
86
|
* <li>{@link #getOtherNode() Other nodes}: <br>
|
87
|
*
|
88
|
* A node may not only link to its subnodes or to a taxon but it may
|
89
|
* also link to {@link #getOtherNode() another node} (with a different parent) of either the same key
|
90
|
* or another key.
|
91
|
* <br>
|
92
|
* <b>NOTE: </b>
|
93
|
* If an {@link #getOtherNode() otherNode} represents a node
|
94
|
* of the same tree the key does not represent a strict tree structure
|
95
|
* anymore. However, as this is a rare case we will still use this term
|
96
|
* at some places.</li>
|
97
|
*
|
98
|
* <li>{@link #getSubkey() Subkey}:<br>
|
99
|
*
|
100
|
* A node may also link to another key ({@link #getSubkey() subkey}) as a whole, which is
|
101
|
* equal to an {@link #getOtherNode() otherNode} link to the root node of the other key.
|
102
|
* In this case the path in the decision graph spans over multiple keys.</li>
|
103
|
* This structure is typically used when a key covers taxa down to a certain rank, whereas
|
104
|
* taxa below this rank are covered by extra keys (e.g. a parent key may cover all taxa
|
105
|
* of rank species while subspecies and varieties are covered by a subkeys for each of these
|
106
|
* species.
|
107
|
* Another usecase for subkeys is the existence of an alternative key for a certain part
|
108
|
* of the decision tree.
|
109
|
*
|
110
|
* <li>Multiple taxa<br>
|
111
|
*
|
112
|
* Some nodes in legacy keys do link to multiple taxa, meaning that the key ambigous at
|
113
|
* this point. To represent such nodes one must use child nodes with empty
|
114
|
* {@link #getStatement() statements} for each such taxon (in all other cases - except for
|
115
|
* root nodes - the <code>statement</code> is required).
|
116
|
* Applications that do visualize the key should handle such a node-subnode structure as one
|
117
|
* node with multiple taxon links. This complicated data structure has been chosen for
|
118
|
* this rare to avoid a more complicated <code>List<Taxon></code> structure for the standard
|
119
|
* case.</li>
|
120
|
*
|
121
|
* The {@link PolytomousKey#getRoot() root node of the key} may represent the entry point
|
122
|
* question or feature but does naturally neither have a statement nor a linked taxon as
|
123
|
* there is no prior decision yet.
|
124
|
*
|
125
|
* <h4>Notes</h4>
|
126
|
* <p>
|
127
|
* A polytomous key node can be referenced from multiple other nodes via the
|
128
|
* {@link #getOtherNode() otherNode} attribute of the other nodes. Therefore, though
|
129
|
* we speek about a "decision tree" structure a node does not necessarily have only
|
130
|
* one parent.
|
131
|
* However, nodes are mainly represented in a tree structure and therefore do have
|
132
|
* a defined {@link #getParent() parent} which is the "main" parent. But when implementing
|
133
|
* visualizing or editing tools one should keep in mind that this parent may not be
|
134
|
* the only node linking the child node.
|
135
|
*
|
136
|
* @author a.mueller
|
137
|
* @created 13-Oct-2010
|
138
|
*
|
139
|
*/
|
140
|
@SuppressWarnings("serial")
|
141
|
@XmlAccessorType(XmlAccessType.FIELD)
|
142
|
@XmlType(name = "PolytomousKeyNode", propOrder = { "key", "parent", "children",
|
143
|
"sortIndex", "nodeNumber", "statement", "question", "feature", "taxon",
|
144
|
"subkey", "otherNode", "modifyingText" })
|
145
|
@XmlRootElement(name = "FeatureNode")
|
146
|
@Entity
|
147
|
@Audited
|
148
|
public class PolytomousKeyNode extends VersionableEntity implements IMultiLanguageTextHolder {
|
149
|
private static final Logger logger = Logger.getLogger(PolytomousKeyNode.class);
|
150
|
|
151
|
// This is the main key a node belongs to. Although other keys may also
|
152
|
// reference
|
153
|
// <code>this</code> node, a node usually belongs to a given key.
|
154
|
@XmlElement(name = "PolytomousKey")
|
155
|
@XmlIDREF
|
156
|
@XmlSchemaType(name = "IDREF")
|
157
|
// @ManyToOne(fetch = FetchType.LAZY, optional=false)
|
158
|
// @JoinColumn(nullable=false)
|
159
|
// @NotNull
|
160
|
// @Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE /*, CascadeType.DELETE_ORPHAN */})
|
161
|
@ManyToOne(fetch = FetchType.LAZY)
|
162
|
@Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE })
|
163
|
private PolytomousKey key;
|
164
|
|
165
|
@XmlElementWrapper(name = "Children")
|
166
|
@XmlElement(name = "Child")
|
167
|
// @OrderColumn("sortIndex") //JPA 2.0 same as @IndexColumn
|
168
|
// @IndexColumn does not work because not every FeatureNode has a parent.
|
169
|
// But only NotNull will solve the problem (otherwise
|
170
|
// we will need a join table
|
171
|
// http://stackoverflow.com/questions/2956171/jpa-2-0-ordercolumn-annotation-in-hibernate-3-5
|
172
|
// http://docs.jboss.org/hibernate/stable/annotations/reference/en/html_single/#entity-hibspec-collection-extratype-indexbidir
|
173
|
// see also https://forum.hibernate.org/viewtopic.php?p=2392563
|
174
|
// http://opensource.atlassian.com/projects/hibernate/browse/HHH-4390
|
175
|
// reading works, but writing doesn't
|
176
|
//
|
177
|
@IndexColumn(name = "sortIndex", base = 0)
|
178
|
@OrderBy("sortIndex")
|
179
|
@OneToMany(fetch = FetchType.LAZY, mappedBy = "parent")
|
180
|
@Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE })
|
181
|
private List<PolytomousKeyNode> children = new ArrayList<PolytomousKeyNode>();
|
182
|
|
183
|
|
184
|
|
185
|
@XmlElement(name = "Parent")
|
186
|
@XmlIDREF
|
187
|
@XmlSchemaType(name = "IDREF")
|
188
|
@Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE })
|
189
|
@ManyToOne(fetch = FetchType.LAZY, targetEntity = PolytomousKeyNode.class)
|
190
|
@JoinColumn(name = "parent_id" /*
|
191
|
* , insertable=false, updatable=false,
|
192
|
* nullable=false
|
193
|
*/)
|
194
|
private PolytomousKeyNode parent;
|
195
|
|
196
|
// see comment on children @IndexColumn
|
197
|
private Integer sortIndex;
|
198
|
|
199
|
@XmlElement(name = "Statement")
|
200
|
@XmlIDREF
|
201
|
@XmlSchemaType(name = "IDREF")
|
202
|
@ManyToOne(fetch = FetchType.LAZY)
|
203
|
@Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE })
|
204
|
private KeyStatement statement;
|
205
|
|
206
|
@XmlElement(name = "Question")
|
207
|
@XmlIDREF
|
208
|
@XmlSchemaType(name = "IDREF")
|
209
|
@ManyToOne(fetch = FetchType.LAZY)
|
210
|
@Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
|
211
|
private KeyStatement question;
|
212
|
|
213
|
@XmlElement(name = "Feature")
|
214
|
@XmlIDREF
|
215
|
@XmlSchemaType(name = "IDREF")
|
216
|
@ManyToOne(fetch = FetchType.LAZY)
|
217
|
@Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
|
218
|
private Feature feature;
|
219
|
|
220
|
@XmlElement(name = "Taxon")
|
221
|
@XmlIDREF
|
222
|
@XmlSchemaType(name = "IDREF")
|
223
|
@ManyToOne(fetch = FetchType.LAZY)
|
224
|
@Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
|
225
|
private Taxon taxon;
|
226
|
|
227
|
// Refers to an entire key
|
228
|
// <code>this</code> node, a node usually belongs to a given key.
|
229
|
@XmlElement(name = "SubKey")
|
230
|
@XmlIDREF
|
231
|
@XmlSchemaType(name = "IDREF")
|
232
|
@ManyToOne(fetch = FetchType.LAZY)
|
233
|
@Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
|
234
|
private PolytomousKey subkey;
|
235
|
|
236
|
// Refers to an other node within this key or an other key
|
237
|
@XmlElement(name = "PolytomousKey")
|
238
|
@XmlIDREF
|
239
|
@XmlSchemaType(name = "IDREF")
|
240
|
@ManyToOne(fetch = FetchType.LAZY)
|
241
|
@Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
|
242
|
private PolytomousKeyNode otherNode;
|
243
|
|
244
|
private Integer nodeNumber = null;
|
245
|
|
246
|
// TODO should be available for each taxon/result
|
247
|
@XmlElement(name = "ModifyingText")
|
248
|
@XmlJavaTypeAdapter(MultilanguageTextAdapter.class)
|
249
|
@OneToMany(fetch = FetchType.LAZY)
|
250
|
@MapKeyJoinColumn(name="modifyingtext_mapkey_id")
|
251
|
@Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE })
|
252
|
private Map<Language, LanguageString> modifyingText = new HashMap<Language, LanguageString>();
|
253
|
|
254
|
// ************************** FACTORY ********************************/
|
255
|
|
256
|
/**
|
257
|
* Creates a new empty polytomous key node instance.
|
258
|
*/
|
259
|
public static PolytomousKeyNode NewInstance() {
|
260
|
return new PolytomousKeyNode();
|
261
|
}
|
262
|
|
263
|
/**
|
264
|
* Creates a new polytomous key node instance.
|
265
|
*
|
266
|
*/
|
267
|
public static PolytomousKeyNode NewInstance(String statement) {
|
268
|
PolytomousKeyNode result = new PolytomousKeyNode();
|
269
|
result.setStatement(KeyStatement.NewInstance(statement));
|
270
|
return result;
|
271
|
}
|
272
|
|
273
|
/**
|
274
|
* Creates a new polytomous key node instance.
|
275
|
*
|
276
|
*/
|
277
|
public static PolytomousKeyNode NewInstance(String statement,
|
278
|
String question, Taxon taxon, Feature feature) {
|
279
|
PolytomousKeyNode result = new PolytomousKeyNode();
|
280
|
result.setTaxon(taxon);
|
281
|
result.setStatement(KeyStatement.NewInstance(statement));
|
282
|
result.setQuestion(KeyStatement.NewInstance(question));
|
283
|
result.setFeature(feature);
|
284
|
return result;
|
285
|
}
|
286
|
|
287
|
// ************************** CONSTRUCTOR *****************************/
|
288
|
|
289
|
/**
|
290
|
* Class constructor: creates a new empty feature node instance.
|
291
|
*/
|
292
|
protected PolytomousKeyNode() {
|
293
|
super();
|
294
|
}
|
295
|
|
296
|
// ** ********************** GETTER / SETTER ******************************/
|
297
|
|
298
|
|
299
|
//see #4278 and #4200, alternatively can be private and use deproxy(this, PolytomousKeyNode.class)
|
300
|
protected void setSortIndex(Integer sortIndex) {
|
301
|
this.sortIndex = sortIndex;
|
302
|
}
|
303
|
|
304
|
/**
|
305
|
* @return
|
306
|
*/
|
307
|
public PolytomousKey getKey() {
|
308
|
return key;
|
309
|
}
|
310
|
|
311
|
/**
|
312
|
* @param key
|
313
|
*/
|
314
|
public void setKey(PolytomousKey key) {
|
315
|
this.key = key;
|
316
|
}
|
317
|
|
318
|
/**
|
319
|
* The node number is the number of the node within the key. This
|
320
|
* corresponds to the number for key choices in written keys.
|
321
|
*/
|
322
|
public Integer getNodeNumber() {
|
323
|
return nodeNumber;
|
324
|
}
|
325
|
|
326
|
/**
|
327
|
* Is computed automatically and therefore should not be set by the user.
|
328
|
*/
|
329
|
public void setNodeNumber(Integer nodeNumber) {
|
330
|
this.nodeNumber = nodeNumber;
|
331
|
}
|
332
|
|
333
|
/**
|
334
|
* Returns the taxon this node links to. This is usually the case when this
|
335
|
* node is a leaf.
|
336
|
*
|
337
|
* @return
|
338
|
* @see #setTaxon(Taxon)
|
339
|
* @see #getSubkey()
|
340
|
* @see #getChildren()
|
341
|
* @see #getOtherNode()
|
342
|
*/
|
343
|
public Taxon getTaxon() {
|
344
|
return taxon;
|
345
|
}
|
346
|
|
347
|
/**
|
348
|
* Sets the taxon this node links to. <BR>
|
349
|
* If a tax
|
350
|
*
|
351
|
* @param taxon
|
352
|
* @see #getTaxon()
|
353
|
*/
|
354
|
public void setTaxon(Taxon taxon) {
|
355
|
this.taxon = taxon;
|
356
|
}
|
357
|
|
358
|
/**
|
359
|
* @return
|
360
|
* @see #setSubkey(PolytomousKey)
|
361
|
* @see #getTaxon()
|
362
|
* @see #getChildren()
|
363
|
* @see #getOtherNode()
|
364
|
*/
|
365
|
public PolytomousKey getSubkey() {
|
366
|
return subkey;
|
367
|
}
|
368
|
|
369
|
/**
|
370
|
* @param subkey
|
371
|
* @see #getSubkey()
|
372
|
*/
|
373
|
public void setSubkey(PolytomousKey subkey) {
|
374
|
this.subkey = subkey;
|
375
|
}
|
376
|
|
377
|
/**
|
378
|
* @return
|
379
|
* @see #setOtherNode(PolytomousKeyNode)
|
380
|
* @see #getTaxon()
|
381
|
* @see #getChildren()
|
382
|
* @see #getSubkey()
|
383
|
*/
|
384
|
public PolytomousKeyNode getOtherNode() {
|
385
|
return otherNode;
|
386
|
}
|
387
|
|
388
|
/**
|
389
|
* @param otherNode
|
390
|
* @see #getOtherNode()
|
391
|
*/
|
392
|
public void setOtherNode(PolytomousKeyNode otherNode) {
|
393
|
this.otherNode = otherNode;
|
394
|
}
|
395
|
|
396
|
// TODO
|
397
|
public void setFeature(Feature feature) {
|
398
|
this.feature = feature;
|
399
|
}
|
400
|
|
401
|
public Feature getFeature() {
|
402
|
return feature;
|
403
|
}
|
404
|
|
405
|
/**
|
406
|
* Returns the parent node of <code>this</code> child.
|
407
|
*
|
408
|
* @return
|
409
|
*/
|
410
|
public PolytomousKeyNode getParent() {
|
411
|
return parent;
|
412
|
}
|
413
|
|
414
|
/**
|
415
|
* For bidirectional use only !
|
416
|
*
|
417
|
* @param parent
|
418
|
*/
|
419
|
protected void setParent(PolytomousKeyNode parent) {
|
420
|
PolytomousKeyNode oldParent = this.parent;
|
421
|
if (oldParent != null){
|
422
|
if (oldParent.getChildren().contains(this)){
|
423
|
oldParent.removeChild(this);
|
424
|
}
|
425
|
}
|
426
|
this.parent = parent;
|
427
|
|
428
|
}
|
429
|
|
430
|
/**
|
431
|
* Returns the (ordered) list of feature nodes which are children nodes of
|
432
|
* <i>this</i> feature node.
|
433
|
*/
|
434
|
public List<PolytomousKeyNode> getChildren() {
|
435
|
removeNullValueFromChildren();
|
436
|
return children;
|
437
|
}
|
438
|
|
439
|
/**
|
440
|
* Adds the given polytomous key node at the end of the list of children of
|
441
|
* <i>this</i> polytomous key node.
|
442
|
*
|
443
|
* @param child
|
444
|
* the feature node to be added
|
445
|
* @see #getChildren()
|
446
|
* @see #setChildren(List)
|
447
|
* @see #addChild(PolytomousKeyNode, int)
|
448
|
* @see #removeChild(PolytomousKeyNode)
|
449
|
* @see #removeChild(int)
|
450
|
*/
|
451
|
public void addChild(PolytomousKeyNode child) {
|
452
|
addChild(child, children.size());
|
453
|
}
|
454
|
|
455
|
/**
|
456
|
* Inserts the given child node in the list of children of <i>this</i>
|
457
|
* polytomous key node at the given (index + 1) position. If the given index
|
458
|
* is out of bounds an exception will be thrown.<BR>
|
459
|
*
|
460
|
* @param child
|
461
|
* the polytomous key node to be added
|
462
|
* @param index
|
463
|
* the integer indicating the position at which the child should
|
464
|
* be added
|
465
|
* @see #getChildren()
|
466
|
* @see #setChildren(List)
|
467
|
* @see #addChild(PolytomousKeyNode)
|
468
|
* @see #removeChild(PolytomousKeyNode)
|
469
|
* @see #removeChild(int)
|
470
|
*/
|
471
|
public void addChild(PolytomousKeyNode child, int index) {
|
472
|
if (index < 0 || index > children.size() + 1) {
|
473
|
throw new IndexOutOfBoundsException("Wrong index: " + index);
|
474
|
}
|
475
|
removeNullValueFromChildren();
|
476
|
|
477
|
if(nodeNumber == null) {
|
478
|
nodeNumber = getMaxNodeNumberFromRoot() + 1;
|
479
|
}
|
480
|
|
481
|
|
482
|
children.add(index, child);
|
483
|
child.setKey(this.getKey());
|
484
|
|
485
|
// TODO workaround (see sortIndex doc)
|
486
|
for (int i = 0; i < children.size(); i++) {
|
487
|
children.get(i).setSortIndex(i);
|
488
|
}
|
489
|
child.setSortIndex(index);
|
490
|
child.setParent(this);
|
491
|
}
|
492
|
|
493
|
|
494
|
|
495
|
/**
|
496
|
* Removes the given polytomous key node from the list of
|
497
|
* {@link #getChildren() children} of <i>this</i> polytomous key node.
|
498
|
*
|
499
|
* @param child
|
500
|
* the feature node which should be removed
|
501
|
* @see #getChildren()
|
502
|
* @see #addChild(PolytomousKeyNode, int)
|
503
|
* @see #addChild(PolytomousKeyNode)
|
504
|
* @see #removeChild(int)
|
505
|
*/
|
506
|
public void removeChild(PolytomousKeyNode child) {
|
507
|
int index = children.indexOf(child);
|
508
|
removeNullValueFromChildren();
|
509
|
if (index >= 0) {
|
510
|
removeChild(index);
|
511
|
}
|
512
|
}
|
513
|
|
514
|
|
515
|
private void removeNullValueFromChildren(){
|
516
|
try {
|
517
|
if (children.contains(null)){
|
518
|
while(children.contains(null)){
|
519
|
children.remove(null);
|
520
|
}
|
521
|
}
|
522
|
} catch (LazyInitializationException e) {
|
523
|
logger.info("Cannot clean up uninitialized children without a session, skipping.");
|
524
|
}
|
525
|
}
|
526
|
/**
|
527
|
* Removes the feature node placed at the given (index + 1) position from
|
528
|
* the list of {@link #getChildren() children} of <i>this</i> feature node.
|
529
|
* If the given index is out of bounds no child will be removed.
|
530
|
*
|
531
|
* @param index
|
532
|
* the integer indicating the position of the feature node to be
|
533
|
* removed
|
534
|
* @see #getChildren()
|
535
|
* @see #addChild(PolytomousKeyNode, int)
|
536
|
* @see #addChild(PolytomousKeyNode)
|
537
|
* @see #removeChild(PolytomousKeyNode)
|
538
|
*/
|
539
|
public void removeChild(int index) {
|
540
|
PolytomousKeyNode child = children.get(index);
|
541
|
if (child != null) {
|
542
|
children.remove(index);
|
543
|
child.setParent(null);
|
544
|
// TODO workaround (see sortIndex doc)
|
545
|
for (int i = 0; i < children.size(); i++) {
|
546
|
PolytomousKeyNode childAt = children.get(i);
|
547
|
childAt.setSortIndex(i);
|
548
|
}
|
549
|
child.setSortIndex(null);
|
550
|
child.setNodeNumber(null);
|
551
|
}
|
552
|
refreshNodeNumbering();
|
553
|
}
|
554
|
|
555
|
// **************************** METHODS ************************************/
|
556
|
|
557
|
/**
|
558
|
* Returns the current maximum value of the node number in the entire key
|
559
|
* starting from the root.
|
560
|
*
|
561
|
* @return
|
562
|
*/
|
563
|
private int getMaxNodeNumberFromRoot() {
|
564
|
PolytomousKeyNode rootKeyNode = this.getKey().getRoot();
|
565
|
int rootNumber = this.getKey().getStartNumber();
|
566
|
return getMaxNodeNumber(rootNumber, rootKeyNode);
|
567
|
}
|
568
|
|
569
|
/**
|
570
|
* Returns the current maximum value of the node number in the entire key
|
571
|
* starting from the given key node, comparing with a given max value as input.
|
572
|
*
|
573
|
* @return
|
574
|
*/
|
575
|
private int getMaxNodeNumber(int maxNumber, PolytomousKeyNode parent) {
|
576
|
if (parent.getNodeNumber() != null) {
|
577
|
maxNumber = (maxNumber < parent.getNodeNumber()) ? parent.getNodeNumber() : maxNumber;
|
578
|
for (PolytomousKeyNode child : parent.getChildren()) {
|
579
|
if (parent == child){
|
580
|
throw new RuntimeException("Parent and child are the same for the given key node. This will lead to an infinite loop when updating the max node number.");
|
581
|
}else{
|
582
|
maxNumber = getMaxNodeNumber(maxNumber, child);
|
583
|
}
|
584
|
}
|
585
|
}
|
586
|
return maxNumber;
|
587
|
}
|
588
|
|
589
|
/**
|
590
|
* Refresh numbering of key nodes starting from root.
|
591
|
*
|
592
|
*/
|
593
|
public void refreshNodeNumbering() {
|
594
|
updateNodeNumbering(getKey().getRoot(), getKey().getStartNumber());
|
595
|
}
|
596
|
|
597
|
/**
|
598
|
* Recursively (depth-first) refresh numbering of key nodes starting from the given key node,
|
599
|
* starting with a given node number.
|
600
|
*
|
601
|
* @return new starting node number value
|
602
|
*/
|
603
|
private int updateNodeNumbering(PolytomousKeyNode node,int nodeN) {
|
604
|
int newNodeN = nodeN;
|
605
|
if (node.isLeaf()) {
|
606
|
node.setNodeNumber(null);
|
607
|
} else {
|
608
|
node.setNodeNumber(nodeN);
|
609
|
newNodeN++;
|
610
|
List<PolytomousKeyNode> children = node.getChildren();
|
611
|
while (children.contains(null)){
|
612
|
children.remove(null);
|
613
|
}
|
614
|
|
615
|
for (PolytomousKeyNode child : children) {
|
616
|
if (node == child){
|
617
|
throw new RuntimeException("Parent and child are the same for the given key node. This will lead to an infinite loop when updating node numbers.");
|
618
|
}else{
|
619
|
newNodeN = updateNodeNumbering(child, newNodeN);
|
620
|
}
|
621
|
}
|
622
|
}
|
623
|
return newNodeN;
|
624
|
}
|
625
|
|
626
|
|
627
|
|
628
|
|
629
|
/**
|
630
|
* Returns the feature node placed at the given (childIndex + 1) position
|
631
|
* within the list of {@link #getChildren() children} of <i>this</i> feature
|
632
|
* node. If the given index is out of bounds no child will be returned.
|
633
|
*
|
634
|
* @param childIndex
|
635
|
* the integer indicating the position of the feature node
|
636
|
* @see #getChildren()
|
637
|
* @see #addChild(PolytomousKeyNode, int)
|
638
|
* @see #removeChild(int)
|
639
|
*/
|
640
|
public PolytomousKeyNode getChildAt(int childIndex) {
|
641
|
return children.get(childIndex);
|
642
|
}
|
643
|
|
644
|
/**
|
645
|
* Returns the number of children nodes of <i>this</i> feature node.
|
646
|
*
|
647
|
* @see #getChildren()
|
648
|
*/
|
649
|
@Transient
|
650
|
public int childCount() {
|
651
|
return children.size();
|
652
|
}
|
653
|
|
654
|
/**
|
655
|
* Returns the integer indicating the position of the given feature node
|
656
|
* within the list of {@link #getChildren() children} of <i>this</i> feature
|
657
|
* node. If the list does not contain this node then -1 will be returned.
|
658
|
*
|
659
|
* @param node
|
660
|
* the feature node the position of which is being searched
|
661
|
* @see #addChild(PolytomousKeyNode, int)
|
662
|
* @see #removeChild(int)
|
663
|
*/
|
664
|
public int getIndex(PolytomousKeyNode node) {
|
665
|
if (!children.contains(node)) {
|
666
|
return -1;
|
667
|
} else {
|
668
|
return children.indexOf(node);
|
669
|
}
|
670
|
}
|
671
|
|
672
|
/**
|
673
|
* Returns the boolean value indicating if <i>this</i> feature node has
|
674
|
* children (false) or not (true). A node without children is at the
|
675
|
* bottommost level of a tree and is called a leaf.
|
676
|
*
|
677
|
* @see #getChildren()
|
678
|
* @see #getChildCount()
|
679
|
*/
|
680
|
@Transient
|
681
|
public boolean isLeaf() {
|
682
|
return children.size() < 1;
|
683
|
}
|
684
|
|
685
|
// ** ********************** QUESTIONS AND STATEMENTS ************************/
|
686
|
|
687
|
/**
|
688
|
* Returns the statement for <code>this</code> PolytomousKeyNode. When coming
|
689
|
* from the parent node the user needs to agree with the statement (and disagree
|
690
|
* with all statements of sibling nodes) to follow <code>this</code> node.<BR>
|
691
|
* The statement may stand alone (standard in classical keys) or it may be
|
692
|
* either the answer to the {@link #getQuestion() question} or the
|
693
|
* value for the {@link #getFeature() feature} of the parent node.
|
694
|
*
|
695
|
* @return the statement
|
696
|
* @see #getQuestion()
|
697
|
*/
|
698
|
public KeyStatement getStatement() {
|
699
|
return statement;
|
700
|
}
|
701
|
|
702
|
/**
|
703
|
* This is a convenience method to set the statement text for this node in
|
704
|
* the given language. <BR>
|
705
|
* If no statement exists yet a new statement is created. <BR>
|
706
|
* If a statement text in the given language exists already it is
|
707
|
* overwritten and the old text is returned. If language is
|
708
|
* <code>null</code> the default language is used instead.
|
709
|
*
|
710
|
* @param text
|
711
|
* the statement text
|
712
|
* @param language
|
713
|
* the language of the statement text
|
714
|
* @return the old statement text in the given language as LanguageString
|
715
|
*/
|
716
|
public LanguageString addStatementText(String text, Language language) {
|
717
|
if (language == null) {
|
718
|
language = Language.DEFAULT();
|
719
|
}
|
720
|
if (this.statement == null) {
|
721
|
setStatement(KeyStatement.NewInstance());
|
722
|
}
|
723
|
return getStatement().putLabel(language, text);
|
724
|
}
|
725
|
|
726
|
/**
|
727
|
* @param statement
|
728
|
* @see #getStatement()
|
729
|
*/
|
730
|
public void setStatement(KeyStatement statement) {
|
731
|
this.statement = statement;
|
732
|
}
|
733
|
|
734
|
/**
|
735
|
* Returns the question for <code>this</code> PolytomousKeyNode. <BR>
|
736
|
* A question is answered by statements in leads below this tree node.
|
737
|
* Questions are optional and are usually empty in traditional keys.
|
738
|
*
|
739
|
* @return the question
|
740
|
* @see #getStatement()
|
741
|
*/
|
742
|
public KeyStatement getQuestion() {
|
743
|
return question;
|
744
|
}
|
745
|
|
746
|
/**
|
747
|
* This is a convenience method to sets the question text for this node in
|
748
|
* the given language. <BR>
|
749
|
* If no question exists yet a new question is created. <BR>
|
750
|
* If a question text in the given language exists already it is overwritten
|
751
|
* and the old text is returned. If language is <code>null</code> the
|
752
|
* default language is used instead.
|
753
|
*
|
754
|
* @param text
|
755
|
* @param language
|
756
|
* @return
|
757
|
*/
|
758
|
public LanguageString addQuestionText(String text, Language language) {
|
759
|
if (language == null) {
|
760
|
language = Language.DEFAULT();
|
761
|
}
|
762
|
if (this.question == null) {
|
763
|
setQuestion(KeyStatement.NewInstance());
|
764
|
}
|
765
|
return getQuestion().putLabel(language, text);
|
766
|
}
|
767
|
|
768
|
/**
|
769
|
* @param question
|
770
|
* @see #getQuestion()
|
771
|
*/
|
772
|
public void setQuestion(KeyStatement question) {
|
773
|
this.question = question;
|
774
|
}
|
775
|
|
776
|
// **************** modifying text ***************************************
|
777
|
|
778
|
/**
|
779
|
* Returns the {@link MultilanguageText} like "an unusual form of",
|
780
|
* commenting the determined taxon. That is a modifyingText may by used to
|
781
|
* comment or to constraint the decision step represented by the edge
|
782
|
* leading to <i>this</i> node
|
783
|
* <p>
|
784
|
* All {@link LanguageString language strings} contained in the
|
785
|
* multilanguage texts should all have the same meaning.<BR>
|
786
|
*/
|
787
|
public Map<Language, LanguageString> getModifyingText() {
|
788
|
return this.modifyingText;
|
789
|
}
|
790
|
|
791
|
/**
|
792
|
* See {@link #getModifyingText}
|
793
|
*
|
794
|
* @param description
|
795
|
* the language string describing the validity in a particular
|
796
|
* language
|
797
|
* @see #getModifyingText()
|
798
|
* @see #putModifyingText(Language, String)
|
799
|
* @deprecated should follow the put semantic of maps, this method will be
|
800
|
* removed in v4.0 Use the
|
801
|
* {@link #putModifyingText(LanguageString) putModifyingText}
|
802
|
* method instead
|
803
|
*/
|
804
|
@Deprecated
|
805
|
public LanguageString addModifyingText(LanguageString description) {
|
806
|
return this.putModifyingText(description);
|
807
|
}
|
808
|
|
809
|
/**
|
810
|
* See {@link #getModifyingText}
|
811
|
*
|
812
|
* @param description
|
813
|
* the language string describing the validity in a particular
|
814
|
* language
|
815
|
* @see #getModifyingText()
|
816
|
* @see #putModifyingText(Language, String)
|
817
|
*/
|
818
|
public LanguageString putModifyingText(LanguageString description) {
|
819
|
return this.modifyingText.put(description.getLanguage(), description);
|
820
|
}
|
821
|
|
822
|
/**
|
823
|
* See {@link #getModifyingText}
|
824
|
*
|
825
|
* @param text
|
826
|
* the string describing the validity in a particular language
|
827
|
* @param language
|
828
|
* the language in which the text string is formulated
|
829
|
* @see #getModifyingText()
|
830
|
* @see #putModifyingText(LanguageString)
|
831
|
* @deprecated should follow the put semantic of maps, this method will be
|
832
|
* removed in v4.0 Use the
|
833
|
* {@link #putModifyingText(Language, String) putModifyingText}
|
834
|
* method instead
|
835
|
*/
|
836
|
@Deprecated
|
837
|
public LanguageString addModifyingText(String text, Language language) {
|
838
|
return this.putModifyingText(language, text);
|
839
|
}
|
840
|
|
841
|
/**
|
842
|
* See {@link #getModifyingText}
|
843
|
*
|
844
|
* @param text
|
845
|
* the string describing the validity in a particular language
|
846
|
* @param language
|
847
|
* the language in which the text string is formulated
|
848
|
* @see #getModifyingText()
|
849
|
* @see #putModifyingText(LanguageString)
|
850
|
*/
|
851
|
public LanguageString putModifyingText(Language language, String text) {
|
852
|
return this.modifyingText.put(language,
|
853
|
LanguageString.NewInstance(text, language));
|
854
|
}
|
855
|
|
856
|
/**
|
857
|
* See {@link #getModifyingText}
|
858
|
*
|
859
|
* @param language
|
860
|
* the language in which the language string to be removed has
|
861
|
* been formulated
|
862
|
* @see #getModifyingText()
|
863
|
*/
|
864
|
public LanguageString removeModifyingText(Language language) {
|
865
|
return this.modifyingText.remove(language);
|
866
|
}
|
867
|
|
868
|
|
869
|
// *********************** CLONE ********************************************************/
|
870
|
|
871
|
/**
|
872
|
* Clones <i>this</i> PolytomousKeyNode. This is a shortcut that enables to
|
873
|
* create a new instance that differs only slightly from <i>this</i>
|
874
|
* PolytomousKeyNode by modifying only some of the attributes. The parent,
|
875
|
* the feature and the key are the are the same as for the original feature
|
876
|
* node the children are removed.
|
877
|
*
|
878
|
* @see eu.etaxonomy.cdm.model.common.VersionableEntity#clone()
|
879
|
* @see java.lang.Object#clone()
|
880
|
*/
|
881
|
@Override
|
882
|
public Object clone() {
|
883
|
PolytomousKeyNode result;
|
884
|
try {
|
885
|
result = (PolytomousKeyNode) super.clone();
|
886
|
result.children = new ArrayList<PolytomousKeyNode>();
|
887
|
|
888
|
result.modifyingText = new HashMap<Language, LanguageString>();
|
889
|
for (Entry<Language, LanguageString> entry : this.modifyingText
|
890
|
.entrySet()) {
|
891
|
result.putModifyingText(entry.getValue());
|
892
|
}
|
893
|
|
894
|
return result;
|
895
|
} catch (CloneNotSupportedException e) {
|
896
|
logger.warn("Object does not implement cloneable");
|
897
|
e.printStackTrace();
|
898
|
return null;
|
899
|
}
|
900
|
}
|
901
|
|
902
|
/**
|
903
|
*
|
904
|
*/
|
905
|
public void removeTaxon() {
|
906
|
this.taxon = null;
|
907
|
|
908
|
}
|
909
|
|
910
|
|
911
|
}
|