Fixes a bug in PolytomousKeyNode.removeChild(int)
[cdmlib.git] / cdmlib-model / src / main / java / eu / etaxonomy / cdm / model / description / PolytomousKey.java
1 // $Id$
2 /**
3 * Copyright (C) 2007 EDIT
4 * European Distributed Institute of Taxonomy
5 * http://www.e-taxonomy.eu
6 *
7 * The contents of this file are subject to the Mozilla Public License Version 1.1
8 * See LICENSE.TXT at the top of this package for the full license terms.
9 */
10
11 package eu.etaxonomy.cdm.model.description;
12
13 import java.io.PrintStream;
14 import java.util.HashSet;
15 import java.util.Set;
16
17 import javax.persistence.Entity;
18 import javax.persistence.FetchType;
19 import javax.persistence.JoinColumn;
20 import javax.persistence.JoinTable;
21 import javax.persistence.ManyToMany;
22 import javax.persistence.OneToOne;
23 import javax.validation.constraints.NotNull;
24 import javax.xml.bind.annotation.XmlAccessType;
25 import javax.xml.bind.annotation.XmlAccessorType;
26 import javax.xml.bind.annotation.XmlElement;
27 import javax.xml.bind.annotation.XmlElementWrapper;
28 import javax.xml.bind.annotation.XmlIDREF;
29 import javax.xml.bind.annotation.XmlRootElement;
30 import javax.xml.bind.annotation.XmlSchemaType;
31 import javax.xml.bind.annotation.XmlType;
32
33 import org.apache.log4j.Logger;
34 import org.hibernate.annotations.Cascade;
35 import org.hibernate.annotations.CascadeType;
36 import org.hibernate.envers.Audited;
37 import org.hibernate.search.annotations.Indexed;
38 import org.hibernate.tool.hbm2x.StringUtils;
39
40 import eu.etaxonomy.cdm.common.CdmUtils;
41 import eu.etaxonomy.cdm.model.common.IdentifiableEntity;
42 import eu.etaxonomy.cdm.model.common.Language;
43 import eu.etaxonomy.cdm.model.location.NamedArea;
44 import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationBase;
45 import eu.etaxonomy.cdm.model.taxon.Taxon;
46 import eu.etaxonomy.cdm.strategy.generate.PolytomousKeyGenerator;
47
48 /**
49 * The class allowing the representation of single-access fixed dichotomous or
50 * polytomous decision keys used to identify {@link SpecimenOrObservationBase
51 * specimens or observations} (this means to assign {@link Taxon taxa} to). The
52 * key may be written by an author or may be generated by the
53 * {@link PolytomousKeyGenerator}. The different paths to the taxa are expressed
54 * by a decision graph consisting of {@link PolytomousKeyNode
55 * PolytomousKeyNodes}. The root node of such graph is accessible by
56 * {@link #getRoot()}. Please refer to {@link PolytomousKeyNode} for detailed
57 * documentation on this decision graph please
58 *
59 * @author h.fradin
60 * @created 13.08.2009
61 * @version 1.0
62 *
63 * @author a.mueller
64 * @created 08.11.2010
65 * @version 2.0
66 */
67 @XmlAccessorType(XmlAccessType.FIELD)
68 @XmlType(name = "PolytomousKey", propOrder = { "coveredTaxa", "taxonomicScope",
69 "geographicalScope", "scopeRestrictions", "root" })
70 @XmlRootElement(name = "PolytomousKey")
71 @Entity
72 @Indexed(index = "eu.etaxonomy.cdm.model.media.FeatureTree")
73 @Audited
74 public class PolytomousKey extends IdentifiableEntity implements
75 IIdentificationKey {
76 private static final long serialVersionUID = -3368243754557343942L;
77 @SuppressWarnings("unused")
78 private static final Logger logger = Logger.getLogger(PolytomousKey.class);
79
80 @XmlElementWrapper(name = "CoveredTaxa")
81 @XmlElement(name = "CoveredTaxon")
82 @XmlIDREF
83 @XmlSchemaType(name = "IDREF")
84 @ManyToMany(fetch = FetchType.LAZY)
85 @NotNull
86 private Set<Taxon> coveredTaxa = new HashSet<Taxon>();
87
88 @XmlElementWrapper(name = "TaxonomicScope")
89 @XmlElement(name = "Taxon")
90 @XmlIDREF
91 @XmlSchemaType(name = "IDREF")
92 @ManyToMany(fetch = FetchType.LAZY)
93 @JoinTable(name = "PolytomousKey_Taxon", joinColumns = @JoinColumn(name = "polytomousKey_id"), inverseJoinColumns = @JoinColumn(name = "taxon_id"))
94 @NotNull
95 private Set<Taxon> taxonomicScope = new HashSet<Taxon>();
96
97 @XmlElementWrapper(name = "GeographicalScope")
98 @XmlElement(name = "Area")
99 @XmlIDREF
100 @XmlSchemaType(name = "IDREF")
101 @ManyToMany(fetch = FetchType.LAZY)
102 @JoinTable(name = "PolytomousKey_NamedArea")
103 @NotNull
104 private Set<NamedArea> geographicalScope = new HashSet<NamedArea>();
105
106 @XmlElementWrapper(name = "ScopeRestrictions")
107 @XmlElement(name = "Restriction")
108 @XmlIDREF
109 @XmlSchemaType(name = "IDREF")
110 @ManyToMany(fetch = FetchType.LAZY)
111 @JoinTable(name = "PolytomousKey_Scope")
112 @NotNull
113 private Set<Scope> scopeRestrictions = new HashSet<Scope>();
114
115 @XmlElement(name = "Root")
116 @OneToOne(fetch = FetchType.LAZY)
117 @Cascade({ CascadeType.SAVE_UPDATE, CascadeType.MERGE })
118 private PolytomousKeyNode root;
119
120 // ******************************** STATIC METHODS
121 // ********************************/
122
123 /**
124 * Creates a new empty identification multi-access key instance.
125 */
126 public static PolytomousKey NewInstance() {
127 return new PolytomousKey();
128 }
129
130 /**
131 * Creates a new empty identification polytomous key instance.
132 */
133 public static PolytomousKey NewTitledInstance(String title) {
134 PolytomousKey result = new PolytomousKey();
135 result.setTitleCache(title, true);
136 return result;
137 }
138
139 // ************************** CONSTRUCTOR
140 // *******************************************/
141
142 /**
143 * Class constructor: creates a new empty multi-access key instance.
144 */
145 protected PolytomousKey() {
146 super();
147 root = PolytomousKeyNode.NewRootInstance();
148 root.setKey(this);
149 }
150
151 // ************************ GETTER/ SETTER
152
153 /**
154 * Returns the topmost {@link PolytomousKeyNode polytomous key node} (root
155 * node) of <i>this</i> polytomous key. The root node does not have any
156 * parent. Since polytomous key nodes recursively point to their child nodes
157 * the complete polytomous key is defined by its root node.
158 */
159 public PolytomousKeyNode getRoot() {
160 return root;
161 }
162
163 /**
164 * This method should be used by Hibernate only. If we want to make this
165 * method public we have to think about biderionality and also what should
166 * happen with the old root node.
167 *
168 * @see #getRoot()
169 */
170 public void setRoot(PolytomousKeyNode root) {
171 this.root = root;
172 }
173
174 /**
175 * Returns the set of possible {@link Taxon taxa} corresponding to
176 * <i>this</i> identification key.
177 */
178 public Set<Taxon> getCoveredTaxa() {
179 if (coveredTaxa == null) {
180 this.coveredTaxa = new HashSet<Taxon>();
181 }
182 return coveredTaxa;
183 }
184
185 /**
186 * @see #getCoveredTaxa()
187 */
188 protected void setCoveredTaxa(Set<Taxon> coveredTaxa) {
189 this.coveredTaxa = coveredTaxa;
190 }
191
192 /**
193 * Adds a {@link Taxon taxa} to the set of {@link #getCoveredTaxa() covered
194 * taxa} corresponding to <i>this</i> identification key.
195 *
196 * @param taxon
197 * the taxon to be added to <i>this</i> identification key
198 * @see #getCoveredTaxa()
199 */
200 public void addCoveredTaxon(Taxon taxon) {
201 this.coveredTaxa.add(taxon);
202 }
203
204 /**
205 * Removes one element from the set of {@link #getCoveredTaxa() covered
206 * taxa} corresponding to <i>this</i> identification key.
207 *
208 * @param taxon
209 * the taxon which should be removed
210 * @see #getCoveredTaxa()
211 * @see #addCoveredTaxon(Taxon)
212 */
213 public void removeCoveredTaxon(Taxon taxon) {
214 this.coveredTaxa.remove(taxon);
215 }
216
217 /**
218 * Returns the set of {@link NamedArea named areas} indicating the
219 * geospatial data where <i>this</i> identification key is valid.
220 */
221 public Set<NamedArea> getGeographicalScope() {
222 if (geographicalScope == null) {
223 this.geographicalScope = new HashSet<NamedArea>();
224 }
225 return geographicalScope;
226 }
227
228 /**
229 * Adds a {@link NamedArea geoScope} to the set of {@link #getGeoScopes()
230 * geogspatial scopes} corresponding to <i>this</i> identification key.
231 *
232 * @param geoScope
233 * the named area to be added to <i>this</i> identification key
234 * @see #getGeoScopes()
235 */
236 public void addGeographicalScope(NamedArea geoScope) {
237 this.geographicalScope.add(geoScope);
238 }
239
240 /**
241 * Removes one element from the set of {@link #getGeoScopes() geogspatial
242 * scopes} corresponding to <i>this</i> identification key.
243 *
244 * @param geoScope
245 * the named area which should be removed
246 * @see #getGeoScopes()
247 * @see #addGeoScope(NamedArea)
248 */
249 public void removeGeographicalScope(NamedArea geoScope) {
250 this.geographicalScope.remove(geoScope);
251 }
252
253 /**
254 * Returns the set of {@link Taxon taxa} that define the taxonomic scope of
255 * <i>this</i> identification key
256 */
257 public Set<Taxon> getTaxonomicScope() {
258 if (taxonomicScope == null) {
259 this.taxonomicScope = new HashSet<Taxon>();
260 }
261 return taxonomicScope;
262 }
263
264 /**
265 * Adds a {@link Taxon taxa} to the set of {@link #getTaxonomicScope()
266 * taxonomic scopes} corresponding to <i>this</i> identification key.
267 *
268 * @param taxon
269 * the taxon to be added to <i>this</i> identification key
270 * @see #getTaxonomicScope()
271 */
272 public void addTaxonomicScope(Taxon taxon) {
273 this.taxonomicScope.add(taxon);
274 }
275
276 /**
277 * Removes one element from the set of {@link #getTaxonomicScope() taxonomic
278 * scopes} corresponding to <i>this</i> identification key.
279 *
280 * @param taxon
281 * the taxon which should be removed
282 * @see #getTaxonomicScope()
283 * @see #addTaxonomicScope(Taxon)
284 */
285 public void removeTaxonomicScope(Taxon taxon) {
286 this.taxonomicScope.remove(taxon);
287 }
288
289 /**
290 * Returns the set of {@link Scope scope restrictions} corresponding to
291 * <i>this</i> identification key
292 */
293 public Set<Scope> getScopeRestrictions() {
294 if (scopeRestrictions == null) {
295 this.scopeRestrictions = new HashSet<Scope>();
296 }
297 return scopeRestrictions;
298 }
299
300 /**
301 * Adds a {@link Scope scope restriction} to the set of
302 * {@link #getScopeRestrictions() scope restrictions} corresponding to
303 * <i>this</i> identification key.
304 *
305 * @param scopeRestriction
306 * the scope restriction to be added to <i>this</i>
307 * identification key
308 * @see #getScopeRestrictions()
309 */
310 public void addScopeRestriction(Scope scopeRestriction) {
311 this.scopeRestrictions.add(scopeRestriction);
312 }
313
314 /**
315 * Removes one element from the set of {@link #getScopeRestrictions() scope
316 * restrictions} corresponding to <i>this</i> identification key.
317 *
318 * @param scopeRestriction
319 * the scope restriction which should be removed
320 * @see #getScopeRestrictions()
321 * @see #addScopeRestriction(Scope)
322 */
323 public void removeScopeRestriction(Scope scopeRestriction) {
324 this.scopeRestrictions.remove(scopeRestriction);
325 }
326
327 // ******************** toString *****************************************/
328
329 private class IntegerObject {
330 int number = 0;
331
332 int inc() {
333 return number++;
334 };
335
336 @Override
337 public String toString() {
338 return String.valueOf(number);
339 }
340 }
341
342 public String print(PrintStream stream) {
343 String title = this.getTitleCache() + "\n";
344 String strPrint = title;
345
346 if (stream != null) {
347 stream.print(title);
348 }
349
350 PolytomousKeyNode root = this.getRoot();
351 strPrint += printNode(root, null, " ", stream);
352 return strPrint;
353 }
354
355 /**
356 * TODO this is a preliminary implementation
357 *
358 * @param node
359 * @param identation
360 * @param no
361 * @param myNumber
362 * @param stream
363 * @return
364 */
365 private String printNode(PolytomousKeyNode node, PolytomousKeyNode parent2,
366 String identation, PrintStream stream) {
367 String separator = ", ";
368
369 String result = identation + node.getNodeNumber() + ". ";
370 if (node != null) {
371 // key choice
372 String question = null;
373 String feature = null;
374 if (node.getQuestion() != null) {
375 question = node.getQuestion().getLabelText(Language.DEFAULT());
376 }
377 if (node.getFeature() != null) {
378 feature = node.getFeature().getLabel(Language.DEFAULT());
379 }
380 result += CdmUtils.concat(" - ", question, feature) + "\n";
381 ;
382
383 // Leads
384 char nextCounter = 'a';
385 for (PolytomousKeyNode child : node.getChildren()) {
386 String leadNumber = String.valueOf(nextCounter++);
387 if (child.getStatement() != null) {
388 String statement = child.getStatement().getLabelText(
389 Language.DEFAULT());
390 result += identation + " " + leadNumber + ") "
391 + (statement == null ? "" : (statement));
392 result += " ... ";
393 // child node
394 if (!child.isLeaf()) {
395 result += child.getNodeNumber() + separator;
396 }
397 // taxon
398 if (child.getTaxon() != null) {
399 String strTaxon = "";
400 if (child.getTaxon().getName() != null) {
401 strTaxon = child.getTaxon().getName()
402 .getTitleCache();
403 } else {
404 strTaxon = child.getTaxon().getTitleCache();
405 }
406 result += strTaxon + separator;
407 }
408 // subkey
409 if (child.getSubkey() != null) {
410 String subkey = child.getSubkey().getTitleCache();
411 result += subkey + separator;
412 }
413 // other node
414 if (child.getOtherNode() != null) {
415 PolytomousKeyNode otherNode = child.getOtherNode();
416 String otherNodeString = null;
417 if (child.getKey().equals(otherNode.getKey())) {
418 otherNodeString = String.valueOf(otherNode
419 .getNodeNumber());
420 } else {
421 otherNodeString = otherNode.getKey() + " "
422 + otherNode.getNodeNumber();
423 }
424 result += otherNodeString + separator;
425 }
426
427 result = StringUtils.chompLast(result, separator);
428 result += "\n";
429 }
430 }
431
432 if (stream != null) {
433 stream.print(result);
434 }
435 for (PolytomousKeyNode child : node.getChildren()) {
436 if (!child.isLeaf()) {
437 result += printNode(child, node, identation + "", stream);
438 }
439 }
440 }
441 return result;
442 }
443
444 //
445 // public List<PolytomousKeyNode> getChildren() {
446 // return getRoot().getChildren();
447 // }
448
449 // *********************** CLONE
450 // ********************************************************/
451
452 /**
453 * Clones <i>this</i> PolytomousKey. This is a shortcut that enables to
454 * create a new instance that differs only slightly from <i>this</i>
455 * PolytomousKey by modifying only some of the attributes.
456 *
457 * @see eu.etaxonomy.cdm.model.common.IdentifiableEntity#clone()
458 * @see java.lang.Object#clone()
459 */
460 @Override
461 public Object clone() {
462 PolytomousKey result;
463
464 try {
465 result = (PolytomousKey) super.clone();
466
467 result.coveredTaxa = new HashSet<Taxon>();
468 for (Taxon taxon : this.coveredTaxa) {
469 result.addCoveredTaxon(taxon);
470 }
471
472 result.geographicalScope = new HashSet<NamedArea>();
473 for (NamedArea area : this.geographicalScope) {
474 result.addGeographicalScope(area);
475 }
476
477 result.root = (PolytomousKeyNode) this.root.clone();
478
479 result.scopeRestrictions = new HashSet<Scope>();
480 for (Scope scope : this.scopeRestrictions) {
481 result.addScopeRestriction(scope);
482 }
483
484 result.taxonomicScope = new HashSet<Taxon>();
485 for (Taxon taxon : this.taxonomicScope) {
486 result.addTaxonomicScope(taxon);
487 }
488
489 return result;
490
491 } catch (CloneNotSupportedException e) {
492 logger.warn("Object does not implement cloneable");
493 e.printStackTrace();
494 return null;
495 }
496
497 }
498
499 }