merge-update from trunk
[cdmlib.git] / cdmlib-model / src / main / java / eu / etaxonomy / cdm / model / taxon / Classification.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.taxon;
12
13 import java.util.ArrayList;
14 import java.util.HashMap;
15 import java.util.HashSet;
16 import java.util.Iterator;
17 import java.util.List;
18 import java.util.Map;
19 import java.util.Set;
20
21 import javax.persistence.Entity;
22 import javax.persistence.FetchType;
23 import javax.persistence.JoinColumn;
24 import javax.persistence.JoinTable;
25 import javax.persistence.ManyToMany;
26 import javax.persistence.ManyToOne;
27 import javax.persistence.MapKeyJoinColumn;
28 import javax.persistence.OneToMany;
29 import javax.persistence.OneToOne;
30 import javax.persistence.Transient;
31 import javax.validation.constraints.NotNull;
32 import javax.xml.bind.annotation.XmlAccessType;
33 import javax.xml.bind.annotation.XmlAccessorType;
34 import javax.xml.bind.annotation.XmlElement;
35 import javax.xml.bind.annotation.XmlElementWrapper;
36 import javax.xml.bind.annotation.XmlIDREF;
37 import javax.xml.bind.annotation.XmlRootElement;
38 import javax.xml.bind.annotation.XmlSchemaType;
39 import javax.xml.bind.annotation.XmlType;
40 import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
41
42 import org.apache.log4j.Logger;
43 import org.hibernate.annotations.Cascade;
44 import org.hibernate.annotations.CascadeType;
45 import org.hibernate.envers.Audited;
46 import org.hibernate.search.annotations.Field;
47 import org.hibernate.search.annotations.FieldBridge;
48 import org.hibernate.search.annotations.Indexed;
49 import org.hibernate.search.annotations.IndexedEmbedded;
50 import org.hibernate.search.annotations.Store;
51
52 import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
53 import eu.etaxonomy.cdm.hibernate.search.MultilanguageTextFieldBridge;
54 import eu.etaxonomy.cdm.jaxb.MultilanguageTextAdapter;
55 import eu.etaxonomy.cdm.model.common.IReferencedEntity;
56 import eu.etaxonomy.cdm.model.common.IdentifiableEntity;
57 import eu.etaxonomy.cdm.model.common.Language;
58 import eu.etaxonomy.cdm.model.common.LanguageString;
59 import eu.etaxonomy.cdm.model.common.MultilanguageText;
60 import eu.etaxonomy.cdm.model.common.TimePeriod;
61 import eu.etaxonomy.cdm.model.location.NamedArea;
62 import eu.etaxonomy.cdm.model.reference.Reference;
63 import eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy;
64
65 /**
66 * @author a.mueller
67 * @created 31.03.2009
68 */
69 @XmlAccessorType(XmlAccessType.FIELD)
70 @XmlType(name = "Classification", propOrder = {
71 "name",
72 "description",
73 "rootNode",
74 "reference",
75 "microReference",
76 "timeperiod",
77 "geoScopes"
78 })
79 @XmlRootElement(name = "Classification")
80 @Entity
81 @Audited
82 @Indexed(index = "eu.etaxonomy.cdm.model.taxon.Classification")
83 public class Classification extends IdentifiableEntity<IIdentifiableEntityCacheStrategy<Classification>> implements IReferencedEntity, ITaxonTreeNode, Cloneable{
84 private static final long serialVersionUID = -753804821474209635L;
85 private static final Logger logger = Logger.getLogger(Classification.class);
86
87 @XmlElement(name = "Name")
88 @OneToOne(fetch = FetchType.LAZY)
89 @Cascade({CascadeType.SAVE_UPDATE})
90 @JoinColumn(name = "name_id", referencedColumnName = "id")
91 @IndexedEmbedded
92 private LanguageString name;
93
94
95 @XmlElement(name = "rootNode")
96 @XmlIDREF
97 @XmlSchemaType(name = "IDREF")
98 @OneToOne(fetch=FetchType.LAZY)
99 @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
100 private TaxonNode rootNode;
101
102 @XmlElement(name = "reference")
103 @XmlIDREF
104 @XmlSchemaType(name = "IDREF")
105 @ManyToOne(fetch = FetchType.LAZY)
106 @Cascade({CascadeType.SAVE_UPDATE})
107 private Reference<?> reference;
108
109 @XmlElement(name = "microReference")
110 private String microReference;
111
112 @XmlElement(name = "TimePeriod")
113 private TimePeriod timeperiod = TimePeriod.NewInstance();
114
115 @XmlElementWrapper( name = "GeoScopes")
116 @XmlElement( name = "GeoScope")
117 @XmlIDREF
118 @XmlSchemaType(name="IDREF")
119 @ManyToMany(fetch = FetchType.LAZY)
120 @JoinTable(name="Classification_GeoScope")
121 @Cascade({CascadeType.SAVE_UPDATE})
122 private Set<NamedArea> geoScopes = new HashSet<NamedArea>();
123
124 @XmlElement(name = "Description")
125 @XmlJavaTypeAdapter(MultilanguageTextAdapter.class)
126 @OneToMany(fetch = FetchType.LAZY, orphanRemoval=true)
127 @MapKeyJoinColumn(name="description_mapkey_id")
128 @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE, CascadeType.DELETE })
129 @JoinTable(name = "Classification_Description")
130 // @Field(name="text", store=Store.YES)
131 // @FieldBridge(impl=MultilanguageTextFieldBridge.class)
132 private Map<Language,LanguageString> description = new HashMap<Language,LanguageString>();
133
134
135
136 // /**
137 // * If this classification is an alternative classification for a subclassification in
138 // * an other classification(parent view),
139 // * the alternativeViewRoot is the connection node from this classification to the parent classification.
140 // * It replaces another node in the parent view.
141 // */
142 // private AlternativeViewRoot alternativeViewRoot;
143
144 // ********************** FACTORY METHODS *********************************************/
145
146 public static Classification NewInstance(String name){
147 return NewInstance(name, null, Language.DEFAULT());
148 }
149
150 public static Classification NewInstance(String name, Language language){
151 return NewInstance(name, null, language);
152 }
153
154 public static Classification NewInstance(String name, Reference reference){
155 return NewInstance(name, reference, Language.DEFAULT());
156 }
157
158 public static Classification NewInstance(String name, Reference reference, Language language){
159 return new Classification(name, reference, language);
160 }
161
162 // **************************** CONSTRUCTOR *********************************/
163
164 //for hibernate use only, protected required by Javassist
165 protected Classification(){super();}
166
167 protected Classification(String name, Reference reference, Language language){
168 this();
169 LanguageString langName = LanguageString.NewInstance(name, language);
170 setName(langName);
171 setReference(reference);
172 this.rootNode = new TaxonNode();
173 rootNode.setClassification(this);
174 }
175
176 //********************** xxxxxxxxxxxxx ******************************************/
177 /**
178 * Returns the topmost {@link TaxonNode taxon node} (root node) of <i>this</i>
179 * classification. The root node does not have any parent and no taxon. Since taxon nodes
180 * recursively point to their child nodes the complete classification is
181 * defined by its root node.
182 */
183 public TaxonNode getRootNode(){
184 return rootNode;
185 }
186
187 public void setRootNode(TaxonNode root){
188 this.rootNode = root;
189 }
190
191 @Override
192 public TaxonNode addChildNode(TaxonNode childNode, Reference citation, String microCitation) {
193 return addChildNode(childNode, rootNode.getCountChildren(), citation, microCitation);
194 }
195
196 @Override
197 public TaxonNode addChildNode(TaxonNode childNode, int index, Reference citation, String microCitation) {
198
199 childNode.setParentTreeNode(this.rootNode, index);
200
201 childNode.setReference(citation);
202 childNode.setMicroReference(microCitation);
203 // childNode.setSynonymToBeUsed(synonymToBeUsed);
204
205 return childNode;
206 }
207
208 @Override
209 public TaxonNode addChildTaxon(Taxon taxon, Reference citation, String microCitation) {
210 return addChildTaxon(taxon, rootNode.getCountChildren(), citation, microCitation);
211 }
212
213 @Override
214 public TaxonNode addChildTaxon(Taxon taxon, int index, Reference citation, String microCitation) {
215 return addChildNode(new TaxonNode(taxon), index, citation, microCitation);
216 }
217
218 @Override
219 public boolean deleteChildNode(TaxonNode node) {
220 boolean result = removeChildNode(node);
221
222 if (node.hasTaxon()){
223 node.getTaxon().removeTaxonNode(node);
224 node.setTaxon(null);
225 }
226
227 ArrayList<TaxonNode> childNodes = new ArrayList<TaxonNode>(node.getChildNodes());
228 for (TaxonNode childNode : childNodes){
229 if (childNode != null){
230 node.deleteChildNode(childNode);
231 }
232 }
233 return result;
234 }
235
236 public boolean deleteChildNode(TaxonNode node, boolean deleteChildren) {
237 boolean result = removeChildNode(node);
238
239 node.getTaxon().removeTaxonNode(node);
240 //node.setTaxon(null);
241 if (deleteChildren){
242 ArrayList<TaxonNode> childNodes = new ArrayList<TaxonNode>(node.getChildNodes());
243 for (TaxonNode childNode : childNodes){
244 node.deleteChildNode(childNode);
245 }
246 }
247 return result;
248 }
249
250 /**
251 *
252 * @param node
253 * @return
254 */
255 protected boolean removeChildNode(TaxonNode node){
256 boolean result = false;
257 if(!rootNode.getChildNodes().contains(node)){
258 throw new IllegalArgumentException("TaxonNode is a not a root node of this classification");
259 }
260
261 result = rootNode.removeChildNode(node);
262
263 node.setParent(null);
264 node.setClassification(null);
265
266 return result;
267 }
268
269 public boolean removeRootNode(){
270 boolean result = false;
271
272 if (rootNode != null){
273 this.rootNode.setChildNodes(new ArrayList<TaxonNode>());
274 this.rootNode.setParent(null);
275 rootNode = null;
276 result = true;
277 }
278 return result;
279
280 }
281
282 /**
283 * Appends an existing topmost node to another node of this tree. The existing topmost node becomes
284 * an ordinary node.
285 * @param topmostNode
286 * @param otherNode
287 * @param ref
288 * @param microReference
289 * @throws IllegalArgumentException
290 */
291 public void makeTopmostNodeChildOfOtherNode(TaxonNode topmostNode, TaxonNode otherNode, Reference ref, String microReference)
292 throws IllegalArgumentException{
293 if (otherNode == null){
294 throw new NullPointerException("other node must not be null");
295 }
296 if (! getChildNodes().contains(topmostNode)){
297 throw new IllegalArgumentException("root node to be added as child must already be root node within this tree");
298 }
299 if (otherNode.getClassification() == null || ! otherNode.getClassification().equals(this)){
300 throw new IllegalArgumentException("other node must already be node within this tree");
301 }
302 if (otherNode.equals(topmostNode)){
303 throw new IllegalArgumentException("root node and other node must not be the same");
304 }
305 otherNode.addChildNode(topmostNode, ref, microReference);
306 }
307
308
309 /**
310 * Checks if the given taxon is part of <b>this</b> tree.
311 * @param taxon
312 * @return
313 */
314 public boolean isTaxonInTree(Taxon taxon){
315 return (getNode(taxon) != null);
316 }
317
318 /**
319 * Checks if the given taxon is part of <b>this</b> tree. If so the according TaxonNode is returned.
320 * Otherwise null is returned.
321 * @param taxon
322 * @return
323 */
324 public TaxonNode getNode(Taxon taxon){
325 if (taxon == null){
326 return null;
327 }
328
329 for (TaxonNode taxonNode: taxon.getTaxonNodes()){
330 Classification classification = HibernateProxyHelper.deproxy(taxonNode.getClassification(), Classification.class);
331 if (classification.equals(this)){
332 return taxonNode;
333 }
334 }
335 return null;
336 }
337
338 /**
339 * Checks if the given taxon is one of the topmost taxa in <b>this</b> tree.
340 * @param taxon
341 * @return
342 */
343 public boolean isTopmostInTree(Taxon taxon){
344 return (getTopmostNode(taxon) != null);
345 }
346
347
348 /**
349 * Checks if the taxon is a direct child of <b>this</b> tree and returns the according node if true.
350 * Returns null otherwise.
351 * @param taxon
352 * @return
353 */
354 public TaxonNode getTopmostNode(Taxon taxon){
355 if (taxon == null){
356 return null;
357 }
358 for (TaxonNode taxonNode: taxon.getTaxonNodes()){
359 if (taxonNode.getClassification().equals(this)){
360 if (this.getChildNodes().contains(taxonNode)){
361 if (taxonNode.getParent() == null){
362 logger.warn("A topmost node should always have the root node as parent but actually has no parent");
363 }else if (taxonNode.getParent().getParent() != null){
364 logger.warn("The root node should have not parent but actually has one");
365 }else if (taxonNode.getParent().getTaxon() != null){
366 logger.warn("The root node should have not taxon but actually has one");
367 }
368 return taxonNode;
369 }
370 }
371 }
372 return null;
373 }
374
375 private boolean handleCitationOverwrite(TaxonNode childNode, Reference citation, String microCitation){
376 if (citation != null){
377 if (childNode.getReference() != null && ! childNode.getReference().equals(citation)){
378 logger.warn("ReferenceForParentChildRelation will be overwritten");
379 }
380 childNode.setReference(citation);
381 }
382 if (microCitation != null){
383 if (childNode.getMicroReference() != null && ! childNode.getMicroReference().equals(microCitation)){
384 logger.warn("MicroReferenceForParentChildRelation will be overwritten");
385 }
386 childNode.setMicroReference(microCitation);
387 }
388 return true;
389 }
390
391 /**
392 * Relates two taxa as parent-child nodes within a classification. <BR>
393 * If the taxa are not yet part of the tree they are added to it.<Br>
394 * If the child taxon is a topmost node still it is added as child and deleted from the rootNode set.<Br>
395 * If the child is a child of another parent already an IllegalStateException is thrown because a child can have only
396 * one parent. <Br>
397 * If the parent-child relationship between these two taxa already exists nothing is changed. Only
398 * citation and microcitation are overwritten by the new values if these values are not null.
399 *
400 * @param parent
401 * @param child
402 * @param citation
403 * @param microCitation
404 * @return the childNode
405 * @throws IllegalStateException If the child is a child of another parent already
406 */
407 public TaxonNode addParentChild (Taxon parent, Taxon child, Reference citation, String microCitation)
408 throws IllegalStateException{
409 try {
410 if (parent == null || child == null){
411 logger.warn("Child or parent taxon is null.");
412 return null;
413 }
414 if (parent == child){
415 logger.warn("A taxon should never be its own child. Child not added");
416 return null;
417 }
418 TaxonNode parentNode = this.getNode(parent);
419 TaxonNode childNode = this.getNode(child);
420
421 //if child exists in tree and has a parent
422 //no multiple parents are allowed in the tree
423 if (childNode != null && ! childNode.isTopmostNode()){
424 //...different to the parent taxon throw exception
425 if ( !(childNode.getParent().getTaxon().equals(parent) )){
426 if (childNode.getParent().getTaxon().getId() != 0 && childNode.getParent().getTaxon().getName().equals(parent.getName())){
427 throw new IllegalStateException("The child taxon is already part of the tree but has an other parent taxon than the parent to be added. Child: " + child.toString() + ", new parent:" + parent.toString() + ", old parent: " + childNode.getParent().getTaxon().toString()) ;
428 }
429 //... same as the parent taxon do nothing but overwriting citation and microCitation
430 }else{
431 handleCitationOverwrite(childNode, citation, microCitation);
432 return childNode;
433 }
434 }
435
436 //add parent node if not exist
437 if (parentNode == null){
438 parentNode = this.addChildTaxon(parent, null, null);
439 }
440
441 //add child if not exists
442 if (childNode == null){
443 childNode = parentNode.addChildTaxon(child, citation, microCitation);
444 }else{
445 //child is still topmost node
446 //TODO test if child is topmostNode otherwise throw IllegalStateException
447 if (! this.isTopmostInTree(child)){
448 //throw new IllegalStateException("Child is not a topmost node but must be");
449 if (childNode.getClassification() != null && childNode.getParent() == null){
450 logger.warn("Child has no parent and is not a topmost node, child: " + child.getId() + " classification: " + childNode.getClassification().getId());
451 }else{
452 logger.warn("ChildNode has no classification: " + childNode.getId());
453 }
454 parentNode.addChildNode(childNode, citation, microCitation);
455 if (!parentNode.isTopmostNode()){
456 this.addChildNode(parentNode, citation, microCitation);
457 logger.warn("parent is added as a topmost node");
458 }else{
459 logger.warn("parent is already a topmost node");
460 }
461 }else{
462 this.makeTopmostNodeChildOfOtherNode(childNode, parentNode, citation, microCitation);
463 }
464 }
465 return childNode;
466 } catch (IllegalStateException e) {
467 throw e;
468 } catch (RuntimeException e){
469 throw e;
470 }
471 }
472
473
474 @Override
475 @Transient
476 public Reference getCitation() {
477 return reference;
478 }
479
480 public LanguageString getName() {
481 return name;
482 }
483
484 public void setName(LanguageString name) {
485 this.name = name;
486 }
487
488 /**
489 * Returns a set containing all nodes in this classification.
490 *
491 * Caution: Use this method with care. It can be very time and resource consuming and might
492 * run into OutOfMemoryExceptions for big trees.
493 *
494 * @return
495 */
496 @Transient
497 public Set<TaxonNode> getAllNodes() {
498 Set<TaxonNode> allNodes = new HashSet<TaxonNode>();
499
500 for(TaxonNode rootNode : getChildNodes()){
501 allNodes.addAll(rootNode.getDescendants());
502 }
503
504 return allNodes;
505 }
506
507 @Override
508 @Transient
509 public List<TaxonNode> getChildNodes() {
510 return rootNode.getChildNodes();
511 }
512
513 @Override
514 public Reference getReference() {
515 return reference;
516 }
517
518 public void setReference(Reference reference) {
519 this.reference = reference;
520 }
521
522
523 @Override
524 public String getMicroReference() {
525 return microReference;
526 }
527
528 /**
529 * @param microReference the microReference to set
530 */
531 public void setMicroReference(String microReference) {
532 this.microReference = microReference;
533 }
534
535
536 /**
537 * The point in time, the time period or the season for which this description element
538 * is valid. A season may be expressed by not filling the year part(s) of the time period.
539 */
540 public TimePeriod getTimeperiod() {
541 return timeperiod;
542 }
543
544 /**
545 * @see #getTimeperiod()
546 */
547 public void setTimeperiod(TimePeriod timeperiod) {
548 if (timeperiod == null){
549 timeperiod = TimePeriod.NewInstance();
550 }
551 this.timeperiod = timeperiod;
552 }
553
554
555 /**
556 * Returns the set of {@link NamedArea named areas} indicating the geospatial
557 * data where <i>this</i> {@link Classification} is valid.
558 */
559 public Set<NamedArea> getGeoScopes(){
560 return this.geoScopes;
561 }
562
563 /**
564 * Adds a {@link NamedArea named area} to the set of {@link #getGeoScopes() named areas}
565 * delimiting the geospatial area where <i>this</i> {@link Classification} is valid.
566 *
567 * @param geoScope the named area to be additionally assigned to <i>this</i> taxon description
568 * @see #getGeoScopes()
569 */
570 public void addGeoScope(NamedArea geoScope){
571 this.geoScopes.add(geoScope);
572 }
573
574 /**
575 * Removes one element from the set of {@link #getGeoScopes() named areas} delimiting
576 * the geospatial area where <i>this</i> {@link Classification} is valid.
577 *
578 * @param geoScope the named area which should be removed
579 * @see #getGeoScopes()
580 * @see #addGeoScope(NamedArea)
581 */
582 public void removeGeoScope(NamedArea geoScope){
583 this.geoScopes.remove(geoScope);
584 }
585
586
587 /**
588 * Returns the i18n description used to describe
589 * <i>this</i> {@link Classification}. The different {@link LanguageString language strings}
590 * contained in the multilanguage text should all have the same meaning.
591 */
592 public Map<Language,LanguageString> getDescription(){
593 return this.description;
594 }
595
596 /**
597 * Adds a translated {@link LanguageString text in a particular language}
598 * to the {@link MultilanguageText description} used to describe
599 * <i>this</i> {@link Classification}.
600 *
601 * @param description the language string describing the individuals association
602 * in a particular language
603 * @see #getDescription()
604 * @see #putDescription(Language, String)
605 *
606 */
607 public void putDescription(LanguageString description){
608 this.description.put(description.getLanguage(),description);
609 }
610 /**
611 * Creates a {@link LanguageString language string} based on the given text string
612 * and the given {@link Language language} and adds it to the {@link MultilanguageText multilanguage text}
613 * used to describe <i>this</i> {@link Classification}.
614 *
615 * @param text the string describing the individuals association
616 * in a particular language
617 * @param language the language in which the text string is formulated
618 * @see #getDescription()
619 * @see #putDescription(LanguageString)
620 */
621 public void putDescription(Language language, String text){
622 this.description.put(language, LanguageString.NewInstance(text, language));
623 }
624 /**
625 * Removes from the {@link MultilanguageText description} used to describe
626 * <i>this</i> {@link Classification} the one {@link LanguageString language string}
627 * with the given {@link Language language}.
628 *
629 * @param language the language in which the language string to be removed
630 * has been formulated
631 * @see #getDescription()
632 */
633 public void removeDescription(Language language){
634 this.description.remove(language);
635 }
636
637
638 @Override
639 public String generateTitle() {
640 return name.getText();
641 }
642
643 public int compareTo(Object o) {
644 return 0;
645 }
646
647
648 @Override
649 public boolean hasChildNodes() {
650 return getChildNodes().size() > 0;
651 }
652
653 //*********************** CLONE ********************************************************/
654 /**
655 * Clones <i>this</i> classification. This is a shortcut that enables to create
656 * a new instance that differs only slightly from <i>this</i> classification by
657 * modifying only some of the attributes.<BR><BR>
658
659 * @see eu.etaxonomy.cdm.model.media.IdentifiableEntity#clone()
660 * @see java.lang.Object#clone()
661 */
662 @Override
663 public Object clone() {
664 Classification result;
665 try{
666 result = (Classification)super.clone();
667 //result.rootNode.childNodes = new ArrayList<TaxonNode>();
668 List<TaxonNode> rootNodes = new ArrayList<TaxonNode>();
669 TaxonNode rootNodeClone;
670
671
672 rootNodes.addAll(rootNode.getChildNodes());
673 TaxonNode rootNode;
674 Iterator<TaxonNode> iterator = rootNodes.iterator();
675
676 while (iterator.hasNext()){
677 rootNode = iterator.next();
678 rootNodeClone = rootNode.cloneDescendants();
679 rootNodeClone.setClassification(result);
680 result.addChildNode(rootNodeClone, rootNode.getReference(), rootNode.getMicroReference());
681 rootNodeClone.setSynonymToBeUsed(rootNode.getSynonymToBeUsed());
682 }
683
684 //geo-scopes
685 result.geoScopes = new HashSet<NamedArea>();
686 for (NamedArea namedArea : getGeoScopes()){
687 result.geoScopes.add(namedArea);
688 }
689
690 return result;
691
692 }catch (CloneNotSupportedException e) {
693 logger.warn("Object does not implement cloneable");
694 e.printStackTrace();
695 return null;
696 }
697
698
699
700
701
702 }
703
704
705 }