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
.OrderColumn
;
27 import javax
.persistence
.Transient
;
28 import javax
.validation
.constraints
.NotNull
;
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
.commons
.lang
.StringUtils
;
38 import org
.apache
.log4j
.Logger
;
39 import org
.hibernate
.annotations
.Cascade
;
40 import org
.hibernate
.annotations
.CascadeType
;
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
.Index
;
47 import org
.hibernate
.search
.annotations
.SortableField
;
48 import org
.hibernate
.search
.annotations
.Store
;
49 import org
.hibernate
.validator
.constraints
.NotEmpty
;
51 import eu
.etaxonomy
.cdm
.hibernate
.HibernateProxyHelper
;
52 import eu
.etaxonomy
.cdm
.hibernate
.search
.StripHtmlBridge
;
53 import eu
.etaxonomy
.cdm
.jaxb
.FormattedTextAdapter
;
54 import eu
.etaxonomy
.cdm
.jaxb
.LSIDAdapter
;
55 import eu
.etaxonomy
.cdm
.model
.media
.Rights
;
56 import eu
.etaxonomy
.cdm
.model
.name
.BotanicalName
;
57 import eu
.etaxonomy
.cdm
.model
.name
.NonViralName
;
58 import eu
.etaxonomy
.cdm
.model
.name
.TaxonNameBase
;
59 import eu
.etaxonomy
.cdm
.model
.reference
.Reference
;
60 import eu
.etaxonomy
.cdm
.model
.taxon
.TaxonBase
;
61 import eu
.etaxonomy
.cdm
.strategy
.cache
.common
.IIdentifiableEntityCacheStrategy
;
62 import eu
.etaxonomy
.cdm
.strategy
.match
.Match
;
63 import eu
.etaxonomy
.cdm
.strategy
.match
.Match
.ReplaceMode
;
64 import eu
.etaxonomy
.cdm
.strategy
.match
.MatchMode
;
65 import eu
.etaxonomy
.cdm
.strategy
.merge
.Merge
;
66 import eu
.etaxonomy
.cdm
.strategy
.merge
.MergeMode
;
67 import eu
.etaxonomy
.cdm
.validation
.Level2
;
70 * 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.
71 * All subclasses inherit the ability to store additional properties that are stored as {@link Extension Extensions}, basically a string value with a type term.
72 * Any number of right statements can be attached as well as multiple {@link OriginalSourceBase} objects.
73 * 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).
74 * A Taxon for example that was taken from 2 sources like FaunaEuropaea and IPNI would have two originalSource objects.
75 * 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.
78 * @created 08-Nov-2007 13:06:27
80 @XmlAccessorType(XmlAccessType
.FIELD
)
81 @XmlType(name
= "IdentifiableEntity", propOrder
= {
84 "protectedTitleCache",
93 public abstract class IdentifiableEntity
<S
extends IIdentifiableEntityCacheStrategy
> extends AnnotatableEntity
94 implements IIdentifiableEntity
/*, ISourceable<IdentifiableSource> */ {
95 private static final long serialVersionUID
= 7912083412108359559L;
97 private static final Logger logger
= Logger
.getLogger(IdentifiableEntity
.class);
100 public static final boolean PROTECTED
= true;
102 public static final boolean NOT_PROTECTED
= false;
104 @XmlElement(name
= "LSID", type
= String
.class)
105 @XmlJavaTypeAdapter(LSIDAdapter
.class)
109 @XmlElement(name
= "TitleCache", required
= true)
110 @XmlJavaTypeAdapter(FormattedTextAdapter
.class)
111 @Column(name
="titleCache", length
=800) //see #1592
112 @Match(value
=MatchMode
.CACHE
, cacheReplaceMode
=ReplaceMode
.ALL
)
113 @NotEmpty(groups
= Level2
.class) // implictly NotNull
115 @Field(store
=Store
.YES
),
116 // If the field is only needed for sorting and nothing else, you may configure it as
117 // un-indexed and un-stored, thus avoid unnecessary index growth.
118 @Field(name
= "titleCache__sort", analyze
= Analyze
.NO
, store
=Store
.NO
, index
= Index
.NO
)
120 @SortableField(forField
= "titleCache__sort")
121 @FieldBridge(impl
=StripHtmlBridge
.class)
122 protected String titleCache
;
124 //if true titleCache will not be automatically generated/updated
125 @XmlElement(name
= "ProtectedTitleCache")
126 protected boolean protectedTitleCache
;
128 @XmlElementWrapper(name
= "Rights", nillable
= true)
129 @XmlElement(name
= "Rights")
130 @OneToMany(fetch
= FetchType
.LAZY
, orphanRemoval
=true)
131 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
, CascadeType
.DELETE
})
133 @Merge(MergeMode
.ADD_CLONE
)
135 private Set
<Rights
> rights
= new HashSet
<Rights
>();
137 @XmlElementWrapper(name
= "Credits", nillable
= true)
138 @XmlElement(name
= "Credit")
139 @OrderColumn(name
="sortIndex")
140 @OneToMany(fetch
= FetchType
.LAZY
, orphanRemoval
=true)
141 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
, CascadeType
.DELETE
})
143 @Merge(MergeMode
.ADD_CLONE
)
145 private List
<Credit
> credits
= new ArrayList
<Credit
>();
147 @XmlElementWrapper(name
= "Extensions", nillable
= true)
148 @XmlElement(name
= "Extension")
149 @OneToMany(fetch
= FetchType
.LAZY
, orphanRemoval
=true)
150 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
, CascadeType
.DELETE
})
151 @Merge(MergeMode
.ADD_CLONE
)
153 private Set
<Extension
> extensions
= new HashSet
<Extension
>();
155 @XmlElementWrapper(name
= "Identifiers", nillable
= true)
156 @XmlElement(name
= "Identifier")
157 @OrderColumn(name
="sortIndex")
158 @OneToMany(fetch
= FetchType
.LAZY
, orphanRemoval
=true)
159 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
, CascadeType
.DELETE
})
160 @Merge(MergeMode
.ADD_CLONE
)
162 private List
<Identifier
> identifiers
= new ArrayList
<Identifier
>();
164 @XmlElementWrapper(name
= "Sources", nillable
= true)
165 @XmlElement(name
= "IdentifiableSource")
166 @OneToMany(fetch
= FetchType
.LAZY
, orphanRemoval
=true)
167 @Cascade({CascadeType
.SAVE_UPDATE
, CascadeType
.MERGE
, CascadeType
.DELETE
})
168 @Merge(MergeMode
.ADD_CLONE
)
170 private Set
<IdentifiableSource
> sources
= new HashSet
<IdentifiableSource
>();
174 protected S cacheStrategy
;
176 protected IdentifiableEntity(){
181 public void initListener(){
182 PropertyChangeListener listener
= new PropertyChangeListener() {
184 public void propertyChange(PropertyChangeEvent ev
) {
185 if (! "titleCache".equals(ev
.getPropertyName()) && !"cacheStrategy".equals(ev
.getPropertyName()) && ! isProtectedTitleCache()){
190 addPropertyChangeListener(listener
);
194 * By default, we expect most cdm objects to be abstract things
195 * i.e. unable to return a data representation.
197 * Specific subclasses (e.g. Sequence) can override if necessary.
200 public byte[] getData() {
204 //******************************** CACHE *****************************************************/
206 // @Transient - must not be transient, since this property needs to to be included in all serializations produced by the remote layer
208 public String
getTitleCache(){
209 if (protectedTitleCache
){
210 return this.titleCache
;
212 // is title dirty, i.e. equal NULL?
213 if (titleCache
== null){
214 this.titleCache
= generateTitle();
215 this.titleCache
= getTruncatedCache(this.titleCache
) ;
217 //removed due to #5849
218 // if(StringUtils.isBlank(titleCache)){
219 // titleCache = this.toString();
226 public void setTitleCache(String titleCache
){
227 //TODO shouldn't we call setTitleCache(String, boolean),but is this conformant with Java Bean Specification?
228 this.titleCache
= getTruncatedCache(titleCache
);
232 public void setTitleCache(String titleCache
, boolean protectCache
){
233 titleCache
= getTruncatedCache(titleCache
);
234 this.titleCache
= titleCache
;
235 this.protectedTitleCache
= protectCache
;
243 protected String
getTruncatedCache(String cache
) {
245 if (cache
!= null && cache
.length() > maxLength
){
246 logger
.warn("Truncation of cache: " + this.toString() + "/" + cache
);
247 cache
= cache
.substring(0, maxLength
- 4) + "..."; //TODO do we need -4 or is -3 enough
254 public boolean isProtectedTitleCache() {
255 return protectedTitleCache
;
259 public void setProtectedTitleCache(boolean protectedTitleCache
) {
260 this.protectedTitleCache
= protectedTitleCache
;
265 * @return true, if the current state of the titleCache (without generating it new)
266 * is <code>null</code> or the empty string. This is primarily meant for internal use.
268 public boolean hasEmptyTitleCache(){
269 return this.titleCache
== null || "".equals(this.titleCache
);
272 //**************************************************************************************
275 public LSID
getLsid(){
279 public void setLsid(LSID lsid
){
283 public Set
<Rights
> getRights() {
285 this.rights
= new HashSet
<Rights
>();
291 public void addRights(Rights right
){
292 getRights().add(right
);
295 public void removeRights(Rights right
){
296 getRights().remove(right
);
301 public List
<Credit
> getCredits() {
302 if(credits
== null) {
303 this.credits
= new ArrayList
<Credit
>();
309 public Credit
getCredits(Integer index
){
310 return getCredits().get(index
);
314 public void addCredit(Credit credit
){
315 getCredits().add(credit
);
320 public void addCredit(Credit credit
, int index
){
321 getCredits().add(index
, credit
);
325 public void removeCredit(Credit credit
){
326 getCredits().remove(credit
);
330 public void removeCredit(int index
){
331 getCredits().remove(index
);
335 public boolean replaceCredit(Credit newObject
, Credit oldObject
){
336 return replaceInList(this.credits
, newObject
, oldObject
);
341 public List
<Identifier
> getIdentifiers(){
342 if(this.identifiers
== null) {
343 this.identifiers
= new ArrayList
<Identifier
>();
345 return this.identifiers
;
349 * @return a set of identifier value strings
351 public Set
<String
> getIdentifiers(DefinedTerm type
){
352 return getIdentifiers(type
.getUuid());
355 * @param identifierTypeUuid
356 * @return a set of identifier value strings
358 public Set
<String
> getIdentifiers(UUID identifierTypeUuid
){
359 Set
<String
> result
= new HashSet
<String
>();
360 for (Identifier
<?
> identifier
: getIdentifiers()){
361 if (identifier
.getType().getUuid().equals(identifierTypeUuid
)){
362 result
.add(identifier
.getIdentifier());
369 public Identifier
addIdentifier(String identifier
, DefinedTerm identifierType
){
370 Identifier
<?
> result
= Identifier
.NewInstance(identifier
, identifierType
);
371 addIdentifier(result
);
376 public void addIdentifier(int index
, Identifier identifier
){
377 if (identifier
!= null){
379 int oldIndex
= getIdentifiers().indexOf(identifier
);
381 getIdentifiers().remove(identifier
);
382 if (oldIndex
< index
){
386 getIdentifiers().add(index
, identifier
);
391 public void addIdentifier(Identifier identifier
){
392 addIdentifier(getIdentifiers().size(), identifier
);
396 public void removeIdentifier(Identifier identifier
){
397 if (identifier
!= null){
398 getIdentifiers().remove(identifier
);
402 public void removeIdentifier(int index
){
403 getIdentifiers().remove(index
);
407 public boolean replaceIdentifier(Identifier newObject
, Identifier oldObject
){
408 return replaceInList(this.identifiers
, newObject
, oldObject
);
413 public Set
<Extension
> getExtensions(){
414 if(extensions
== null) {
415 this.extensions
= new HashSet
<Extension
>();
417 return this.extensions
;
421 * @return a Set of extension value strings
423 public Set
<String
> getExtensions(ExtensionType type
){
424 return getExtensions(type
.getUuid());
427 * @param extensionTypeUuid
428 * @return a Set of extension value strings
430 public Set
<String
> getExtensions(UUID extensionTypeUuid
){
431 Set
<String
> result
= new HashSet
<String
>();
432 for (Extension extension
: getExtensions()){
433 if (extension
.getType().getUuid().equals(extensionTypeUuid
)){
434 result
.add(extension
.getValue());
440 public void addExtension(String value
, ExtensionType extensionType
){
441 Extension
.NewInstance(this, value
, extensionType
);
445 public void addExtension(Extension extension
){
446 if (extension
!= null){
447 getExtensions().add(extension
);
451 public void removeExtension(Extension extension
){
452 if (extension
!= null){
453 getExtensions().remove(extension
);
459 public Set
<IdentifiableSource
> getSources() {
460 if(sources
== null) {
461 this.sources
= new HashSet
<IdentifiableSource
>();
467 public void addSource(IdentifiableSource source
) {
469 getSources().add(source
);
474 public void addSources(Set
<IdentifiableSource
> sources
) {
475 if (sources
!= null){
476 for (IdentifiableSource source
: sources
){
477 getSources().add(source
);
483 public void removeSources() {
484 this.sources
.clear();
488 public IdentifiableSource
addSource(OriginalSourceType type
, String id
, String idNamespace
, Reference citation
, String microCitation
) {
489 if (id
== null && idNamespace
== null && citation
== null && microCitation
== null){
492 IdentifiableSource source
= IdentifiableSource
.NewInstance(type
, id
, idNamespace
, citation
, microCitation
);
499 public IdentifiableSource
addImportSource(String id
, String idNamespace
, Reference citation
, String microCitation
) {
500 if (id
== null && idNamespace
== null && citation
== null && microCitation
== null){
503 IdentifiableSource source
= IdentifiableSource
.NewInstance(OriginalSourceType
.Import
, id
, idNamespace
, citation
, microCitation
);
510 public void removeSource(IdentifiableSource source
) {
511 getSources().remove(source
);
514 //******************************** TO STRING *****************************************************/
517 public String
toString() {
519 if (StringUtils
.isBlank(titleCache
)){
520 result
= super.toString();
522 result
= this.titleCache
;
528 public int compareTo(IdentifiableEntity identifiableEntity
) {
532 if (identifiableEntity
== null) {
533 throw new NullPointerException("Cannot compare to null.");
536 // First, compare the name cache.
537 // TODO: Avoid using instanceof operator
538 // Use Class.getDeclaredMethod() instead to find out whether class has getNameCache() method?
540 String specifiedNameCache
= "";
541 String thisNameCache
= "";
542 String specifiedTitleCache
= "";
543 String thisTitleCache
= "";
544 String specifiedReferenceTitleCache
= "";
545 String thisReferenceTitleCache
= "";
546 String thisGenusString
= "";
547 String specifiedGenusString
= "";
548 int thisrank_order
= 0;
549 final String HYBRID_SIGN
= "\u00D7";
550 final String QUOT_SIGN
= "[\\u02BA\\u0022\\u0022]";
551 //TODO we can remove all the deproxies here except for the first one
552 identifiableEntity
= HibernateProxyHelper
.deproxy(identifiableEntity
, IdentifiableEntity
.class);
553 if(identifiableEntity
instanceof NonViralName
) {
554 specifiedNameCache
= HibernateProxyHelper
.deproxy(identifiableEntity
, NonViralName
.class).getNameCache();
555 specifiedTitleCache
= identifiableEntity
.getTitleCache();
556 if (identifiableEntity
instanceof BotanicalName
){
557 if (((BotanicalName
)identifiableEntity
).isAutonym()){
558 boolean isProtected
= false;
559 String oldNameCache
= ((BotanicalName
) identifiableEntity
).getNameCache();
560 if ( ((BotanicalName
)identifiableEntity
).isProtectedNameCache()){
563 ((BotanicalName
)identifiableEntity
).setProtectedNameCache(false);
564 ((BotanicalName
)identifiableEntity
).setNameCache(null, false);
565 specifiedNameCache
= ((BotanicalName
) identifiableEntity
).getNameCache();
566 ((BotanicalName
)identifiableEntity
).setNameCache(oldNameCache
, isProtected
);
571 } else if(identifiableEntity
instanceof TaxonBase
) {
572 TaxonBase taxonBase
= HibernateProxyHelper
.deproxy(identifiableEntity
, TaxonBase
.class);
574 TaxonNameBase
<?
,?
> taxonNameBase
= taxonBase
.getName();
577 NonViralName nonViralName
= HibernateProxyHelper
.deproxy(taxonNameBase
, NonViralName
.class);
578 specifiedNameCache
= nonViralName
.getNameCache();
579 specifiedTitleCache
= taxonNameBase
.getTitleCache();
581 specifiedReferenceTitleCache
= ((TaxonBase
)identifiableEntity
).getSec().getTitleCache();
582 Reference reference
= taxonBase
.getSec();
583 if (reference
!= null) {
584 reference
= HibernateProxyHelper
.deproxy(reference
, Reference
.class);
585 specifiedReferenceTitleCache
= reference
.getTitleCache();
589 if(this.isInstanceOf(NonViralName
.class)) {
590 thisNameCache
= HibernateProxyHelper
.deproxy(this, NonViralName
.class).getNameCache();
591 thisTitleCache
= getTitleCache();
593 if (this instanceof BotanicalName
){
594 if (((BotanicalName
)this).isAutonym()){
595 boolean isProtected
= false;
596 String oldNameCache
= ((BotanicalName
) this).getNameCache();
597 if ( ((BotanicalName
)this).isProtectedNameCache()){
600 ((BotanicalName
)this).setProtectedNameCache(false);
601 ((BotanicalName
)this).setNameCache(null, false);
602 thisNameCache
= ((BotanicalName
) this).getNameCache();
603 ((BotanicalName
)this).setNameCache(oldNameCache
, isProtected
);
606 } else if(this.isInstanceOf(TaxonBase
.class)) {
607 TaxonNameBase
<?
,?
> taxonNameBase
= HibernateProxyHelper
.deproxy(this, TaxonBase
.class).getName();
608 NonViralName nonViralName
= HibernateProxyHelper
.deproxy(taxonNameBase
, NonViralName
.class);
609 thisNameCache
= nonViralName
.getNameCache();
610 thisTitleCache
= taxonNameBase
.getTitleCache();
611 thisReferenceTitleCache
= ((TaxonBase
)this).getSec().getTitleCache();
612 thisGenusString
= nonViralName
.getGenusOrUninomial();
615 // Compare name cache of taxon names
619 if (!specifiedNameCache
.equals("") && !thisNameCache
.equals("")) {
621 thisNameCache
= thisNameCache
.replaceAll(HYBRID_SIGN
, "");
622 thisNameCache
= thisNameCache
.replaceAll(QUOT_SIGN
, "");
625 specifiedNameCache
= specifiedNameCache
.replaceAll(HYBRID_SIGN
, "");
626 specifiedNameCache
= specifiedNameCache
.replaceAll(QUOT_SIGN
, "");
629 result
= thisNameCache
.compareTo(specifiedNameCache
);
632 // Compare title cache of taxon names
634 if ((result
== 0) && (!specifiedTitleCache
.equals("") || !thisTitleCache
.equals(""))) {
635 thisTitleCache
= thisTitleCache
.replaceAll(HYBRID_SIGN
, "");
636 thisTitleCache
= thisTitleCache
.replaceAll(QUOT_SIGN
, "");
638 specifiedTitleCache
= specifiedTitleCache
.replaceAll(HYBRID_SIGN
, "");
639 specifiedTitleCache
= specifiedTitleCache
.replaceAll(QUOT_SIGN
, "");
640 result
= thisTitleCache
.compareTo(specifiedTitleCache
);
643 // Compare title cache of taxon references
645 if ((result
== 0) && (!specifiedReferenceTitleCache
.equals("") || !thisReferenceTitleCache
.equals(""))) {
646 result
= thisReferenceTitleCache
.compareTo(specifiedReferenceTitleCache
);
653 * Returns the {@link eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy cache strategy} used to generate
654 * several strings corresponding to <i>this</i> identifiable entity
655 * (in particular taxon name caches and author strings).
657 * @return the cache strategy used for <i>this</i> identifiable entity
658 * @see eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy
660 public S
getCacheStrategy() {
661 return this.cacheStrategy
;
664 * @see #getCacheStrategy()
667 public void setCacheStrategy(S cacheStrategy
) {
668 this.cacheStrategy
= cacheStrategy
;
672 public String
generateTitle() {
673 if (getCacheStrategy() == null){
674 //logger.warn("No CacheStrategy defined for "+ this.getClass() + ": " + this.getUuid());
675 return this.getClass() + ": " + this.getUuid();
677 return getCacheStrategy().getTitleCache(this);
681 //****************** CLONE ************************************************/
684 public Object
clone() throws CloneNotSupportedException
{
685 IdentifiableEntity
<?
> result
= (IdentifiableEntity
<?
>)super.clone();
688 result
.extensions
= new HashSet
<Extension
>();
689 for (Extension extension
: getExtensions() ){
690 Extension newExtension
= (Extension
)extension
.clone();
691 result
.addExtension(newExtension
);
695 result
.identifiers
= new ArrayList
<Identifier
>();
696 for (Identifier
<?
> identifier
: getIdentifiers() ){
697 Identifier
<?
> newIdentifier
= (Identifier
<?
>)identifier
.clone();
698 result
.addIdentifier(newIdentifier
);
702 result
.sources
= new HashSet
<IdentifiableSource
>();
703 for (IdentifiableSource source
: getSources()){
704 IdentifiableSource newSource
= (IdentifiableSource
)source
.clone();
705 result
.addSource(newSource
);
709 result
.rights
= new HashSet
<Rights
>();
710 for(Rights rights
: getRights()) {
711 Rights newRights
= (Rights
)rights
.clone();
712 result
.addRights(newRights
);
717 result
.credits
= new ArrayList
<Credit
>();
718 for(Credit credit
: getCredits()) {
719 Credit newCredit
= (Credit
)credit
.clone();
720 result
.addCredit(newCredit
);
723 //no changes to: lsid, titleCache, protectedTitleCache
726 if (! protectedTitleCache
){
727 result
.titleCache
= null;
730 result
.initListener();