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
.common
;
13 import java
.beans
.PropertyChangeEvent
;
14 import java
.beans
.PropertyChangeListener
;
15 import java
.util
.ArrayList
;
16 import java
.util
.HashSet
;
17 import java
.util
.List
;
19 import java
.util
.UUID
;
21 import javax
.persistence
.Column
;
22 import javax
.persistence
.Embedded
;
23 import javax
.persistence
.FetchType
;
24 import javax
.persistence
.MappedSuperclass
;
25 import javax
.persistence
.OneToMany
;
26 import javax
.persistence
.Transient
;
27 import javax
.validation
.constraints
.NotNull
;
28 import javax
.validation
.constraints
.Size
;
29 import javax
.xml
.bind
.annotation
.XmlAccessType
;
30 import javax
.xml
.bind
.annotation
.XmlAccessorType
;
31 import javax
.xml
.bind
.annotation
.XmlElement
;
32 import javax
.xml
.bind
.annotation
.XmlElementWrapper
;
33 import javax
.xml
.bind
.annotation
.XmlTransient
;
34 import javax
.xml
.bind
.annotation
.XmlType
;
35 import javax
.xml
.bind
.annotation
.adapters
.XmlJavaTypeAdapter
;
37 import org
.apache
.log4j
.Logger
;
38 import org
.hibernate
.annotations
.Cascade
;
39 import org
.hibernate
.annotations
.CascadeType
;
40 import org
.hibernate
.annotations
.IndexColumn
;
41 import org
.hibernate
.envers
.Audited
;
42 import org
.hibernate
.search
.annotations
.Analyze
;
43 import org
.hibernate
.search
.annotations
.Field
;
44 import org
.hibernate
.search
.annotations
.FieldBridge
;
45 import org
.hibernate
.search
.annotations
.Fields
;
46 import org
.hibernate
.search
.annotations
.Store
;
47 import org
.hibernate
.validator
.constraints
.NotEmpty
;
49 import eu
.etaxonomy
.cdm
.hibernate
.HibernateProxyHelper
;
50 import eu
.etaxonomy
.cdm
.hibernate
.search
.StripHtmlBridge
;
51 import eu
.etaxonomy
.cdm
.jaxb
.FormattedTextAdapter
;
52 import eu
.etaxonomy
.cdm
.jaxb
.LSIDAdapter
;
53 import eu
.etaxonomy
.cdm
.model
.media
.Rights
;
54 import eu
.etaxonomy
.cdm
.model
.name
.BotanicalName
;
55 import eu
.etaxonomy
.cdm
.model
.name
.NonViralName
;
56 import eu
.etaxonomy
.cdm
.model
.name
.TaxonNameBase
;
57 import eu
.etaxonomy
.cdm
.model
.reference
.Reference
;
58 import eu
.etaxonomy
.cdm
.model
.taxon
.TaxonBase
;
59 import eu
.etaxonomy
.cdm
.strategy
.cache
.common
.IIdentifiableEntityCacheStrategy
;
60 import eu
.etaxonomy
.cdm
.strategy
.match
.Match
;
61 import eu
.etaxonomy
.cdm
.strategy
.match
.Match
.ReplaceMode
;
62 import eu
.etaxonomy
.cdm
.strategy
.match
.MatchMode
;
63 import eu
.etaxonomy
.cdm
.strategy
.merge
.Merge
;
64 import eu
.etaxonomy
.cdm
.strategy
.merge
.MergeMode
;
65 import eu
.etaxonomy
.cdm
.validation
.Level2
;
68 * Superclass for the primary CDM classes that can be referenced from outside via LSIDs and contain a simple generated title string as a label for human reading.
69 * All subclasses inherit the ability to store additional properties that are stored as {@link Extension Extensions}, basically a string value with a type term.
70 * Any number of right statements can be attached as well as multiple {@link OriginalSourceBase} objects.
71 * Original sources carry a reference to the source, an ID within that source and the original title/label of this object as it was used in that source (originalNameString).
72 * A Taxon for example that was taken from 2 sources like FaunaEuropaea and IPNI would have two originalSource objects.
73 * The originalSource representing that taxon as it was found in IPNI would contain IPNI as the reference, the IPNI id of the taxon and the name of the taxon exactly as it was used in IPNI.
77 * @created 08-Nov-2007 13:06:27
79 @XmlAccessorType(XmlAccessType
.FIELD
)
80 @XmlType(name
= "IdentifiableEntity", propOrder
= {
83 "protectedTitleCache",
91 public abstract class IdentifiableEntity
<S
extends IIdentifiableEntityCacheStrategy
> extends AnnotatableEntity
92 implements IIdentifiableEntity
/*, ISourceable<IdentifiableSource> */ {
93 private static final long serialVersionUID
= -5610995424730659058L;
94 private static final Logger logger
= Logger
.getLogger(IdentifiableEntity
.class);
97 public static final boolean PROTECTED
= true;
99 public static final boolean NOT_PROTECTED
= false;
101 @XmlElement(name
= "LSID", type
= String
.class)
102 @XmlJavaTypeAdapter(LSIDAdapter
.class)
106 @XmlElement(name
= "TitleCache", required
= true)
107 @XmlJavaTypeAdapter(FormattedTextAdapter
.class)
108 @Column(name
="titleCache")
109 @Match(value
=MatchMode
.CACHE
, cacheReplaceMode
=ReplaceMode
.ALL
)
110 @NotEmpty(groups
= Level2
.class) // implictly NotNull
111 @Size(max
= 800) //see #1592
113 @Field(store
=Store
.YES
),
114 @Field(name
= "titleCache__sort", analyze
= Analyze
.NO
, store
=Store
.YES
)
116 @FieldBridge(impl
=StripHtmlBridge
.class)
117 protected String titleCache
;
119 //if true titleCache will not be automatically generated/updated
120 @XmlElement(name
= "ProtectedTitleCache")
121 protected boolean protectedTitleCache
;
123 @XmlElementWrapper(name
= "Rights", nillable
= true)
124 @XmlElement(name
= "Rights")
125 @OneToMany(fetch
= FetchType
.LAZY
, orphanRemoval
=true)
126 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
, CascadeType
.DELETE
})
128 @Merge(MergeMode
.ADD_CLONE
)
130 private Set
<Rights
> rights
= new HashSet
<Rights
>();
132 @XmlElementWrapper(name
= "Credits", nillable
= true)
133 @XmlElement(name
= "Credit")
134 @IndexColumn(name
="sortIndex", base
= 0)
135 @OneToMany(fetch
= FetchType
.LAZY
, orphanRemoval
=true)
136 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
, CascadeType
.DELETE
})
138 @Merge(MergeMode
.ADD_CLONE
)
140 private List
<Credit
> credits
= new ArrayList
<Credit
>();
142 @XmlElementWrapper(name
= "Extensions", nillable
= true)
143 @XmlElement(name
= "Extension")
144 @OneToMany(fetch
= FetchType
.LAZY
, orphanRemoval
=true)
145 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
, CascadeType
.DELETE
})
146 @Merge(MergeMode
.ADD_CLONE
)
148 private Set
<Extension
> extensions
= new HashSet
<Extension
>();
150 // @XmlElementWrapper(name = "Identifiers", nillable = true)
151 // @XmlElement(name = "Identifier")
152 // @OneToMany(fetch = FetchType.LAZY, orphanRemoval=true)
153 // @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
154 // @Merge(MergeMode.ADD_CLONE)
156 // private Set<Identifier> identifiers = new HashSet<Identifier>();
158 @XmlElementWrapper(name
= "Sources", nillable
= true)
159 @XmlElement(name
= "IdentifiableSource")
160 @OneToMany(fetch
= FetchType
.LAZY
, orphanRemoval
=true)
161 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
, CascadeType
.DELETE
})
162 @Merge(MergeMode
.ADD_CLONE
)
164 private Set
<IdentifiableSource
> sources
= new HashSet
<IdentifiableSource
>();
168 protected S cacheStrategy
;
170 protected IdentifiableEntity(){
174 protected void initListener(){
175 PropertyChangeListener listener
= new PropertyChangeListener() {
177 public void propertyChange(PropertyChangeEvent ev
) {
178 if (!ev
.getPropertyName().equals("titleCache") && !ev
.getPropertyName().equals("cacheStrategy") && ! isProtectedTitleCache()){
183 addPropertyChangeListener(listener
);
187 * By default, we expect most cdm objects to be abstract things
188 * i.e. unable to return a data representation.
190 * Specific subclasses (e.g. Sequence) can override if necessary.
193 public byte[] getData() {
197 //******************************** CACHE *****************************************************/
199 // @Transient - must not be transient, since this property needs to to be included in all serializations produced by the remote layer
201 public String
getTitleCache(){
202 if (protectedTitleCache
){
203 return this.titleCache
;
205 // is title dirty, i.e. equal NULL?
206 if (titleCache
== null){
207 this.titleCache
= generateTitle();
208 this.titleCache
= getTruncatedCache(this.titleCache
) ;
214 * The titleCache will be regenerated from scratch if not protected
215 * @return <code>true</code> if title cache was regenerated, <code>false</code> otherwise
217 protected boolean regenerateTitleCache() {
218 if (!protectedTitleCache
) {
219 this.titleCache
= null;
222 return protectedTitleCache
;
227 public void setTitleCache(String titleCache
){
228 //TODO shouldn't we call setTitleCache(String, boolean),but is this conformant with Java Bean Specification?
229 this.titleCache
= getTruncatedCache(titleCache
);
233 public void setTitleCache(String titleCache
, boolean protectCache
){
234 titleCache
= getTruncatedCache(titleCache
);
235 this.titleCache
= titleCache
;
236 this.protectedTitleCache
= protectCache
;
245 protected String
getTruncatedCache(String cache
) {
247 if (cache
!= null && cache
.length() > maxLength
){
248 logger
.warn("Truncation of cache: " + this.toString() + "/" + cache
);
249 cache
= cache
.substring(0, maxLength
- 4) + "..."; //TODO do we need -4 or is -3 enough
254 //**************************************************************************************
257 public LSID
getLsid(){
261 public void setLsid(LSID lsid
){
265 public Set
<Rights
> getRights() {
267 this.rights
= new HashSet
<Rights
>();
273 public void addRights(Rights right
){
274 getRights().add(right
);
277 public void removeRights(Rights right
){
278 getRights().remove(right
);
283 public List
<Credit
> getCredits() {
284 if(credits
== null) {
285 this.credits
= new ArrayList
<Credit
>();
291 public Credit
getCredits(Integer index
){
292 return getCredits().get(index
);
296 public void addCredit(Credit credit
){
297 getCredits().add(credit
);
302 public void addCredit(Credit credit
, int index
){
303 getCredits().add(index
, credit
);
307 public void removeCredit(Credit credit
){
308 getCredits().remove(credit
);
312 public void removeCredit(int index
){
313 getCredits().remove(index
);
317 // public Set<Identifier> getIdentifiers(){
318 // if(this.identifiers == null) {
319 // this.identifiers = new HashSet<Identifier>();
321 // return this.identifiers;
325 // * @return a Set of extension value strings
327 // public Set<String> getIdentifiers(DefinedTerm type){
328 // return getIdentifiers(type.getUuid());
331 // * @param extensionTypeUuid
332 // * @return a Set of extension value strings
334 // public Set<String> getIdentifiers(UUID identifierTypeUuid){
335 // Set<String> result = new HashSet<String>();
336 // for (Identifier identifier : getIdentifiers()){
337 // if (identifier.getType().getUuid().equals(identifierTypeUuid)){
338 // result.add(identifier.getIdentifier());
344 // public Identifier addIdentifier(String identifier, DefinedTerm identifierType){
345 // Identifier result = Identifier.NewInstance(this, identifier, identifierType);
350 // public void addIdentifier(Identifier identifier){
351 // if (identifier != null){
352 // identifier.setIdentifiedObj(this);
353 // getIdentifiers().add(identifier);
357 // public void removeIdentifier(Identifier identifier){
358 // if (identifier != null){
359 // logger.warn("TODO");
360 //// identifier.setExtendedObj(null);
361 // getIdentifiers().remove(identifier);
366 public Set
<Extension
> getExtensions(){
367 if(extensions
== null) {
368 this.extensions
= new HashSet
<Extension
>();
370 return this.extensions
;
374 * @return a Set of extension value strings
376 public Set
<String
> getExtensions(ExtensionType type
){
377 return getExtensions(type
.getUuid());
380 * @param extensionTypeUuid
381 * @return a Set of extension value strings
383 public Set
<String
> getExtensions(UUID extensionTypeUuid
){
384 Set
<String
> result
= new HashSet
<String
>();
385 for (Extension extension
: getExtensions()){
386 if (extension
.getType().getUuid().equals(extensionTypeUuid
)){
387 result
.add(extension
.getValue());
393 public void addExtension(String value
, ExtensionType extensionType
){
394 Extension
.NewInstance(this, value
, extensionType
);
398 public void addExtension(Extension extension
){
399 if (extension
!= null){
400 extension
.setExtendedObj(this);
401 getExtensions().add(extension
);
405 public void removeExtension(Extension extension
){
406 if (extension
!= null){
407 extension
.setExtendedObj(null);
408 getExtensions().remove(extension
);
413 public boolean isProtectedTitleCache() {
414 return protectedTitleCache
;
418 public void setProtectedTitleCache(boolean protectedTitleCache
) {
419 this.protectedTitleCache
= protectedTitleCache
;
423 public Set
<IdentifiableSource
> getSources() {
424 if(sources
== null) {
425 this.sources
= new HashSet
<IdentifiableSource
>();
431 public void addSource(IdentifiableSource source
) {
433 IdentifiableEntity
<?
> oldSourcedObj
= source
.getSourcedObj();
434 if (oldSourcedObj
!= null && oldSourcedObj
!= this){
435 oldSourcedObj
.getSources().remove(source
);
437 getSources().add(source
);
438 source
.setSourcedObj(this);
443 public IdentifiableSource
addSource(OriginalSourceType type
, String id
, String idNamespace
, Reference citation
, String microCitation
) {
444 if (id
== null && idNamespace
== null && citation
== null && microCitation
== null){
447 IdentifiableSource source
= IdentifiableSource
.NewInstance(type
, id
, idNamespace
, citation
, microCitation
);
454 public IdentifiableSource
addImportSource(String id
, String idNamespace
, Reference
<?
> citation
, String microCitation
) {
455 if (id
== null && idNamespace
== null && citation
== null && microCitation
== null){
458 IdentifiableSource source
= IdentifiableSource
.NewInstance(OriginalSourceType
.Import
, id
, idNamespace
, citation
, microCitation
);
465 public void removeSource(IdentifiableSource source
) {
466 getSources().remove(source
);
469 //******************************** TO STRING *****************************************************/
472 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#toString()
475 public String
toString() {
477 if (titleCache
== null){
478 result
= super.toString();
480 result
= this.titleCache
;
486 public int compareTo(IdentifiableEntity identifiableEntity
) {
490 if (identifiableEntity
== null) {
491 throw new NullPointerException("Cannot compare to null.");
494 // First, compare the name cache.
495 // TODO: Avoid using instanceof operator
496 // Use Class.getDeclaredMethod() instead to find out whether class has getNameCache() method?
498 String specifiedNameCache
= "";
499 String thisNameCache
= "";
500 String specifiedTitleCache
= "";
501 String thisTitleCache
= "";
502 String specifiedReferenceTitleCache
= "";
503 String thisReferenceTitleCache
= "";
504 String thisGenusString
= "";
505 String specifiedGenusString
= "";
506 int thisrank_order
= 0;
508 //TODO we can remove all the deproxies here except for the first one
509 identifiableEntity
= HibernateProxyHelper
.deproxy(identifiableEntity
, IdentifiableEntity
.class);
510 if(identifiableEntity
instanceof NonViralName
) {
511 specifiedNameCache
= HibernateProxyHelper
.deproxy(identifiableEntity
, NonViralName
.class).getNameCache();
512 specifiedTitleCache
= identifiableEntity
.getTitleCache();
513 if (identifiableEntity
instanceof BotanicalName
){
514 if (((BotanicalName
)identifiableEntity
).isAutonym()){
515 boolean isProtected
= false;
516 String oldNameCache
= ((BotanicalName
) identifiableEntity
).getNameCache();
517 if ( ((BotanicalName
)identifiableEntity
).isProtectedNameCache()){
520 ((BotanicalName
)identifiableEntity
).setProtectedNameCache(false);
521 ((BotanicalName
)identifiableEntity
).setNameCache(null, false);
522 specifiedNameCache
= ((BotanicalName
) identifiableEntity
).getNameCache();
523 ((BotanicalName
)identifiableEntity
).setNameCache(oldNameCache
, isProtected
);
528 } else if(identifiableEntity
instanceof TaxonBase
) {
529 TaxonBase taxonBase
= HibernateProxyHelper
.deproxy(identifiableEntity
, TaxonBase
.class);
531 TaxonNameBase
<?
,?
> taxonNameBase
= taxonBase
.getName();
534 NonViralName nonViralName
= HibernateProxyHelper
.deproxy(taxonNameBase
, NonViralName
.class);
535 specifiedNameCache
= nonViralName
.getNameCache();
536 specifiedTitleCache
= taxonNameBase
.getTitleCache();
538 specifiedReferenceTitleCache
= ((TaxonBase
)identifiableEntity
).getSec().getTitleCache();
539 Reference reference
= taxonBase
.getSec();
540 if (reference
!= null) {
541 reference
= HibernateProxyHelper
.deproxy(reference
, Reference
.class);
542 specifiedReferenceTitleCache
= reference
.getTitleCache();
546 if(this.isInstanceOf(NonViralName
.class)) {
547 thisNameCache
= HibernateProxyHelper
.deproxy(this, NonViralName
.class).getNameCache();
548 thisTitleCache
= getTitleCache();
550 if (this instanceof BotanicalName
){
551 if (((BotanicalName
)this).isAutonym()){
552 boolean isProtected
= false;
553 String oldNameCache
= ((BotanicalName
) this).getNameCache();
554 if ( ((BotanicalName
)this).isProtectedNameCache()){
557 ((BotanicalName
)this).setProtectedNameCache(false);
558 ((BotanicalName
)this).setNameCache(null, false);
559 thisNameCache
= ((BotanicalName
) this).getNameCache();
560 ((BotanicalName
)this).setNameCache(oldNameCache
, isProtected
);
563 } else if(this.isInstanceOf(TaxonBase
.class)) {
564 TaxonNameBase
<?
,?
> taxonNameBase
= HibernateProxyHelper
.deproxy(this, TaxonBase
.class).getName();
565 NonViralName nonViralName
= HibernateProxyHelper
.deproxy(taxonNameBase
, NonViralName
.class);
566 thisNameCache
= nonViralName
.getNameCache();
567 thisTitleCache
= taxonNameBase
.getTitleCache();
568 thisReferenceTitleCache
= ((TaxonBase
)this).getSec().getTitleCache();
569 thisGenusString
= nonViralName
.getGenusOrUninomial();
572 // Compare name cache of taxon names
576 if (!specifiedNameCache
.equals("") && !thisNameCache
.equals("")) {
577 result
= thisNameCache
.compareTo(specifiedNameCache
);
580 // Compare title cache of taxon names
582 if ((result
== 0) && (!specifiedTitleCache
.equals("") || !thisTitleCache
.equals(""))) {
583 result
= thisTitleCache
.compareTo(specifiedTitleCache
);
586 // Compare title cache of taxon references
588 if ((result
== 0) && (!specifiedReferenceTitleCache
.equals("") || !thisReferenceTitleCache
.equals(""))) {
589 result
= thisReferenceTitleCache
.compareTo(specifiedReferenceTitleCache
);
596 * Returns the {@link eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy cache strategy} used to generate
597 * several strings corresponding to <i>this</i> identifiable entity
598 * (in particular taxon name caches and author strings).
600 * @return the cache strategy used for <i>this</i> identifiable entity
601 * @see eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy
603 public S
getCacheStrategy() {
604 return this.cacheStrategy
;
607 * @see #getCacheStrategy()
610 public void setCacheStrategy(S cacheStrategy
) {
611 this.cacheStrategy
= cacheStrategy
;
615 public String
generateTitle() {
616 if (getCacheStrategy() == null){
617 //logger.warn("No CacheStrategy defined for "+ this.getClass() + ": " + this.getUuid());
618 return this.getClass() + ": " + this.getUuid();
620 return getCacheStrategy().getTitleCache(this);
624 //****************** CLONE ************************************************/
627 * @see eu.etaxonomy.cdm.model.common.AnnotatableEntity#clone()
630 public Object
clone() throws CloneNotSupportedException
{
631 IdentifiableEntity result
= (IdentifiableEntity
)super.clone();
634 result
.extensions
= new HashSet
<Extension
>();
635 for (Extension extension
: getExtensions() ){
636 Extension newExtension
= (Extension
)extension
.clone();
637 result
.addExtension(newExtension
);
641 result
.sources
= new HashSet
<IdentifiableSource
>();
642 for (IdentifiableSource source
: getSources()){
643 IdentifiableSource newSource
= (IdentifiableSource
)source
.clone();
644 result
.addSource(newSource
);
648 result
.rights
= new HashSet
<Rights
>();
649 for(Rights rights
: getRights()) {
650 result
.addRights(rights
);
655 result
.credits
= new ArrayList
<Credit
>();
656 for(Credit credit
: getCredits()) {
657 result
.addCredit(credit
);
660 //no changes to: lsid, titleCache, protectedTitleCache
663 if (! protectedTitleCache
){
664 result
.titleCache
= null;
667 result
.initListener();