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.OrderColumn;
|
25
|
import javax.persistence.Transient;
|
26
|
import javax.xml.bind.annotation.XmlAccessType;
|
27
|
import javax.xml.bind.annotation.XmlAccessorType;
|
28
|
import javax.xml.bind.annotation.XmlElement;
|
29
|
import javax.xml.bind.annotation.XmlElementWrapper;
|
30
|
import javax.xml.bind.annotation.XmlIDREF;
|
31
|
import javax.xml.bind.annotation.XmlRootElement;
|
32
|
import javax.xml.bind.annotation.XmlSchemaType;
|
33
|
import javax.xml.bind.annotation.XmlType;
|
34
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
35
|
|
36
|
import org.apache.log4j.Logger;
|
37
|
import org.hibernate.annotations.Cascade;
|
38
|
import org.hibernate.annotations.CascadeType;
|
39
|
import org.hibernate.envers.Audited;
|
40
|
|
41
|
import eu.etaxonomy.cdm.hibernate.HHH_9751_Util;
|
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 <code>this</code> node, a node usually belongs to a given key.
|
153
|
@XmlElement(name = "PolytomousKey")
|
154
|
@XmlIDREF
|
155
|
@XmlSchemaType(name = "IDREF")
|
156
|
// @NotNull
|
157
|
@ManyToOne(fetch = FetchType.LAZY)
|
158
|
@Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE })
|
159
|
private PolytomousKey key;
|
160
|
|
161
|
@XmlElementWrapper(name = "Children")
|
162
|
@XmlElement(name = "Child")
|
163
|
// @OrderColumn("sortIndex") //JPA 2.0 same as @IndexColumn
|
164
|
// @IndexColumn does not work because not every FeatureNode has a parent.
|
165
|
// But only NotNull will solve the problem (otherwise
|
166
|
// we will need a join table
|
167
|
// http://stackoverflow.com/questions/2956171/jpa-2-0-ordercolumn-annotation-in-hibernate-3-5
|
168
|
// http://docs.jboss.org/hibernate/stable/annotations/reference/en/html_single/#entity-hibspec-collection-extratype-indexbidir
|
169
|
// see also https://forum.hibernate.org/viewtopic.php?p=2392563
|
170
|
// http://opensource.atlassian.com/projects/hibernate/browse/HHH-4390
|
171
|
// reading works, but writing doesn't
|
172
|
//
|
173
|
@OrderColumn(name = "sortIndex", nullable=true) //, base = 0
|
174
|
@OrderBy("sortIndex")
|
175
|
@OneToMany(fetch = FetchType.LAZY, mappedBy = "parent")
|
176
|
@Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE })
|
177
|
private List<PolytomousKeyNode> children = new ArrayList<PolytomousKeyNode>();
|
178
|
|
179
|
|
180
|
|
181
|
@XmlElement(name = "Parent")
|
182
|
@XmlIDREF
|
183
|
@XmlSchemaType(name = "IDREF")
|
184
|
@Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE })
|
185
|
@ManyToOne(fetch = FetchType.LAZY, targetEntity = PolytomousKeyNode.class)
|
186
|
@JoinColumn(name = "parent_id" /*, insertable=false, updatable=false, nullable=false */)
|
187
|
private PolytomousKeyNode parent;
|
188
|
|
189
|
// see comment on children @IndexColumn
|
190
|
private Integer sortIndex;
|
191
|
|
192
|
@XmlElement(name = "Statement")
|
193
|
@XmlIDREF
|
194
|
@XmlSchemaType(name = "IDREF")
|
195
|
@ManyToOne(fetch = FetchType.LAZY)
|
196
|
@Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE })
|
197
|
private KeyStatement statement;
|
198
|
|
199
|
@XmlElement(name = "Question")
|
200
|
@XmlIDREF
|
201
|
@XmlSchemaType(name = "IDREF")
|
202
|
@ManyToOne(fetch = FetchType.LAZY)
|
203
|
@Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
|
204
|
private KeyStatement question;
|
205
|
|
206
|
@XmlElement(name = "Feature")
|
207
|
@XmlIDREF
|
208
|
@XmlSchemaType(name = "IDREF")
|
209
|
@ManyToOne(fetch = FetchType.LAZY)
|
210
|
// @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE}) remove cascade #5755
|
211
|
private Feature feature;
|
212
|
|
213
|
@XmlElement(name = "Taxon")
|
214
|
@XmlIDREF
|
215
|
@XmlSchemaType(name = "IDREF")
|
216
|
@ManyToOne(fetch = FetchType.LAZY)
|
217
|
@Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
|
218
|
private Taxon taxon;
|
219
|
|
220
|
// Refers to an entire key
|
221
|
// <code>this</code> node, a node usually belongs to a given key.
|
222
|
@XmlElement(name = "SubKey")
|
223
|
@XmlIDREF
|
224
|
@XmlSchemaType(name = "IDREF")
|
225
|
@ManyToOne(fetch = FetchType.LAZY)
|
226
|
@Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
|
227
|
private PolytomousKey subkey;
|
228
|
|
229
|
// Refers to an other node within this key or an other key
|
230
|
@XmlElement(name = "PolytomousKey")
|
231
|
@XmlIDREF
|
232
|
@XmlSchemaType(name = "IDREF")
|
233
|
@ManyToOne(fetch = FetchType.LAZY)
|
234
|
@Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
|
235
|
private PolytomousKeyNode otherNode;
|
236
|
|
237
|
private Integer nodeNumber = null;
|
238
|
|
239
|
// TODO should be available for each taxon/result
|
240
|
@XmlElement(name = "ModifyingText")
|
241
|
@XmlJavaTypeAdapter(MultilanguageTextAdapter.class)
|
242
|
@OneToMany(fetch = FetchType.LAZY)
|
243
|
@MapKeyJoinColumn(name="modifyingtext_mapkey_id")
|
244
|
@Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE })
|
245
|
private Map<Language, LanguageString> modifyingText = new HashMap<Language, LanguageString>();
|
246
|
|
247
|
// ************************** FACTORY ********************************/
|
248
|
|
249
|
/**
|
250
|
* Creates a new empty polytomous key node instance.
|
251
|
*/
|
252
|
public static PolytomousKeyNode NewInstance() {
|
253
|
return new PolytomousKeyNode();
|
254
|
}
|
255
|
|
256
|
/**
|
257
|
* Creates a new polytomous key node instance.
|
258
|
*
|
259
|
*/
|
260
|
public static PolytomousKeyNode NewInstance(String statement) {
|
261
|
PolytomousKeyNode result = new PolytomousKeyNode();
|
262
|
result.setStatement(KeyStatement.NewInstance(statement));
|
263
|
return result;
|
264
|
}
|
265
|
|
266
|
/**
|
267
|
* Creates a new polytomous key node instance.
|
268
|
*
|
269
|
*/
|
270
|
public static PolytomousKeyNode NewInstance(String statement,
|
271
|
String question, Taxon taxon, Feature feature) {
|
272
|
PolytomousKeyNode result = new PolytomousKeyNode();
|
273
|
result.setTaxon(taxon);
|
274
|
result.setStatement(KeyStatement.NewInstance(statement));
|
275
|
result.setQuestion(KeyStatement.NewInstance(question));
|
276
|
result.setFeature(feature);
|
277
|
return result;
|
278
|
}
|
279
|
|
280
|
// ************************** CONSTRUCTOR *****************************/
|
281
|
|
282
|
/**
|
283
|
* Class constructor: creates a new empty feature node instance.
|
284
|
*/
|
285
|
protected PolytomousKeyNode() {
|
286
|
super();
|
287
|
}
|
288
|
|
289
|
// ** ********************** GETTER / SETTER ******************************/
|
290
|
|
291
|
|
292
|
//see #4278 and #4200, alternatively can be private and use deproxy(this, PolytomousKeyNode.class)
|
293
|
protected void setSortIndex(Integer sortIndex) {
|
294
|
this.sortIndex = sortIndex;
|
295
|
}
|
296
|
|
297
|
/**
|
298
|
* @return
|
299
|
*/
|
300
|
public PolytomousKey getKey() {
|
301
|
return key;
|
302
|
}
|
303
|
|
304
|
/**
|
305
|
* @param key
|
306
|
*/
|
307
|
public void setKey(PolytomousKey key) {
|
308
|
this.key = key;
|
309
|
}
|
310
|
|
311
|
/**
|
312
|
* The node number is the number of the node within the key. This
|
313
|
* corresponds to the number for key choices in written keys.
|
314
|
*/
|
315
|
public Integer getNodeNumber() {
|
316
|
return nodeNumber;
|
317
|
}
|
318
|
|
319
|
/**
|
320
|
* Is computed automatically and therefore should not be set by the user.
|
321
|
*/
|
322
|
public void setNodeNumber(Integer nodeNumber) {
|
323
|
this.nodeNumber = nodeNumber;
|
324
|
}
|
325
|
|
326
|
/**
|
327
|
* Returns the taxon this node links to. This is usually the case when this
|
328
|
* node is a leaf.
|
329
|
*
|
330
|
* @return
|
331
|
* @see #setTaxon(Taxon)
|
332
|
* @see #getSubkey()
|
333
|
* @see #getChildren()
|
334
|
* @see #getOtherNode()
|
335
|
*/
|
336
|
public Taxon getTaxon() {
|
337
|
return taxon;
|
338
|
}
|
339
|
|
340
|
/**
|
341
|
* Sets the taxon this node links to. <BR>
|
342
|
* If a tax
|
343
|
*
|
344
|
* @param taxon
|
345
|
* @see #getTaxon()
|
346
|
*/
|
347
|
public void setTaxon(Taxon taxon) {
|
348
|
this.taxon = taxon;
|
349
|
}
|
350
|
|
351
|
/**
|
352
|
* @return
|
353
|
* @see #setSubkey(PolytomousKey)
|
354
|
* @see #getTaxon()
|
355
|
* @see #getChildren()
|
356
|
* @see #getOtherNode()
|
357
|
*/
|
358
|
public PolytomousKey getSubkey() {
|
359
|
return subkey;
|
360
|
}
|
361
|
|
362
|
/**
|
363
|
* @param subkey
|
364
|
* @see #getSubkey()
|
365
|
*/
|
366
|
public void setSubkey(PolytomousKey subkey) {
|
367
|
this.subkey = subkey;
|
368
|
}
|
369
|
|
370
|
/**
|
371
|
* @return
|
372
|
* @see #setOtherNode(PolytomousKeyNode)
|
373
|
* @see #getTaxon()
|
374
|
* @see #getChildren()
|
375
|
* @see #getSubkey()
|
376
|
*/
|
377
|
public PolytomousKeyNode getOtherNode() {
|
378
|
return otherNode;
|
379
|
}
|
380
|
|
381
|
/**
|
382
|
* @param otherNode
|
383
|
* @see #getOtherNode()
|
384
|
*/
|
385
|
public void setOtherNode(PolytomousKeyNode otherNode) {
|
386
|
this.otherNode = otherNode;
|
387
|
}
|
388
|
|
389
|
// TODO
|
390
|
public void setFeature(Feature feature) {
|
391
|
this.feature = feature;
|
392
|
}
|
393
|
|
394
|
public Feature getFeature() {
|
395
|
return feature;
|
396
|
}
|
397
|
|
398
|
/**
|
399
|
* Returns the parent node of <code>this</code> child.
|
400
|
*
|
401
|
* @return
|
402
|
*/
|
403
|
public PolytomousKeyNode getParent() {
|
404
|
return parent;
|
405
|
}
|
406
|
|
407
|
/**
|
408
|
* For bidirectional use only !
|
409
|
*
|
410
|
* @param parent
|
411
|
*/
|
412
|
protected void setParent(PolytomousKeyNode parent) {
|
413
|
PolytomousKeyNode oldParent = this.parent;
|
414
|
if (oldParent != null){
|
415
|
if (oldParent.getChildren().contains(this)){
|
416
|
oldParent.removeChild(this);
|
417
|
}
|
418
|
}
|
419
|
this.parent = parent;
|
420
|
|
421
|
}
|
422
|
|
423
|
/**
|
424
|
* Returns the (ordered) list of feature nodes which are children nodes of
|
425
|
* <i>this</i> feature node.
|
426
|
*/
|
427
|
public List<PolytomousKeyNode> getChildren() {
|
428
|
HHH_9751_Util.removeAllNull(children);
|
429
|
return children;
|
430
|
}
|
431
|
|
432
|
/**
|
433
|
* Adds the given polytomous key node at the end of the list of children of
|
434
|
* <i>this</i> polytomous key node.
|
435
|
*
|
436
|
* @param child
|
437
|
* the feature node to be added
|
438
|
* @see #getChildren()
|
439
|
* @see #setChildren(List)
|
440
|
* @see #addChild(PolytomousKeyNode, int)
|
441
|
* @see #removeChild(PolytomousKeyNode)
|
442
|
* @see #removeChild(int)
|
443
|
*/
|
444
|
public void addChild(PolytomousKeyNode child) {
|
445
|
addChild(child, children.size());
|
446
|
}
|
447
|
|
448
|
/**
|
449
|
* Inserts the given child node in the list of children of <i>this</i>
|
450
|
* polytomous key node at the given (index + 1) position. If the given index
|
451
|
* is out of bounds an exception will be thrown.<BR>
|
452
|
*
|
453
|
* @param child
|
454
|
* the polytomous key node to be added
|
455
|
* @param index
|
456
|
* the integer indicating the position at which the child should
|
457
|
* be added
|
458
|
* @see #getChildren()
|
459
|
* @see #setChildren(List)
|
460
|
* @see #addChild(PolytomousKeyNode)
|
461
|
* @see #removeChild(PolytomousKeyNode)
|
462
|
* @see #removeChild(int)
|
463
|
*/
|
464
|
public void addChild(PolytomousKeyNode child, int index) {
|
465
|
if (index < 0 || index > children.size() + 1) {
|
466
|
throw new IndexOutOfBoundsException("Wrong index: " + index);
|
467
|
}
|
468
|
HHH_9751_Util.removeAllNull(children);
|
469
|
|
470
|
if(nodeNumber == null) {
|
471
|
nodeNumber = getMaxNodeNumberFromRoot() + 1;
|
472
|
}
|
473
|
|
474
|
|
475
|
children.add(index, child);
|
476
|
child.setKey(this.getKey());
|
477
|
|
478
|
// TODO workaround (see sortIndex doc)
|
479
|
for (int i = 0; i < children.size(); i++) {
|
480
|
children.get(i).setSortIndex(i);
|
481
|
}
|
482
|
child.setSortIndex(index);
|
483
|
child.setParent(this);
|
484
|
}
|
485
|
|
486
|
|
487
|
|
488
|
/**
|
489
|
* Removes the given polytomous key node from the list of
|
490
|
* {@link #getChildren() children} of <i>this</i> polytomous key node.
|
491
|
*
|
492
|
* @param child
|
493
|
* the feature node which should be removed
|
494
|
* @see #getChildren()
|
495
|
* @see #addChild(PolytomousKeyNode, int)
|
496
|
* @see #addChild(PolytomousKeyNode)
|
497
|
* @see #removeChild(int)
|
498
|
*/
|
499
|
public void removeChild(PolytomousKeyNode child) {
|
500
|
HHH_9751_Util.removeAllNull(children);
|
501
|
int index = children.indexOf(child);
|
502
|
if (index >= 0) {
|
503
|
removeChild(index);
|
504
|
}
|
505
|
}
|
506
|
|
507
|
|
508
|
|
509
|
/**
|
510
|
* Removes the feature node placed at the given (index + 1) position from
|
511
|
* the list of {@link #getChildren() children} of <i>this</i> feature node.
|
512
|
* If the given index is out of bounds no child will be removed.
|
513
|
*
|
514
|
* @param index
|
515
|
* the integer indicating the position of the feature node to be
|
516
|
* removed
|
517
|
* @see #getChildren()
|
518
|
* @see #addChild(PolytomousKeyNode, int)
|
519
|
* @see #addChild(PolytomousKeyNode)
|
520
|
* @see #removeChild(PolytomousKeyNode)
|
521
|
*/
|
522
|
public void removeChild(int index) {
|
523
|
PolytomousKeyNode child = children.get(index);
|
524
|
if (child != null) {
|
525
|
children.remove(index);
|
526
|
child.setParent(null);
|
527
|
// TODO workaround (see sortIndex doc)
|
528
|
for (int i = 0; i < children.size(); i++) {
|
529
|
PolytomousKeyNode childAt = children.get(i);
|
530
|
childAt.setSortIndex(i);
|
531
|
}
|
532
|
child.setSortIndex(null);
|
533
|
child.setNodeNumber(null);
|
534
|
}
|
535
|
refreshNodeNumbering();
|
536
|
}
|
537
|
|
538
|
// **************************** METHODS ************************************/
|
539
|
|
540
|
/**
|
541
|
* Returns the current maximum value of the node number in the entire key
|
542
|
* starting from the root.
|
543
|
*
|
544
|
* @return
|
545
|
*/
|
546
|
private int getMaxNodeNumberFromRoot() {
|
547
|
PolytomousKeyNode rootKeyNode = this.getKey().getRoot();
|
548
|
int rootNumber = this.getKey().getStartNumber();
|
549
|
return getMaxNodeNumber(rootNumber, rootKeyNode);
|
550
|
}
|
551
|
|
552
|
/**
|
553
|
* Returns the current maximum value of the node number in the entire key
|
554
|
* starting from the given key node, comparing with a given max value as input.
|
555
|
*
|
556
|
* @return
|
557
|
*/
|
558
|
private int getMaxNodeNumber(int maxNumber, PolytomousKeyNode parent) {
|
559
|
if (parent.getNodeNumber() != null) {
|
560
|
maxNumber = (maxNumber < parent.getNodeNumber()) ? parent.getNodeNumber() : maxNumber;
|
561
|
for (PolytomousKeyNode child : parent.getChildren()) {
|
562
|
if (parent == child){
|
563
|
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.");
|
564
|
}else{
|
565
|
maxNumber = getMaxNodeNumber(maxNumber, child);
|
566
|
}
|
567
|
}
|
568
|
}
|
569
|
return maxNumber;
|
570
|
}
|
571
|
|
572
|
/**
|
573
|
* Refresh numbering of key nodes starting from root.
|
574
|
*
|
575
|
*/
|
576
|
public void refreshNodeNumbering() {
|
577
|
updateNodeNumbering(getKey().getRoot(), getKey().getStartNumber());
|
578
|
}
|
579
|
|
580
|
/**
|
581
|
* Recursively (depth-first) refresh numbering of key nodes starting from the given key node,
|
582
|
* starting with a given node number.
|
583
|
*
|
584
|
* @return new starting node number value
|
585
|
*/
|
586
|
private int updateNodeNumbering(PolytomousKeyNode node,int nodeN) {
|
587
|
int newNodeN = nodeN;
|
588
|
if (node.isLeaf()) {
|
589
|
node.setNodeNumber(null);
|
590
|
} else {
|
591
|
node.setNodeNumber(nodeN);
|
592
|
newNodeN++;
|
593
|
List<PolytomousKeyNode> children = node.getChildren();
|
594
|
HHH_9751_Util.removeAllNull(children);
|
595
|
for (PolytomousKeyNode child : children) {
|
596
|
if (node == child){
|
597
|
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.");
|
598
|
}else{
|
599
|
newNodeN = updateNodeNumbering(child, newNodeN);
|
600
|
}
|
601
|
}
|
602
|
}
|
603
|
return newNodeN;
|
604
|
}
|
605
|
|
606
|
|
607
|
|
608
|
|
609
|
/**
|
610
|
* Returns the feature node placed at the given (childIndex + 1) position
|
611
|
* within the list of {@link #getChildren() children} of <i>this</i> feature
|
612
|
* node. If the given index is out of bounds no child will be returned.
|
613
|
*
|
614
|
* @param childIndex
|
615
|
* the integer indicating the position of the feature node
|
616
|
* @see #getChildren()
|
617
|
* @see #addChild(PolytomousKeyNode, int)
|
618
|
* @see #removeChild(int)
|
619
|
*/
|
620
|
public PolytomousKeyNode getChildAt(int childIndex) {
|
621
|
return children.get(childIndex);
|
622
|
}
|
623
|
|
624
|
/**
|
625
|
* Returns the number of children nodes of <i>this</i> feature node.
|
626
|
*
|
627
|
* @see #getChildren()
|
628
|
*/
|
629
|
@Transient
|
630
|
public int childCount() {
|
631
|
return children.size();
|
632
|
}
|
633
|
|
634
|
/**
|
635
|
* Returns the integer indicating the position of the given feature node
|
636
|
* within the list of {@link #getChildren() children} of <i>this</i> feature
|
637
|
* node. If the list does not contain this node then -1 will be returned.
|
638
|
*
|
639
|
* @param node
|
640
|
* the feature node the position of which is being searched
|
641
|
* @see #addChild(PolytomousKeyNode, int)
|
642
|
* @see #removeChild(int)
|
643
|
*/
|
644
|
public int getIndex(PolytomousKeyNode node) {
|
645
|
if (!children.contains(node)) {
|
646
|
return -1;
|
647
|
} else {
|
648
|
return children.indexOf(node);
|
649
|
}
|
650
|
}
|
651
|
|
652
|
/**
|
653
|
* Returns the boolean value indicating if <i>this</i> feature node has
|
654
|
* children (false) or not (true). A node without children is at the
|
655
|
* bottommost level of a tree and is called a leaf.
|
656
|
*
|
657
|
* @see #getChildren()
|
658
|
* @see #getChildCount()
|
659
|
*/
|
660
|
@Transient
|
661
|
public boolean isLeaf() {
|
662
|
return children.size() < 1;
|
663
|
}
|
664
|
|
665
|
// ** ********************** QUESTIONS AND STATEMENTS ************************/
|
666
|
|
667
|
/**
|
668
|
* Returns the statement for <code>this</code> PolytomousKeyNode. When coming
|
669
|
* from the parent node the user needs to agree with the statement (and disagree
|
670
|
* with all statements of sibling nodes) to follow <code>this</code> node.<BR>
|
671
|
* The statement may stand alone (standard in classical keys) or it may be
|
672
|
* either the answer to the {@link #getQuestion() question} or the
|
673
|
* value for the {@link #getFeature() feature} of the parent node.
|
674
|
*
|
675
|
* @return the statement
|
676
|
* @see #getQuestion()
|
677
|
*/
|
678
|
public KeyStatement getStatement() {
|
679
|
return statement;
|
680
|
}
|
681
|
|
682
|
/**
|
683
|
* This is a convenience method to set the statement text for this node in
|
684
|
* the given language. <BR>
|
685
|
* If no statement exists yet a new statement is created. <BR>
|
686
|
* If a statement text in the given language exists already it is
|
687
|
* overwritten and the old text is returned. If language is
|
688
|
* <code>null</code> the default language is used instead.
|
689
|
*
|
690
|
* @param text
|
691
|
* the statement text
|
692
|
* @param language
|
693
|
* the language of the statement text
|
694
|
* @return the old statement text in the given language as LanguageString
|
695
|
*/
|
696
|
public LanguageString addStatementText(String text, Language language) {
|
697
|
if (language == null) {
|
698
|
language = Language.DEFAULT();
|
699
|
}
|
700
|
if (this.statement == null) {
|
701
|
setStatement(KeyStatement.NewInstance());
|
702
|
}
|
703
|
return getStatement().putLabel(language, text);
|
704
|
}
|
705
|
|
706
|
/**
|
707
|
* @param statement
|
708
|
* @see #getStatement()
|
709
|
*/
|
710
|
public void setStatement(KeyStatement statement) {
|
711
|
this.statement = statement;
|
712
|
}
|
713
|
|
714
|
/**
|
715
|
* Returns the question for <code>this</code> PolytomousKeyNode. <BR>
|
716
|
* A question is answered by statements in leads below this tree node.
|
717
|
* Questions are optional and are usually empty in traditional keys.
|
718
|
*
|
719
|
* @return the question
|
720
|
* @see #getStatement()
|
721
|
*/
|
722
|
public KeyStatement getQuestion() {
|
723
|
return question;
|
724
|
}
|
725
|
|
726
|
/**
|
727
|
* This is a convenience method to sets the question text for this node in
|
728
|
* the given language. <BR>
|
729
|
* If no question exists yet a new question is created. <BR>
|
730
|
* If a question text in the given language exists already it is overwritten
|
731
|
* and the old text is returned. If language is <code>null</code> the
|
732
|
* default language is used instead.
|
733
|
*
|
734
|
* @param text
|
735
|
* @param language
|
736
|
* @return
|
737
|
*/
|
738
|
public LanguageString addQuestionText(String text, Language language) {
|
739
|
if (language == null) {
|
740
|
language = Language.DEFAULT();
|
741
|
}
|
742
|
if (this.question == null) {
|
743
|
setQuestion(KeyStatement.NewInstance());
|
744
|
}
|
745
|
return getQuestion().putLabel(language, text);
|
746
|
}
|
747
|
|
748
|
/**
|
749
|
* @param question
|
750
|
* @see #getQuestion()
|
751
|
*/
|
752
|
public void setQuestion(KeyStatement question) {
|
753
|
this.question = question;
|
754
|
}
|
755
|
|
756
|
// **************** modifying text ***************************************
|
757
|
|
758
|
/**
|
759
|
* Returns the {@link MultilanguageText} like "an unusual form of",
|
760
|
* commenting the determined taxon. That is a modifyingText may by used to
|
761
|
* comment or to constraint the decision step represented by the edge
|
762
|
* leading to <i>this</i> node
|
763
|
* <p>
|
764
|
* All {@link LanguageString language strings} contained in the
|
765
|
* multilanguage texts should all have the same meaning.<BR>
|
766
|
*/
|
767
|
public Map<Language, LanguageString> getModifyingText() {
|
768
|
return this.modifyingText;
|
769
|
}
|
770
|
|
771
|
/**
|
772
|
* See {@link #getModifyingText}
|
773
|
*
|
774
|
* @param description
|
775
|
* the language string describing the validity in a particular
|
776
|
* language
|
777
|
* @see #getModifyingText()
|
778
|
* @see #putModifyingText(Language, String)
|
779
|
* @deprecated should follow the put semantic of maps, this method will be
|
780
|
* removed in v4.0 Use the
|
781
|
* {@link #putModifyingText(LanguageString) putModifyingText}
|
782
|
* method instead
|
783
|
*/
|
784
|
@Deprecated
|
785
|
public LanguageString addModifyingText(LanguageString description) {
|
786
|
return this.putModifyingText(description);
|
787
|
}
|
788
|
|
789
|
/**
|
790
|
* See {@link #getModifyingText}
|
791
|
*
|
792
|
* @param description
|
793
|
* the language string describing the validity in a particular
|
794
|
* language
|
795
|
* @see #getModifyingText()
|
796
|
* @see #putModifyingText(Language, String)
|
797
|
*/
|
798
|
public LanguageString putModifyingText(LanguageString description) {
|
799
|
return this.modifyingText.put(description.getLanguage(), description);
|
800
|
}
|
801
|
|
802
|
/**
|
803
|
* See {@link #getModifyingText}
|
804
|
*
|
805
|
* @param text
|
806
|
* the string describing the validity in a particular language
|
807
|
* @param language
|
808
|
* the language in which the text string is formulated
|
809
|
* @see #getModifyingText()
|
810
|
* @see #putModifyingText(LanguageString)
|
811
|
* @deprecated should follow the put semantic of maps, this method will be
|
812
|
* removed in v4.0 Use the
|
813
|
* {@link #putModifyingText(Language, String) putModifyingText}
|
814
|
* method instead
|
815
|
*/
|
816
|
@Deprecated
|
817
|
public LanguageString addModifyingText(String text, Language language) {
|
818
|
return this.putModifyingText(language, text);
|
819
|
}
|
820
|
|
821
|
/**
|
822
|
* See {@link #getModifyingText}
|
823
|
*
|
824
|
* @param text
|
825
|
* the string describing the validity in a particular language
|
826
|
* @param language
|
827
|
* the language in which the text string is formulated
|
828
|
* @see #getModifyingText()
|
829
|
* @see #putModifyingText(LanguageString)
|
830
|
*/
|
831
|
public LanguageString putModifyingText(Language language, String text) {
|
832
|
return this.modifyingText.put(language,
|
833
|
LanguageString.NewInstance(text, language));
|
834
|
}
|
835
|
|
836
|
/**
|
837
|
* See {@link #getModifyingText}
|
838
|
*
|
839
|
* @param language
|
840
|
* the language in which the language string to be removed has
|
841
|
* been formulated
|
842
|
* @see #getModifyingText()
|
843
|
*/
|
844
|
public LanguageString removeModifyingText(Language language) {
|
845
|
return this.modifyingText.remove(language);
|
846
|
}
|
847
|
|
848
|
|
849
|
// *********************** CLONE ********************************************************/
|
850
|
|
851
|
/**
|
852
|
* Clones <i>this</i> PolytomousKeyNode. This is a shortcut that enables to
|
853
|
* create a new instance that differs only slightly from <i>this</i>
|
854
|
* PolytomousKeyNode by modifying only some of the attributes. The parent,
|
855
|
* the feature and the key are the are the same as for the original feature
|
856
|
* node the children are removed.
|
857
|
*
|
858
|
* @see eu.etaxonomy.cdm.model.common.VersionableEntity#clone()
|
859
|
* @see java.lang.Object#clone()
|
860
|
*/
|
861
|
@Override
|
862
|
public Object clone() {
|
863
|
PolytomousKeyNode result;
|
864
|
try {
|
865
|
result = (PolytomousKeyNode) super.clone();
|
866
|
result.children = new ArrayList<PolytomousKeyNode>();
|
867
|
|
868
|
result.modifyingText = new HashMap<Language, LanguageString>();
|
869
|
for (Entry<Language, LanguageString> entry : this.modifyingText
|
870
|
.entrySet()) {
|
871
|
result.putModifyingText(entry.getValue());
|
872
|
}
|
873
|
|
874
|
return result;
|
875
|
} catch (CloneNotSupportedException e) {
|
876
|
logger.warn("Object does not implement cloneable");
|
877
|
e.printStackTrace();
|
878
|
return null;
|
879
|
}
|
880
|
}
|
881
|
|
882
|
/**
|
883
|
*
|
884
|
*/
|
885
|
public void removeTaxon() {
|
886
|
this.taxon = null;
|
887
|
|
888
|
}
|
889
|
|
890
|
|
891
|
}
|