implement cache strategy for section references and remove generics from reference...
[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 import java.util.UUID;
20
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;
36
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;
48
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;
66
67 /**
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.
74 *
75 * @author m.doering
76 * @version 1.0
77 * @created 08-Nov-2007 13:06:27
78 */
79 @XmlAccessorType(XmlAccessType.FIELD)
80 @XmlType(name = "IdentifiableEntity", propOrder = {
81 "lsid",
82 "titleCache",
83 "protectedTitleCache",
84 "rights",
85 "extensions",
86 "credits",
87 "sources"
88 })
89 @Audited
90 @MappedSuperclass
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);
95
96 @XmlTransient
97 public static final boolean PROTECTED = true;
98 @XmlTransient
99 public static final boolean NOT_PROTECTED = false;
100
101 @XmlElement(name = "LSID", type = String.class)
102 @XmlJavaTypeAdapter(LSIDAdapter.class)
103 @Embedded
104 private LSID lsid;
105
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
112 @Fields({
113 @Field(store=Store.YES),
114 @Field(name = "titleCache__sort", analyze = Analyze.NO, store=Store.YES)
115 })
116 @FieldBridge(impl=StripHtmlBridge.class)
117 protected String titleCache;
118
119 //if true titleCache will not be automatically generated/updated
120 @XmlElement(name = "ProtectedTitleCache")
121 protected boolean protectedTitleCache;
122
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})
127 //TODO
128 @Merge(MergeMode.ADD_CLONE)
129 @NotNull
130 private Set<Rights> rights = new HashSet<Rights>();
131
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})
137 //TODO
138 @Merge(MergeMode.ADD_CLONE)
139 @NotNull
140 private List<Credit> credits = new ArrayList<Credit>();
141
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)
147 @NotNull
148 private Set<Extension> extensions = new HashSet<Extension>();
149
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)
155 @NotNull
156 private Set<IdentifiableSource> sources = new HashSet<IdentifiableSource>();
157
158 @XmlTransient
159 @Transient
160 protected S cacheStrategy;
161
162 protected IdentifiableEntity(){
163 initListener();
164 }
165
166 protected void initListener(){
167 PropertyChangeListener listener = new PropertyChangeListener() {
168 @Override
169 public void propertyChange(PropertyChangeEvent e) {
170 if (!e.getPropertyName().equals("titleCache") && !e.getPropertyName().equals("cacheStrategy") && ! isProtectedTitleCache()){
171 titleCache = null;
172 }
173 }
174 };
175 addPropertyChangeListener(listener);
176 }
177
178 /**
179 * By default, we expect most cdm objects to be abstract things
180 * i.e. unable to return a data representation.
181 *
182 * Specific subclasses (e.g. Sequence) can override if necessary.
183 */
184 @Override
185 public byte[] getData() {
186 return null;
187 }
188
189 //******************************** CACHE *****************************************************/
190
191
192 /* (non-Javadoc)
193 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#getTitleCache()
194 */
195 // @Transient - must not be transient, since this property needs to to be included in all serializations produced by the remote layer
196 @Override
197 public String getTitleCache(){
198 if (protectedTitleCache){
199 return this.titleCache;
200 }
201 // is title dirty, i.e. equal NULL?
202 if (titleCache == null){
203 this.titleCache = generateTitle();
204 this.titleCache = getTruncatedCache(this.titleCache) ;
205 }
206 return titleCache;
207 }
208
209 /**
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
212 */
213 protected boolean regenerateTitleCache() {
214 if (!protectedTitleCache) {
215 this.titleCache = null;
216 getTitleCache();
217 }
218 return protectedTitleCache;
219 }
220
221 @Deprecated
222 @Override
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);
226 }
227
228 @Override
229 public void setTitleCache(String titleCache, boolean protectCache){
230 titleCache = getTruncatedCache(titleCache);
231 this.titleCache = titleCache;
232 this.protectedTitleCache = protectCache;
233 }
234
235
236 /**
237 * @param cache
238 * @return
239 */
240 @Transient
241 protected String getTruncatedCache(String cache) {
242 int maxLength = 800;
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
246 }
247 return cache;
248 }
249
250 //**************************************************************************************
251
252 @Override
253 public LSID getLsid(){
254 return this.lsid;
255 }
256 @Override
257 public void setLsid(LSID lsid){
258 this.lsid = lsid;
259 }
260 @Override
261 public Set<Rights> getRights() {
262 if(rights == null) {
263 this.rights = new HashSet<Rights>();
264 }
265 return this.rights;
266 }
267
268 @Override
269 public void addRights(Rights right){
270 getRights().add(right);
271 }
272 @Override
273 public void removeRights(Rights right){
274 getRights().remove(right);
275 }
276
277
278 @Override
279 public List<Credit> getCredits() {
280 if(credits == null) {
281 this.credits = new ArrayList<Credit>();
282 }
283 return this.credits;
284 }
285
286 @Override
287 public Credit getCredits(Integer index){
288 return getCredits().get(index);
289 }
290
291 @Override
292 public void addCredit(Credit credit){
293 getCredits().add(credit);
294 }
295
296
297 @Override
298 public void addCredit(Credit credit, int index){
299 getCredits().add(index, credit);
300 }
301
302 @Override
303 public void removeCredit(Credit credit){
304 getCredits().remove(credit);
305 }
306
307 @Override
308 public void removeCredit(int index){
309 getCredits().remove(index);
310 }
311
312 @Override
313 public Set<Extension> getExtensions(){
314 if(extensions == null) {
315 this.extensions = new HashSet<Extension>();
316 }
317 return this.extensions;
318 }
319 /**
320 * @param type
321 * @return a Set of extension value strings
322 */
323 public Set<String> getExtensions(ExtensionType type){
324 return getExtensions(type.getUuid());
325 }
326 /**
327 * @param extensionTypeUuid
328 * @return a Set of extension value strings
329 */
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());
335 }
336 }
337 return result;
338 }
339
340 public void addExtension(String value, ExtensionType extensionType){
341 Extension.NewInstance(this, value, extensionType);
342 }
343
344 @Override
345 public void addExtension(Extension extension){
346 if (extension != null){
347 extension.setExtendedObj(this);
348 getExtensions().add(extension);
349 }
350 }
351 @Override
352 public void removeExtension(Extension extension){
353 if (extension != null){
354 extension.setExtendedObj(null);
355 getExtensions().remove(extension);
356 }
357 }
358
359 @Override
360 public boolean isProtectedTitleCache() {
361 return protectedTitleCache;
362 }
363
364 @Override
365 public void setProtectedTitleCache(boolean protectedTitleCache) {
366 this.protectedTitleCache = protectedTitleCache;
367 }
368
369 @Override
370 public Set<IdentifiableSource> getSources() {
371 if(sources == null) {
372 this.sources = new HashSet<IdentifiableSource>();
373 }
374 return this.sources;
375 }
376
377 @Override
378 public void addSource(IdentifiableSource source) {
379 if (source != null){
380 IdentifiableEntity<?> oldSourcedObj = source.getSourcedObj();
381 if (oldSourcedObj != null && oldSourcedObj != this){
382 oldSourcedObj.getSources().remove(source);
383 }
384 getSources().add(source);
385 source.setSourcedObj(this);
386 }
387 }
388
389 @Override
390 public IdentifiableSource addSource(OriginalSourceType type, String id, String idNamespace, Reference citation, String microCitation) {
391 if (id == null && idNamespace == null && citation == null && microCitation == null){
392 return null;
393 }
394 IdentifiableSource source = IdentifiableSource.NewInstance(type, id, idNamespace, citation, microCitation);
395 addSource(source);
396 return source;
397 }
398
399
400 @Override
401 public IdentifiableSource addImportSource(String id, String idNamespace, Reference<?> citation, String microCitation) {
402 if (id == null && idNamespace == null && citation == null && microCitation == null){
403 return null;
404 }
405 IdentifiableSource source = IdentifiableSource.NewInstance(OriginalSourceType.Import, id, idNamespace, citation, microCitation);
406 addSource(source);
407 return source;
408 }
409
410
411 @Override
412 public void removeSource(IdentifiableSource source) {
413 getSources().remove(source);
414 }
415
416 //******************************** TO STRING *****************************************************/
417
418 /* (non-Javadoc)
419 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#toString()
420 */
421 @Override
422 public String toString() {
423 String result;
424 if (titleCache == null){
425 result = super.toString();
426 }else{
427 result = this.titleCache;
428 }
429 return result;
430 }
431
432
433 public int compareTo(IdentifiableEntity identifiableEntity) {
434
435 int result = 0;
436
437 if (identifiableEntity == null) {
438 throw new NullPointerException("Cannot compare to null.");
439 }
440
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?
444
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;
454
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()){
465 isProtected = true;
466 }
467 ((BotanicalName)identifiableEntity).setProtectedNameCache(false);
468 ((BotanicalName)identifiableEntity).setNameCache(null, false);
469 specifiedNameCache = ((BotanicalName) identifiableEntity).getNameCache();
470 ((BotanicalName)identifiableEntity).setNameCache(oldNameCache, isProtected);
471
472 }
473 }
474
475 } else if(identifiableEntity instanceof TaxonBase) {
476 TaxonBase taxonBase = HibernateProxyHelper.deproxy(identifiableEntity, TaxonBase.class);
477
478 TaxonNameBase<?,?> taxonNameBase = taxonBase.getName();
479
480
481 NonViralName nonViralName = HibernateProxyHelper.deproxy(taxonNameBase, NonViralName.class);
482 specifiedNameCache = nonViralName.getNameCache();
483 specifiedTitleCache = taxonNameBase.getTitleCache();
484
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();
490 }
491 }
492
493 if(this.isInstanceOf(NonViralName.class)) {
494 thisNameCache = HibernateProxyHelper.deproxy(this, NonViralName.class).getNameCache();
495 thisTitleCache = getTitleCache();
496
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()){
502 isProtected = true;
503 }
504 ((BotanicalName)this).setProtectedNameCache(false);
505 ((BotanicalName)this).setNameCache(null, false);
506 thisNameCache = ((BotanicalName) this).getNameCache();
507 ((BotanicalName)this).setNameCache(oldNameCache, isProtected);
508 }
509 }
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();
517 }
518
519 // Compare name cache of taxon names
520
521
522
523 if (!specifiedNameCache.equals("") && !thisNameCache.equals("")) {
524 result = thisNameCache.compareTo(specifiedNameCache);
525 }
526
527 // Compare title cache of taxon names
528
529 if ((result == 0) && (!specifiedTitleCache.equals("") || !thisTitleCache.equals(""))) {
530 result = thisTitleCache.compareTo(specifiedTitleCache);
531 }
532
533 // Compare title cache of taxon references
534
535 if ((result == 0) && (!specifiedReferenceTitleCache.equals("") || !thisReferenceTitleCache.equals(""))) {
536 result = thisReferenceTitleCache.compareTo(specifiedReferenceTitleCache);
537 }
538
539 return result;
540 }
541
542 /**
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).
546 *
547 * @return the cache strategy used for <i>this</i> identifiable entity
548 * @see eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy
549 */
550 public S getCacheStrategy() {
551 return this.cacheStrategy;
552 }
553 /**
554 * @see #getCacheStrategy()
555 */
556
557 public void setCacheStrategy(S cacheStrategy) {
558 this.cacheStrategy = cacheStrategy;
559 }
560
561 @Override
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();
566 }else{
567 return getCacheStrategy().getTitleCache(this);
568 }
569 }
570
571 //****************** CLONE ************************************************/
572
573 /* (non-Javadoc)
574 * @see eu.etaxonomy.cdm.model.common.AnnotatableEntity#clone()
575 */
576 @Override
577 public Object clone() throws CloneNotSupportedException{
578 IdentifiableEntity result = (IdentifiableEntity)super.clone();
579
580 //Extensions
581 result.extensions = new HashSet<Extension>();
582 for (Extension extension : getExtensions() ){
583 Extension newExtension = (Extension)extension.clone();
584 result.addExtension(newExtension);
585 }
586
587 //OriginalSources
588 result.sources = new HashSet<IdentifiableSource>();
589 for (IdentifiableSource source : getSources()){
590 IdentifiableSource newSource = (IdentifiableSource)source.clone();
591 result.addSource(newSource);
592 }
593
594 //Rights
595 result.rights = new HashSet<Rights>();
596 for(Rights rights : getRights()) {
597 result.addRights(rights);
598 }
599
600
601 //Credits
602 result.credits = new ArrayList<Credit>();
603 for(Credit credit : getCredits()) {
604 result.addCredit(credit);
605 }
606
607 //no changes to: lsid, titleCache, protectedTitleCache
608
609 //empty titleCache
610 if (! protectedTitleCache){
611 result.titleCache = null;
612 }
613
614 result.initListener();
615 return result;
616 }
617
618
619 }