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