Fixes an issue where update of title caches did not work for cloned instances.
[cdmlib.git] / cdmlib-model / src / main / java / eu / etaxonomy / cdm / model / common / IdentifiableEntity.java
1 /**
2 * Copyright (C) 2007 EDIT
3 * European Distributed Institute of Taxonomy
4 * http://www.e-taxonomy.eu
5 *
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.
8 */
9
10 package eu.etaxonomy.cdm.model.common;
11
12
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;
18 import java.util.Set;
19
20 import javax.persistence.Column;
21 import javax.persistence.Embedded;
22 import javax.persistence.FetchType;
23 import javax.persistence.MappedSuperclass;
24 import javax.persistence.OneToMany;
25 import javax.persistence.Transient;
26 import javax.validation.constraints.NotNull;
27 import javax.validation.constraints.Size;
28 import javax.xml.bind.annotation.XmlAccessType;
29 import javax.xml.bind.annotation.XmlAccessorType;
30 import javax.xml.bind.annotation.XmlElement;
31 import javax.xml.bind.annotation.XmlElementWrapper;
32 import javax.xml.bind.annotation.XmlTransient;
33 import javax.xml.bind.annotation.XmlType;
34 import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
35
36 import org.apache.log4j.Logger;
37 import org.hibernate.annotations.Cascade;
38 import org.hibernate.annotations.CascadeType;
39 import org.hibernate.annotations.IndexColumn;
40 import org.hibernate.search.annotations.FieldBridge;
41 import org.hibernate.validator.constraints.NotEmpty;
42
43 import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
44 import eu.etaxonomy.cdm.hibernate.StripHtmlBridge;
45 import eu.etaxonomy.cdm.jaxb.FormattedTextAdapter;
46 import eu.etaxonomy.cdm.jaxb.LSIDAdapter;
47 import eu.etaxonomy.cdm.model.media.Rights;
48 import eu.etaxonomy.cdm.model.name.NonViralName;
49 import eu.etaxonomy.cdm.model.name.TaxonNameBase;
50 import eu.etaxonomy.cdm.model.reference.Reference;
51 import eu.etaxonomy.cdm.model.taxon.TaxonBase;
52 import eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy;
53 import eu.etaxonomy.cdm.strategy.match.Match;
54 import eu.etaxonomy.cdm.strategy.match.Match.ReplaceMode;
55 import eu.etaxonomy.cdm.strategy.match.MatchMode;
56 import eu.etaxonomy.cdm.strategy.merge.Merge;
57 import eu.etaxonomy.cdm.strategy.merge.MergeMode;
58 import eu.etaxonomy.cdm.validation.Level2;
59
60 /**
61 * 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.
62 * All subclasses inherit the ability to store additional properties that are stored as {@link Extension Extensions}, basically a string value with a type term.
63 * Any number of right statements can be attached as well as multiple {@link OriginalSourceBase} objects.
64 * 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).
65 * A Taxon for example that was taken from 2 sources like FaunaEuropaea and IPNI would have two originalSource objects.
66 * 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.
67 *
68 * @author m.doering
69 * @version 1.0
70 * @created 08-Nov-2007 13:06:27
71 */
72 @XmlAccessorType(XmlAccessType.FIELD)
73 @XmlType(name = "IdentifiableEntity", propOrder = {
74 "lsid",
75 "titleCache",
76 "protectedTitleCache",
77 "rights",
78 "extensions",
79 "credits",
80 "sources"
81 })
82 @MappedSuperclass
83 public abstract class IdentifiableEntity<S extends IIdentifiableEntityCacheStrategy> extends AnnotatableEntity
84 implements IIdentifiableEntity /*, ISourceable<IdentifiableSource> */ {
85 private static final long serialVersionUID = -5610995424730659058L;
86 private static final Logger logger = Logger.getLogger(IdentifiableEntity.class);
87
88 @XmlTransient
89 public static final boolean PROTECTED = true;
90 @XmlTransient
91 public static final boolean NOT_PROTECTED = false;
92
93 @XmlElement(name = "LSID", type = String.class)
94 @XmlJavaTypeAdapter(LSIDAdapter.class)
95 @Embedded
96 private LSID lsid;
97
98 @XmlElement(name = "TitleCache", required = true)
99 @XmlJavaTypeAdapter(FormattedTextAdapter.class)
100 @Column(length=255, name="titleCache")
101
102 @FieldBridge(impl=StripHtmlBridge.class)
103 @Match(value=MatchMode.CACHE, cacheReplaceMode=ReplaceMode.ALL)
104 @NotEmpty(groups = Level2.class) // implictly NotNull
105 @Size(max = 255)
106 protected String titleCache;
107
108 //if true titleCache will not be automatically generated/updated
109 @XmlElement(name = "ProtectedTitleCache")
110 protected boolean protectedTitleCache;
111
112 @XmlElementWrapper(name = "Rights", nillable = true)
113 @XmlElement(name = "Rights")
114 @OneToMany(fetch = FetchType.LAZY)
115 @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE, CascadeType.DELETE_ORPHAN})
116 //TODO
117 @Merge(MergeMode.ADD_CLONE)
118 @NotNull
119 private Set<Rights> rights = new HashSet<Rights>();
120
121 @XmlElementWrapper(name = "Credits", nillable = true)
122 @XmlElement(name = "Credit")
123 @IndexColumn(name="sortIndex", base = 0)
124 @OneToMany(fetch = FetchType.LAZY)
125 @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE, CascadeType.DELETE_ORPHAN})
126 //TODO
127 @Merge(MergeMode.ADD_CLONE)
128 @NotNull
129 private List<Credit> credits = new ArrayList<Credit>();
130
131 @XmlElementWrapper(name = "Extensions", nillable = true)
132 @XmlElement(name = "Extension")
133 @OneToMany(fetch = FetchType.LAZY)
134 @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE, CascadeType.DELETE_ORPHAN})
135 @Merge(MergeMode.ADD_CLONE)
136 @NotNull
137 private Set<Extension> extensions = new HashSet<Extension>();
138
139 @XmlElementWrapper(name = "Sources", nillable = true)
140 @XmlElement(name = "IdentifiableSource")
141 @OneToMany(fetch = FetchType.LAZY)
142 @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE, CascadeType.DELETE_ORPHAN})
143 @Merge(MergeMode.ADD_CLONE)
144 @NotNull
145 private Set<IdentifiableSource> sources = new HashSet<IdentifiableSource>();
146
147 @XmlTransient
148 @Transient
149 protected S cacheStrategy;
150
151 protected IdentifiableEntity(){
152 initListener();
153 }
154
155 protected void initListener(){
156 PropertyChangeListener listener = new PropertyChangeListener() {
157 public void propertyChange(PropertyChangeEvent e) {
158 if (!e.getPropertyName().equals("titleCache") && ! isProtectedTitleCache()){
159 titleCache = null;
160 }
161 }
162 };
163 addPropertyChangeListener(listener);
164 }
165
166 /**
167 * By default, we expect most cdm objects to be abstract things
168 * i.e. unable to return a data representation.
169 *
170 * Specific subclasses (e.g. Sequence) can override if necessary.
171 */
172 public byte[] getData() {
173 return null;
174 }
175
176 //******************************** CACHE *****************************************************/
177
178
179 /* (non-Javadoc)
180 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#getTitleCache()
181 */
182 // @Transient - must not be transient, since this property needs to to be included in all serializations produced by the remote layer
183 public String getTitleCache(){
184 if (protectedTitleCache){
185 return this.titleCache;
186 }
187 // is title dirty, i.e. equal NULL?
188 if (titleCache == null){
189 this.titleCache = generateTitle();
190 this.titleCache = getTruncatedCache(this.titleCache) ;
191 }
192 return titleCache;
193 }
194 /* (non-Javadoc)
195 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#setTitleCache(java.lang.String)
196 */
197 public void setTitleCache(String titleCache){
198 this.titleCache = titleCache;
199 }
200
201 /* (non-Javadoc)
202 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#setTitleCache(java.lang.String, boolean)
203 */
204 public void setTitleCache(String titleCache, boolean protectCache){
205 titleCache = getTruncatedCache(titleCache);
206 this.titleCache = titleCache;
207 this.protectedTitleCache = protectCache;
208 }
209
210
211 /**
212 * @param cache
213 * @return
214 */
215 @Transient
216 protected String getTruncatedCache(String cache) {
217 if (cache != null && cache.length() > 255){
218 logger.warn("Truncation of cache: " + this.toString() + "/" + cache);
219 cache = cache.substring(0, 252) + "...";
220 }
221 return cache;
222 }
223
224 //**************************************************************************************
225
226 /* (non-Javadoc)
227 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#getLsid()
228 */
229 public LSID getLsid(){
230 return this.lsid;
231 }
232 /* (non-Javadoc)
233 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#setLsid(java.lang.String)
234 */
235 public void setLsid(LSID lsid){
236 this.lsid = lsid;
237 }
238 /* (non-Javadoc)
239 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#getRights()
240 */
241 public Set<Rights> getRights() {
242 if(rights == null) {
243 this.rights = new HashSet<Rights>();
244 }
245 return this.rights;
246 }
247
248 /* (non-Javadoc)
249 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#addRights(eu.etaxonomy.cdm.model.media.Rights)
250 */
251 public void addRights(Rights right){
252 getRights().add(right);
253 }
254 /* (non-Javadoc)
255 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#removeRights(eu.etaxonomy.cdm.model.media.Rights)
256 */
257 public void removeRights(Rights right){
258 getRights().remove(right);
259 }
260
261
262 public List<Credit> getCredits() {
263 if(credits == null) {
264 this.credits = new ArrayList<Credit>();
265 }
266 return this.credits;
267 }
268
269 /* (non-Javadoc)
270 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#getCredits(int)
271 */
272 public Credit getCredits(Integer index){
273 return getCredits().get(index);
274 }
275
276 /* (non-Javadoc)
277 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#addCredit(eu.etaxonomy.cdm.model.common.Credit)
278 */
279 public void addCredit(Credit credit){
280 getCredits().add(credit);
281 }
282
283
284 /* (non-Javadoc)
285 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#addCredit(eu.etaxonomy.cdm.model.common.Credit, int)
286 */
287 public void addCredit(Credit credit, int index){
288 getCredits().add(index, credit);
289 }
290
291 /* (non-Javadoc)
292 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#removeCredit(eu.etaxonomy.cdm.model.common.Credit)
293 */
294 public void removeCredit(Credit credit){
295 getCredits().remove(credit);
296 }
297
298 /* (non-Javadoc)
299 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#removeCredit(int)
300 */
301 public void removeCredit(int index){
302 getCredits().remove(index);
303 }
304
305
306 /* (non-Javadoc)
307 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#getExtensions()
308 */
309 public Set<Extension> getExtensions(){
310 if(extensions == null) {
311 this.extensions = new HashSet<Extension>();
312 }
313 return this.extensions;
314 }
315
316 public void addExtension(String value, ExtensionType extensionType){
317 Extension.NewInstance(this, value, extensionType);
318 }
319
320 /* (non-Javadoc)
321 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#addExtension(eu.etaxonomy.cdm.model.common.Extension)
322 */
323 public void addExtension(Extension extension){
324 if (extension != null){
325 extension.setExtendedObj(this);
326 getExtensions().add(extension);
327 }
328 }
329 /* (non-Javadoc)
330 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#removeExtension(eu.etaxonomy.cdm.model.common.Extension)
331 */
332 public void removeExtension(Extension extension){
333 if (extension != null){
334 extension.setExtendedObj(null);
335 getExtensions().remove(extension);
336 }
337 }
338
339
340 /* (non-Javadoc)
341 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#isProtectedTitleCache()
342 */
343 public boolean isProtectedTitleCache() {
344 return protectedTitleCache;
345 }
346
347 /* (non-Javadoc)
348 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#setProtectedTitleCache(boolean)
349 */
350 public void setProtectedTitleCache(boolean protectedTitleCache) {
351 this.protectedTitleCache = protectedTitleCache;
352 }
353
354 /* (non-Javadoc)
355 * @see eu.etaxonomy.cdm.model.common.ISourceable#getSources()
356 */
357 public Set<IdentifiableSource> getSources() {
358 if(sources == null) {
359 this.sources = new HashSet<IdentifiableSource>();
360 }
361 return this.sources;
362 }
363
364 /* (non-Javadoc)
365 * @see eu.etaxonomy.cdm.model.common.ISourceable#addSource(eu.etaxonomy.cdm.model.common.OriginalSourceBase)
366 */
367 public void addSource(IdentifiableSource source) {
368 if (source != null){
369 IdentifiableEntity oldSourcedObj = source.getSourcedObj();
370 if (oldSourcedObj != null && oldSourcedObj != this){
371 oldSourcedObj.getSources().remove(source);
372 }
373 getSources().add(source);
374 source.setSourcedObj(this);
375 }
376 }
377
378 /* (non-Javadoc)
379 * @see eu.etaxonomy.cdm.model.common.ISourceable#addSource(java.lang.String, java.lang.String, eu.etaxonomy.cdm.model.reference.Reference, java.lang.String)
380 */
381 public IdentifiableSource addSource(String id, String idNamespace, Reference citation, String microCitation) {
382 if (id == null && idNamespace == null && citation == null && microCitation == null){
383 return null;
384 }
385 IdentifiableSource source = IdentifiableSource.NewInstance(id, idNamespace, citation, microCitation);
386 addSource(source);
387 return source;
388 }
389
390 /* (non-Javadoc)
391 * @see eu.etaxonomy.cdm.model.common.ISourceable#removeSource(eu.etaxonomy.cdm.model.common.IOriginalSource)
392 */
393 public void removeSource(IdentifiableSource source) {
394 getSources().remove(source);
395 }
396
397 //******************************** TO STRING *****************************************************/
398
399 /* (non-Javadoc)
400 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#toString()
401 */
402 @Override
403 public String toString() {
404 String result;
405 if (titleCache == null){
406 result = super.toString();
407 }else{
408 result = this.titleCache;
409 }
410 return result;
411 }
412
413
414 public int compareTo(IdentifiableEntity identifiableEntity) {
415
416 int result = 0;
417
418 if (identifiableEntity == null) {
419 throw new NullPointerException("Cannot compare to null.");
420 }
421
422 // First, compare the name cache.
423 // TODO: Avoid using instanceof operator
424 // Use Class.getDeclaredMethod() instead to find out whether class has getNameCache() method?
425
426 String specifiedNameCache = "";
427 String thisNameCache = "";
428 String specifiedTitleCache = "";
429 String thisTitleCache = "";
430 String specifiedReferenceTitleCache = "";
431 String thisReferenceTitleCache = "";
432 String thisGenusString = "";
433 String specifiedGenusString = "";
434 int thisrank_order = 0;
435
436 if(identifiableEntity instanceof NonViralName) {
437 specifiedNameCache = HibernateProxyHelper.deproxy(identifiableEntity, NonViralName.class).getNameCache();
438 specifiedTitleCache = identifiableEntity.getTitleCache();
439
440 } else if(identifiableEntity instanceof TaxonBase) {
441 TaxonBase taxonBase = HibernateProxyHelper.deproxy(identifiableEntity, TaxonBase.class);
442
443 TaxonNameBase<?,?> taxonNameBase = taxonBase.getName();
444
445
446 NonViralName nonViralName = HibernateProxyHelper.deproxy(taxonNameBase, NonViralName.class);
447 specifiedNameCache = nonViralName.getNameCache();
448 specifiedTitleCache = taxonNameBase.getTitleCache();
449
450 specifiedReferenceTitleCache = ((TaxonBase)identifiableEntity).getSec().getTitleCache();
451 Reference reference = taxonBase.getSec();
452 if (reference != null) {
453 reference = HibernateProxyHelper.deproxy(reference, Reference.class);
454 specifiedReferenceTitleCache = reference.getTitleCache();
455 }
456 }
457
458 if(this instanceof NonViralName) {
459 thisNameCache = HibernateProxyHelper.deproxy(this, NonViralName.class).getNameCache();
460 thisTitleCache = getTitleCache();
461 } else if(this instanceof TaxonBase) {
462 TaxonNameBase<?,?> taxonNameBase= HibernateProxyHelper.deproxy(this, TaxonBase.class).getName();
463 NonViralName nonViralName = HibernateProxyHelper.deproxy(taxonNameBase, NonViralName.class);
464 thisNameCache = nonViralName.getNameCache();
465 thisTitleCache = taxonNameBase.getTitleCache();
466 thisReferenceTitleCache = getTitleCache();
467 thisGenusString = nonViralName.getGenusOrUninomial();
468 }
469
470 // Compare name cache of taxon names
471
472 if (!specifiedNameCache.equals("") && !thisNameCache.equals("")) {
473 result = thisNameCache.compareTo(specifiedNameCache);
474 }
475
476 // Compare title cache of taxon names
477
478 if ((result == 0) && (!specifiedTitleCache.equals("") || !thisTitleCache.equals(""))) {
479 result = thisTitleCache.compareTo(specifiedTitleCache);
480 }
481
482 // Compare title cache of taxon references
483
484 if ((result == 0) && (!specifiedReferenceTitleCache.equals("") || !thisReferenceTitleCache.equals(""))) {
485 result = thisReferenceTitleCache.compareTo(specifiedReferenceTitleCache);
486 }
487
488 return result;
489 }
490
491 /**
492 * Returns the {@link eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy cache strategy} used to generate
493 * several strings corresponding to <i>this</i> identifiable entity
494 * (in particular taxon name caches and author strings).
495 *
496 * @return the cache strategy used for <i>this</i> identifiable entity
497 * @see eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy
498 */
499 public S getCacheStrategy() {
500 return this.cacheStrategy;
501 }
502 /**
503 * @see #getCacheStrategy()
504 */
505
506 public void setCacheStrategy(S cacheStrategy) {
507 this.cacheStrategy = cacheStrategy;
508 }
509
510 public String generateTitle() {
511 if (cacheStrategy == null){
512 //logger.warn("No CacheStrategy defined for "+ this.getClass() + ": " + this.getUuid());
513 return this.getClass() + ": " + this.getUuid();
514 }else{
515 return cacheStrategy.getTitleCache(this);
516 }
517 }
518
519 //****************** CLONE ************************************************/
520
521 /* (non-Javadoc)
522 * @see eu.etaxonomy.cdm.model.common.AnnotatableEntity#clone()
523 */
524 @Override
525 public Object clone() throws CloneNotSupportedException{
526 IdentifiableEntity result = (IdentifiableEntity)super.clone();
527
528 //Extensions
529 result.extensions = new HashSet<Extension>();
530 for (Extension extension : getExtensions() ){
531 Extension newExtension = (Extension)extension.clone();
532 result.addExtension(newExtension);
533 }
534
535 //OriginalSources
536 result.sources = new HashSet<IdentifiableSource>();
537 for (IdentifiableSource source : getSources()){
538 IdentifiableSource newSource = (IdentifiableSource)source.clone();
539 result.addSource(newSource);
540 }
541
542 //Rights
543 result.rights = new HashSet<Rights>();
544 for(Rights rights : getRights()) {
545 result.addRights(rights);
546 }
547
548
549 //Credits
550 result.credits = new ArrayList<Credit>();
551 for(Credit credit : getCredits()) {
552 result.addCredit(credit);
553 }
554
555 //no changes to: lsid, titleCache, protectedTitleCache
556
557 //empty titleCache
558 if (! protectedTitleCache){
559 result.titleCache = null;
560 }
561
562 result.initListener();
563 return result;
564 }
565
566
567 }