2 * Copyright (C) 2007 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
6 * The contents of this file are subject to the Mozilla Public License Version 1.1
7 * See LICENSE.TXT at the top of this package for the full license terms.
10 package eu
.etaxonomy
.cdm
.model
.taxon
;
12 import java
.util
.ArrayList
;
13 import java
.util
.HashMap
;
14 import java
.util
.HashSet
;
15 import java
.util
.Iterator
;
16 import java
.util
.List
;
20 import javax
.persistence
.Entity
;
21 import javax
.persistence
.FetchType
;
22 import javax
.persistence
.JoinColumn
;
23 import javax
.persistence
.JoinTable
;
24 import javax
.persistence
.ManyToMany
;
25 import javax
.persistence
.ManyToOne
;
26 import javax
.persistence
.MapKeyJoinColumn
;
27 import javax
.persistence
.OneToMany
;
28 import javax
.persistence
.OneToOne
;
29 import javax
.persistence
.Transient
;
30 import javax
.xml
.bind
.annotation
.XmlAccessType
;
31 import javax
.xml
.bind
.annotation
.XmlAccessorType
;
32 import javax
.xml
.bind
.annotation
.XmlElement
;
33 import javax
.xml
.bind
.annotation
.XmlElementWrapper
;
34 import javax
.xml
.bind
.annotation
.XmlIDREF
;
35 import javax
.xml
.bind
.annotation
.XmlRootElement
;
36 import javax
.xml
.bind
.annotation
.XmlSchemaType
;
37 import javax
.xml
.bind
.annotation
.XmlType
;
38 import javax
.xml
.bind
.annotation
.adapters
.XmlJavaTypeAdapter
;
40 import org
.apache
.log4j
.Logger
;
41 import org
.hibernate
.annotations
.Cascade
;
42 import org
.hibernate
.annotations
.CascadeType
;
43 import org
.hibernate
.envers
.Audited
;
44 import org
.hibernate
.search
.annotations
.Indexed
;
45 import org
.hibernate
.search
.annotations
.IndexedEmbedded
;
47 import eu
.etaxonomy
.cdm
.hibernate
.HibernateProxyHelper
;
48 import eu
.etaxonomy
.cdm
.jaxb
.MultilanguageTextAdapter
;
49 import eu
.etaxonomy
.cdm
.model
.common
.IdentifiableEntity
;
50 import eu
.etaxonomy
.cdm
.model
.common
.IdentifiableSource
;
51 import eu
.etaxonomy
.cdm
.model
.common
.Language
;
52 import eu
.etaxonomy
.cdm
.model
.common
.LanguageString
;
53 import eu
.etaxonomy
.cdm
.model
.common
.MultilanguageText
;
54 import eu
.etaxonomy
.cdm
.model
.common
.TimePeriod
;
55 import eu
.etaxonomy
.cdm
.model
.description
.DescriptionElementSource
;
56 import eu
.etaxonomy
.cdm
.model
.location
.NamedArea
;
57 import eu
.etaxonomy
.cdm
.model
.reference
.Reference
;
58 import eu
.etaxonomy
.cdm
.strategy
.cache
.common
.IIdentifiableEntityCacheStrategy
;
64 @XmlAccessorType(XmlAccessType
.FIELD
)
65 @XmlType(name
= "Classification", propOrder
= {
75 @XmlRootElement(name
= "Classification")
78 @Indexed(index
= "eu.etaxonomy.cdm.model.taxon.Classification")
79 public class Classification
80 extends IdentifiableEntity
<IIdentifiableEntityCacheStrategy
<Classification
>>
81 implements ITaxonTreeNode
{
83 private static final long serialVersionUID
= -753804821474209635L;
84 private static final Logger logger
= Logger
.getLogger(Classification
.class);
86 @XmlElement(name
= "Name")
87 @OneToOne(fetch
= FetchType
.LAZY
)
88 @Cascade({CascadeType
.SAVE_UPDATE
,CascadeType
.MERGE
})
89 @JoinColumn(name
= "name_id", referencedColumnName
= "id")
91 private LanguageString name
;
93 @XmlElement(name
= "rootNode")
95 @XmlSchemaType(name
= "IDREF")
96 @OneToOne(fetch
=FetchType
.LAZY
)
97 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
})
98 private TaxonNode rootNode
;
100 @XmlElement(name
= "reference")
102 @XmlSchemaType(name
= "IDREF")
103 @ManyToOne(fetch
= FetchType
.LAZY
)
104 @Cascade({CascadeType
.SAVE_UPDATE
,CascadeType
.MERGE
})
105 private Reference reference
;
107 @XmlElement(name
= "microReference")
108 private String microReference
;
110 @XmlElement(name
= "TimePeriod")
111 private TimePeriod timeperiod
= TimePeriod
.NewInstance();
113 @XmlElementWrapper( name
= "GeoScopes")
114 @XmlElement( name
= "GeoScope")
116 @XmlSchemaType(name
="IDREF")
117 @ManyToMany(fetch
= FetchType
.LAZY
)
118 @JoinTable(name
="Classification_GeoScope")
119 // @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE}) remove cascade #5755
120 private Set
<NamedArea
> geoScopes
= new HashSet
<>();
122 @XmlElement(name
= "Description")
123 @XmlJavaTypeAdapter(MultilanguageTextAdapter
.class)
124 @OneToMany(fetch
= FetchType
.LAZY
, orphanRemoval
=true)
125 @MapKeyJoinColumn(name
="description_mapkey_id")
126 @Cascade({CascadeType
.SAVE_UPDATE
,CascadeType
.MERGE
, CascadeType
.DELETE
})
127 @JoinTable(name
= "Classification_Description")
128 // @Field(name="text", store=Store.YES)
129 // @FieldBridge(impl=MultilanguageTextFieldBridge.class)
130 private Map
<Language
,LanguageString
> description
= new HashMap
<>();
132 //the source for this single classification
133 @XmlElement(name
= "source")
135 @XmlSchemaType(name
= "IDREF")
136 @OneToOne(fetch
= FetchType
.LAZY
, orphanRemoval
=true)
137 @Cascade({CascadeType
.SAVE_UPDATE
,CascadeType
.MERGE
, CascadeType
.DELETE
})
138 private IdentifiableSource source
;
142 // * If this classification is an alternative classification for a subclassification in
143 // * an other classification(parent view),
144 // * the alternativeViewRoot is the connection node from this classification to the parent classification.
145 // * It replaces another node in the parent view.
147 // private AlternativeViewRoot alternativeViewRoot;
149 // ********************** FACTORY METHODS *********************************************/
151 public static Classification
NewInstance(String name
){
152 return NewInstance(name
, null, Language
.DEFAULT());
155 public static Classification
NewInstance(String name
, Language language
){
156 return NewInstance(name
, null, language
);
159 public static Classification
NewInstance(String name
, Reference reference
){
160 return NewInstance(name
, reference
, Language
.DEFAULT());
163 public static Classification
NewInstance(String name
, Reference reference
, Language language
){
164 return new Classification(name
, reference
, language
);
167 // **************************** CONSTRUCTOR *********************************/
169 //for hibernate use only, protected required by Javassist
170 protected Classification(){super();}
172 protected Classification(String name
, Reference reference
, Language language
){
174 LanguageString langName
= LanguageString
.NewInstance(name
, language
);
176 setReference(reference
);
177 this.rootNode
= new TaxonNode();
178 rootNode
.setClassification(this);
181 //********************** xxxxxxxxxxxxx ******************************************/
184 * Returns the topmost {@link TaxonNode taxon node} (root node) of <i>this</i>
185 * classification. The root node does not have any parent and no taxon. Since taxon nodes
186 * recursively point to their child nodes the complete classification is
187 * defined by its root node.
189 public TaxonNode
getRootNode(){
193 public void setRootNode(TaxonNode root
){
194 this.rootNode
= root
;
198 public TaxonNode
addChildNode(TaxonNode childNode
, Reference citation
, String microCitation
) {
199 return addChildNode(childNode
, rootNode
.getCountChildren(), citation
, microCitation
);
203 public TaxonNode
addChildNode(TaxonNode childNode
, int index
, Reference citation
, String microCitation
) {
205 childNode
.setParentTreeNode(this.rootNode
, index
);
207 childNode
.setCitation(citation
);
208 childNode
.setCitationMicroReference(microCitation
);
209 // childNode.setSynonymToBeUsed(synonymToBeUsed);
215 public TaxonNode
addChildTaxon(Taxon taxon
, Reference citation
, String microCitation
) {
216 return addChildTaxon(taxon
, rootNode
.getCountChildren(), citation
, microCitation
);
220 public TaxonNode
addChildTaxon(Taxon taxon
, int index
, Reference citation
, String microCitation
) {
221 return addChildNode(new TaxonNode(taxon
), index
, DescriptionElementSource
.NewPrimarySourceInstance(citation
, microCitation
));
225 public TaxonNode
addChildTaxon(Taxon taxon
, DescriptionElementSource source
) {
226 return addChildTaxon(taxon
, rootNode
.getCountChildren(), source
);
230 public TaxonNode
addChildTaxon(Taxon taxon
, int index
, DescriptionElementSource source
) {
231 return addChildNode(new TaxonNode(taxon
), index
, source
);
235 public TaxonNode
addChildNode(TaxonNode childNode
, int index
, DescriptionElementSource source
) {
236 childNode
.setParentTreeNode(this.rootNode
, index
);
237 childNode
.setSource(source
);
243 public boolean deleteChildNode(TaxonNode node
) {
244 boolean result
= removeChildNode(node
);
246 if (node
.hasTaxon()){
247 node
.getTaxon().removeTaxonNode(node
);
251 ArrayList
<TaxonNode
> childNodes
= new ArrayList
<>(node
.getChildNodes());
252 for (TaxonNode childNode
: childNodes
){
253 if (childNode
!= null){
254 node
.deleteChildNode(childNode
);
260 public boolean deleteChildNode(TaxonNode node
, boolean deleteChildren
) {
261 boolean result
= removeChildNode(node
);
262 if (node
.hasTaxon()){
263 node
.getTaxon().removeTaxonNode(node
);
268 ArrayList
<TaxonNode
> childNodes
= new ArrayList
<TaxonNode
>(node
.getChildNodes());
269 for (TaxonNode childNode
: childNodes
){
270 node
.deleteChildNode(childNode
);
276 protected boolean removeChildNode(TaxonNode node
){
277 boolean result
= false;
278 rootNode
= HibernateProxyHelper
.deproxy(rootNode
, TaxonNode
.class);
279 if(!rootNode
.getChildNodes().contains(node
)){
280 throw new IllegalArgumentException("TaxonNode is a not a root node of this classification");
282 // rootNode = HibernateProxyHelper.deproxy(rootNode, TaxonNode.class);
283 result
= rootNode
.removeChildNode(node
);
285 node
.setParent(null);
286 node
.setClassification(null);
291 public boolean removeRootNode(){
292 boolean result
= false;
294 if (rootNode
!= null){
295 this.rootNode
.setChildNodes(new ArrayList
<TaxonNode
>());
296 this.rootNode
.setParent(null);
304 * Appends an existing topmost node to another node of this tree. The existing topmost node becomes
309 * @param microReference
310 * @throws IllegalArgumentException
312 public void makeTopmostNodeChildOfOtherNode(TaxonNode topmostNode
, TaxonNode otherNode
, Reference ref
, String microReference
)
313 throws IllegalArgumentException
{
314 if (otherNode
== null){
315 throw new NullPointerException("other node must not be null");
317 if (! getChildNodes().contains(topmostNode
)){
318 throw new IllegalArgumentException("root node to be added as child must already be root node within this tree");
320 if (otherNode
.getClassification() == null || ! otherNode
.getClassification().equals(this)){
321 throw new IllegalArgumentException("other node must already be node within this tree");
323 if (otherNode
.equals(topmostNode
)){
324 throw new IllegalArgumentException("root node and other node must not be the same");
326 otherNode
.addChildNode(topmostNode
, ref
, microReference
);
330 * Checks if the given taxon is part of <b>this</b> tree.
334 public boolean isTaxonInTree(Taxon taxon
){
335 taxon
= HibernateProxyHelper
.deproxy(taxon
, Taxon
.class);
336 return (getNode(taxon
) != null);
340 * Checks if the given taxon is part of <b>this</b> tree. If so the according TaxonNode is returned.
341 * Otherwise <code>null</code> is returned.
345 public TaxonNode
getNode(Taxon taxon
){
350 for (TaxonNode taxonNode
: taxon
.getTaxonNodes()){
351 Classification classification
= deproxy(taxonNode
.getClassification(), Classification
.class);
352 if (classification
.equals(this)){
360 * Checks if the given taxon is one of the topmost taxa in <b>this</b> tree.
364 public boolean isTopmostInTree(Taxon taxon
){
365 return (getTopmostNode(taxon
) != null);
369 * Checks if the taxon is a direct child of the root of <b>this</b> tree and returns the according node if true.
370 * Returns <code>null</code> otherwise.
374 public TaxonNode
getTopmostNode(Taxon taxon
){
378 for (TaxonNode taxonNode
: taxon
.getTaxonNodes()){
379 if (taxonNode
.getClassification().equals(this)){
380 if (this.getChildNodes().contains(taxonNode
)){
381 if (taxonNode
.getParent() == null){
382 logger
.warn("A topmost node should always have the root node as parent but actually has no parent");
383 }else if (taxonNode
.getParent().getParent() != null){
384 logger
.warn("The root node should have not parent but actually has one");
385 }else if (taxonNode
.getParent().getTaxon() != null){
386 logger
.warn("The root node should have not taxon but actually has one");
395 private boolean handleCitationOverwrite(TaxonNode childNode
, Reference citation
, String microCitation
){
396 if (citation
!= null){
397 if (childNode
.getReference() != null && ! childNode
.getReference().equals(citation
)){
398 logger
.warn("TaxonNode source will be overwritten");
400 childNode
.setCitation(citation
);
402 if (microCitation
!= null){
403 if (childNode
.getMicroReference() != null && ! childNode
.getMicroReference().equals(microCitation
)){
404 logger
.warn("TaxonNode source detail will be overwritten");
406 childNode
.setCitationMicroReference(microCitation
);
412 * Relates two taxa as parent-child nodes within a classification. <BR>
413 * If the taxa are not yet part of the tree they are added to it.<Br>
414 * If the child taxon is a topmost node still it is added as child and deleted from the rootNode set.<Br>
415 * If the child is a child of another parent already an IllegalStateException is thrown because a child can have only
417 * If the parent-child relationship between these two taxa already exists nothing is changed. Only
418 * citation and microcitation are overwritten by the new values if these values are not null.
423 * @param microCitation
424 * @return the childNode
425 * @throws IllegalStateException If the child is a child of another parent already
427 public TaxonNode
addParentChild (Taxon parent
, Taxon child
, Reference citation
, String microCitation
)
428 throws IllegalStateException
{
430 if (parent
== null || child
== null){
431 logger
.warn("Child or parent taxon is null.");
434 if (parent
== child
){
435 logger
.warn("A taxon should never be its own child. Child not added");
438 TaxonNode parentNode
= this.getNode(parent
);
439 TaxonNode childNode
= this.getNode(child
);
441 //if child exists in tree and has a parent
442 //no multiple parents are allowed in the tree
443 if (childNode
!= null && ! childNode
.isTopmostNode()){
444 //...different to the parent taxon throw exception
445 if ( !(childNode
.getParent().getTaxon().equals(parent
) )){
446 if (childNode
.getParent().getTaxon().getId() != 0 && childNode
.getParent().getTaxon().getName().equals(parent
.getName())){
447 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()) ;
449 //... same as the parent taxon do nothing but overwriting citation and microCitation
451 handleCitationOverwrite(childNode
, citation
, microCitation
);
456 //add parent node if not exist
457 if (parentNode
== null){
458 parentNode
= this.addChildTaxon(parent
, null, null);
461 //add child if not exists
462 if (childNode
== null){
463 childNode
= parentNode
.addChildTaxon(child
, citation
, microCitation
);
465 //child is still topmost node
466 //TODO test if child is topmostNode otherwise throw IllegalStateException
467 if (! this.isTopmostInTree(child
)){
468 //throw new IllegalStateException("Child is not a topmost node but must be");
469 if (childNode
.getClassification() != null && childNode
.getParent() == null){
470 logger
.warn("Child has no parent and is not a topmost node, child: " + child
.getId() + " classification: " + childNode
.getClassification().getId());
472 logger
.warn("ChildNode has no classification: " + childNode
.getId());
474 parentNode
.addChildNode(childNode
, citation
, microCitation
);
475 if (!parentNode
.isTopmostNode()){
476 this.addChildNode(parentNode
, citation
, microCitation
);
477 logger
.warn("parent is added as a topmost node");
479 logger
.warn("parent is already a topmost node");
482 this.makeTopmostNodeChildOfOtherNode(childNode
, parentNode
, citation
, microCitation
);
486 } catch (IllegalStateException e
) {
488 } catch (RuntimeException e
){
494 public Reference
getCitation() {
498 public LanguageString
getName() {
501 public void setName(LanguageString name
) {
506 * Returns a set containing all nodes in this classification.
508 * Caution: Use this method with care. It can be very time and resource consuming and might
509 * run into OutOfMemoryExceptions for big trees.
514 public Set
<TaxonNode
> getAllNodes() {
515 Set
<TaxonNode
> allNodes
= new HashSet
<>();
517 for(TaxonNode rootNode
: getChildNodes()){
518 allNodes
.addAll(rootNode
.getDescendants());
526 public List
<TaxonNode
> getChildNodes() {
527 return rootNode
.getChildNodes();
531 public Reference
getReference() {
534 public void setReference(Reference reference
) {
535 this.reference
= reference
;
539 public String
getMicroReference() {
540 return microReference
;
542 public void setMicroReference(String microReference
) {
543 this.microReference
= microReference
;
547 * The point in time, the time period or the season for which this description element
548 * is valid. A season may be expressed by not filling the year part(s) of the time period.
550 public TimePeriod
getTimeperiod() {
554 * @see #getTimeperiod()
556 public void setTimeperiod(TimePeriod timeperiod
) {
557 if (timeperiod
== null){
558 timeperiod
= TimePeriod
.NewInstance();
560 this.timeperiod
= timeperiod
;
564 * Returns the set of {@link NamedArea named areas} indicating the geospatial
565 * data where <i>this</i> {@link Classification} is valid.
567 public Set
<NamedArea
> getGeoScopes(){
568 return this.geoScopes
;
572 * Adds a {@link NamedArea named area} to the set of {@link #getGeoScopes() named areas}
573 * delimiting the geospatial area where <i>this</i> {@link Classification} is valid.
575 * @param geoScope the named area to be additionally assigned to <i>this</i> taxon description
576 * @see #getGeoScopes()
578 public void addGeoScope(NamedArea geoScope
){
579 this.geoScopes
.add(geoScope
);
583 * Removes one element from the set of {@link #getGeoScopes() named areas} delimiting
584 * the geospatial area where <i>this</i> {@link Classification} is valid.
586 * @param geoScope the named area which should be removed
587 * @see #getGeoScopes()
588 * @see #addGeoScope(NamedArea)
590 public void removeGeoScope(NamedArea geoScope
){
591 this.geoScopes
.remove(geoScope
);
595 * Returns the i18n description used to describe
596 * <i>this</i> {@link Classification}. The different {@link LanguageString language strings}
597 * contained in the multilanguage text should all have the same meaning.
599 public Map
<Language
,LanguageString
> getDescription(){
600 return this.description
;
604 * Adds a translated {@link LanguageString text in a particular language}
605 * to the {@link MultilanguageText description} used to describe
606 * <i>this</i> {@link Classification}.
608 * @param description the language string describing the individuals association
609 * in a particular language
610 * @see #getDescription()
611 * @see #putDescription(Language, String)
614 public void putDescription(LanguageString description
){
615 this.description
.put(description
.getLanguage(),description
);
618 * Creates a {@link LanguageString language string} based on the given text string
619 * and the given {@link Language language} and adds it to the {@link MultilanguageText multilanguage text}
620 * used to describe <i>this</i> {@link Classification}.
622 * @param text the string describing the individuals association
623 * in a particular language
624 * @param language the language in which the text string is formulated
625 * @see #getDescription()
626 * @see #putDescription(LanguageString)
628 public void putDescription(Language language
, String text
){
629 this.description
.put(language
, LanguageString
.NewInstance(text
, language
));
632 * Removes from the {@link MultilanguageText description} used to describe
633 * <i>this</i> {@link Classification} the one {@link LanguageString language string}
634 * with the given {@link Language language}.
636 * @param language the language in which the language string to be removed
637 * has been formulated
638 * @see #getDescription()
640 public void removeDescription(Language language
){
641 this.description
.remove(language
);
645 public String
generateTitle() {
646 //TODO implement as cache strategy
647 if (protectedTitleCache
){
648 return this.titleCache
;
649 }else if (name
!= null){
650 return name
.getText();
651 }else if (reference
!= null){
652 return this.reference
.getTitleCache();
654 return this.toString();
658 public int compareTo(Object o
) {
659 //TODO needs to be implemented
664 public boolean hasChildNodes() {
665 return getChildNodes().size() > 0;
668 //*********************** CLONE ********************************************************/
670 * Clones <i>this</i> classification. This is a shortcut that enables to create
671 * a new instance that differs only slightly from <i>this</i> classification by
672 * modifying only some of the attributes.<BR><BR>
674 * @see eu.etaxonomy.cdm.model.media.IdentifiableEntity#clone()
675 * @see java.lang.Object#clone()
678 public Object
clone() {
679 Classification result
;
681 result
= (Classification
)super.clone();
682 //result.rootNode.childNodes = new ArrayList<TaxonNode>();
683 List
<TaxonNode
> rootNodes
= new ArrayList
<>();
684 TaxonNode rootNodeClone
;
686 rootNodes
.addAll(rootNode
.getChildNodes());
688 Iterator
<TaxonNode
> iterator
= rootNodes
.iterator();
690 result
.description
= cloneLanguageString(this.description
);
692 while (iterator
.hasNext()){
693 rootNode
= iterator
.next();
694 rootNodeClone
= rootNode
.cloneDescendants();
695 rootNodeClone
.setClassification(result
);
696 result
.addChildNode(rootNodeClone
, rootNode
.getReference(), rootNode
.getMicroReference());
697 rootNodeClone
.setSynonymToBeUsed(rootNode
.getSynonymToBeUsed());
701 result
.geoScopes
= new HashSet
<>();
702 for (NamedArea namedArea
: getGeoScopes()){
703 result
.geoScopes
.add(namedArea
);
708 }catch (CloneNotSupportedException e
) {
709 logger
.warn("Object does not implement cloneable");