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(length
=255, 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
= "Sources", nillable
= true)
151 @XmlElement(name
= "IdentifiableSource")
152 @OneToMany(fetch
= FetchType
.LAZY
, orphanRemoval
=true)
153 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
, CascadeType
.DELETE
})
154 @Merge(MergeMode
.ADD_CLONE
)
156 private Set
<IdentifiableSource
> sources
= new HashSet
<IdentifiableSource
>();
160 protected S cacheStrategy
;
162 protected IdentifiableEntity(){
166 protected void initListener(){
167 PropertyChangeListener listener
= new PropertyChangeListener() {
169 public void propertyChange(PropertyChangeEvent e
) {
170 if (!e
.getPropertyName().equals("titleCache") && !e
.getPropertyName().equals("cacheStrategy") && ! isProtectedTitleCache()){
175 addPropertyChangeListener(listener
);
179 * By default, we expect most cdm objects to be abstract things
180 * i.e. unable to return a data representation.
182 * Specific subclasses (e.g. Sequence) can override if necessary.
185 public byte[] getData() {
189 //******************************** CACHE *****************************************************/
193 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#getTitleCache()
195 // @Transient - must not be transient, since this property needs to to be included in all serializations produced by the remote layer
197 public String
getTitleCache(){
198 if (protectedTitleCache
){
199 return this.titleCache
;
201 // is title dirty, i.e. equal NULL?
202 if (titleCache
== null){
203 this.titleCache
= generateTitle();
204 this.titleCache
= getTruncatedCache(this.titleCache
) ;
210 * The titleCache will be regenerated from scratch if not protected
211 * @return <code>true</code> if title cache was regenerated, <code>false</code> otherwise
213 protected boolean regenerateTitleCache() {
214 if (!protectedTitleCache
) {
215 this.titleCache
= null;
218 return protectedTitleCache
;
223 public void setTitleCache(String titleCache
){
224 //TODO shouldn't we call setTitleCache(String, boolean),but is this conformant with Java Bean Specification?
225 this.titleCache
= getTruncatedCache(titleCache
);
229 public void setTitleCache(String titleCache
, boolean protectCache
){
230 titleCache
= getTruncatedCache(titleCache
);
231 this.titleCache
= titleCache
;
232 this.protectedTitleCache
= protectCache
;
241 protected String
getTruncatedCache(String cache
) {
243 if (cache
!= null && cache
.length() > maxLength
){
244 logger
.warn("Truncation of cache: " + this.toString() + "/" + cache
);
245 cache
= cache
.substring(0, maxLength
- 4) + "..."; //TODO do we need -4 or is -3 enough
250 //**************************************************************************************
253 public LSID
getLsid(){
257 public void setLsid(LSID lsid
){
261 public Set
<Rights
> getRights() {
263 this.rights
= new HashSet
<Rights
>();
269 public void addRights(Rights right
){
270 getRights().add(right
);
273 public void removeRights(Rights right
){
274 getRights().remove(right
);
279 public List
<Credit
> getCredits() {
280 if(credits
== null) {
281 this.credits
= new ArrayList
<Credit
>();
287 public Credit
getCredits(Integer index
){
288 return getCredits().get(index
);
292 public void addCredit(Credit credit
){
293 getCredits().add(credit
);
298 public void addCredit(Credit credit
, int index
){
299 getCredits().add(index
, credit
);
303 public void removeCredit(Credit credit
){
304 getCredits().remove(credit
);
308 public void removeCredit(int index
){
309 getCredits().remove(index
);
313 public Set
<Extension
> getExtensions(){
314 if(extensions
== null) {
315 this.extensions
= new HashSet
<Extension
>();
317 return this.extensions
;
321 * @return a Set of extension value strings
323 public Set
<String
> getExtensions(ExtensionType type
){
324 return getExtensions(type
.getUuid());
327 * @param extensionTypeUuid
328 * @return a Set of extension value strings
330 public Set
<String
> getExtensions(UUID extensionTypeUuid
){
331 Set
<String
> result
= new HashSet
<String
>();
332 for (Extension extension
: getExtensions()){
333 if (extension
.getType().getUuid().equals(extensionTypeUuid
)){
334 result
.add(extension
.getValue());
340 public void addExtension(String value
, ExtensionType extensionType
){
341 Extension
.NewInstance(this, value
, extensionType
);
345 public void addExtension(Extension extension
){
346 if (extension
!= null){
347 extension
.setExtendedObj(this);
348 getExtensions().add(extension
);
352 public void removeExtension(Extension extension
){
353 if (extension
!= null){
354 extension
.setExtendedObj(null);
355 getExtensions().remove(extension
);
360 public boolean isProtectedTitleCache() {
361 return protectedTitleCache
;
365 public void setProtectedTitleCache(boolean protectedTitleCache
) {
366 this.protectedTitleCache
= protectedTitleCache
;
370 public Set
<IdentifiableSource
> getSources() {
371 if(sources
== null) {
372 this.sources
= new HashSet
<IdentifiableSource
>();
378 public void addSource(IdentifiableSource source
) {
380 IdentifiableEntity
<?
> oldSourcedObj
= source
.getSourcedObj();
381 if (oldSourcedObj
!= null && oldSourcedObj
!= this){
382 oldSourcedObj
.getSources().remove(source
);
384 getSources().add(source
);
385 source
.setSourcedObj(this);
390 public IdentifiableSource
addSource(OriginalSourceType type
, String id
, String idNamespace
, Reference citation
, String microCitation
) {
391 if (id
== null && idNamespace
== null && citation
== null && microCitation
== null){
394 IdentifiableSource source
= IdentifiableSource
.NewInstance(type
, id
, idNamespace
, citation
, microCitation
);
401 public IdentifiableSource
addImportSource(String id
, String idNamespace
, Reference
<?
> citation
, String microCitation
) {
402 if (id
== null && idNamespace
== null && citation
== null && microCitation
== null){
405 IdentifiableSource source
= IdentifiableSource
.NewInstance(OriginalSourceType
.Import
, id
, idNamespace
, citation
, microCitation
);
412 public void removeSource(IdentifiableSource source
) {
413 getSources().remove(source
);
416 //******************************** TO STRING *****************************************************/
419 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#toString()
422 public String
toString() {
424 if (titleCache
== null){
425 result
= super.toString();
427 result
= this.titleCache
;
433 public int compareTo(IdentifiableEntity identifiableEntity
) {
437 if (identifiableEntity
== null) {
438 throw new NullPointerException("Cannot compare to null.");
441 // First, compare the name cache.
442 // TODO: Avoid using instanceof operator
443 // Use Class.getDeclaredMethod() instead to find out whether class has getNameCache() method?
445 String specifiedNameCache
= "";
446 String thisNameCache
= "";
447 String specifiedTitleCache
= "";
448 String thisTitleCache
= "";
449 String specifiedReferenceTitleCache
= "";
450 String thisReferenceTitleCache
= "";
451 String thisGenusString
= "";
452 String specifiedGenusString
= "";
453 int thisrank_order
= 0;
455 //TODO we can remove all the deproxies here except for the first one
456 identifiableEntity
= HibernateProxyHelper
.deproxy(identifiableEntity
, IdentifiableEntity
.class);
457 if(identifiableEntity
instanceof NonViralName
) {
458 specifiedNameCache
= HibernateProxyHelper
.deproxy(identifiableEntity
, NonViralName
.class).getNameCache();
459 specifiedTitleCache
= identifiableEntity
.getTitleCache();
460 if (identifiableEntity
instanceof BotanicalName
){
461 if (((BotanicalName
)identifiableEntity
).isAutonym()){
462 boolean isProtected
= false;
463 String oldNameCache
= ((BotanicalName
) identifiableEntity
).getNameCache();
464 if ( ((BotanicalName
)identifiableEntity
).isProtectedNameCache()){
467 ((BotanicalName
)identifiableEntity
).setProtectedNameCache(false);
468 ((BotanicalName
)identifiableEntity
).setNameCache(null, false);
469 specifiedNameCache
= ((BotanicalName
) identifiableEntity
).getNameCache();
470 ((BotanicalName
)identifiableEntity
).setNameCache(oldNameCache
, isProtected
);
475 } else if(identifiableEntity
instanceof TaxonBase
) {
476 TaxonBase taxonBase
= HibernateProxyHelper
.deproxy(identifiableEntity
, TaxonBase
.class);
478 TaxonNameBase
<?
,?
> taxonNameBase
= taxonBase
.getName();
481 NonViralName nonViralName
= HibernateProxyHelper
.deproxy(taxonNameBase
, NonViralName
.class);
482 specifiedNameCache
= nonViralName
.getNameCache();
483 specifiedTitleCache
= taxonNameBase
.getTitleCache();
485 specifiedReferenceTitleCache
= ((TaxonBase
)identifiableEntity
).getSec().getTitleCache();
486 Reference reference
= taxonBase
.getSec();
487 if (reference
!= null) {
488 reference
= HibernateProxyHelper
.deproxy(reference
, Reference
.class);
489 specifiedReferenceTitleCache
= reference
.getTitleCache();
493 if(this.isInstanceOf(NonViralName
.class)) {
494 thisNameCache
= HibernateProxyHelper
.deproxy(this, NonViralName
.class).getNameCache();
495 thisTitleCache
= getTitleCache();
497 if (this instanceof BotanicalName
){
498 if (((BotanicalName
)this).isAutonym()){
499 boolean isProtected
= false;
500 String oldNameCache
= ((BotanicalName
) this).getNameCache();
501 if ( ((BotanicalName
)this).isProtectedNameCache()){
504 ((BotanicalName
)this).setProtectedNameCache(false);
505 ((BotanicalName
)this).setNameCache(null, false);
506 thisNameCache
= ((BotanicalName
) this).getNameCache();
507 ((BotanicalName
)this).setNameCache(oldNameCache
, isProtected
);
510 } else if(this.isInstanceOf(TaxonBase
.class)) {
511 TaxonNameBase
<?
,?
> taxonNameBase
= HibernateProxyHelper
.deproxy(this, TaxonBase
.class).getName();
512 NonViralName nonViralName
= HibernateProxyHelper
.deproxy(taxonNameBase
, NonViralName
.class);
513 thisNameCache
= nonViralName
.getNameCache();
514 thisTitleCache
= taxonNameBase
.getTitleCache();
515 thisReferenceTitleCache
= ((TaxonBase
)this).getSec().getTitleCache();
516 thisGenusString
= nonViralName
.getGenusOrUninomial();
519 // Compare name cache of taxon names
523 if (!specifiedNameCache
.equals("") && !thisNameCache
.equals("")) {
524 result
= thisNameCache
.compareTo(specifiedNameCache
);
527 // Compare title cache of taxon names
529 if ((result
== 0) && (!specifiedTitleCache
.equals("") || !thisTitleCache
.equals(""))) {
530 result
= thisTitleCache
.compareTo(specifiedTitleCache
);
533 // Compare title cache of taxon references
535 if ((result
== 0) && (!specifiedReferenceTitleCache
.equals("") || !thisReferenceTitleCache
.equals(""))) {
536 result
= thisReferenceTitleCache
.compareTo(specifiedReferenceTitleCache
);
543 * Returns the {@link eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy cache strategy} used to generate
544 * several strings corresponding to <i>this</i> identifiable entity
545 * (in particular taxon name caches and author strings).
547 * @return the cache strategy used for <i>this</i> identifiable entity
548 * @see eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy
550 public S
getCacheStrategy() {
551 return this.cacheStrategy
;
554 * @see #getCacheStrategy()
557 public void setCacheStrategy(S cacheStrategy
) {
558 this.cacheStrategy
= cacheStrategy
;
562 public String
generateTitle() {
563 if (getCacheStrategy() == null){
564 //logger.warn("No CacheStrategy defined for "+ this.getClass() + ": " + this.getUuid());
565 return this.getClass() + ": " + this.getUuid();
567 return getCacheStrategy().getTitleCache(this);
571 //****************** CLONE ************************************************/
574 * @see eu.etaxonomy.cdm.model.common.AnnotatableEntity#clone()
577 public Object
clone() throws CloneNotSupportedException
{
578 IdentifiableEntity result
= (IdentifiableEntity
)super.clone();
581 result
.extensions
= new HashSet
<Extension
>();
582 for (Extension extension
: getExtensions() ){
583 Extension newExtension
= (Extension
)extension
.clone();
584 result
.addExtension(newExtension
);
588 result
.sources
= new HashSet
<IdentifiableSource
>();
589 for (IdentifiableSource source
: getSources()){
590 IdentifiableSource newSource
= (IdentifiableSource
)source
.clone();
591 result
.addSource(newSource
);
595 result
.rights
= new HashSet
<Rights
>();
596 for(Rights rights
: getRights()) {
597 result
.addRights(rights
);
602 result
.credits
= new ArrayList
<Credit
>();
603 for(Credit credit
: getCredits()) {
604 result
.addCredit(credit
);
607 //no changes to: lsid, titleCache, protectedTitleCache
610 if (! protectedTitleCache
){
611 result
.titleCache
= null;
614 result
.initListener();