merge-update from trunk
[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(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 = "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)
155 // @NotNull
156 // private Set<Identifier> identifiers = new HashSet<Identifier>();
157
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)
163 @NotNull
164 private Set<IdentifiableSource> sources = new HashSet<IdentifiableSource>();
165
166 @XmlTransient
167 @Transient
168 protected S cacheStrategy;
169
170 protected IdentifiableEntity(){
171 initListener();
172 }
173
174 protected void initListener(){
175 PropertyChangeListener listener = new PropertyChangeListener() {
176 @Override
177 public void propertyChange(PropertyChangeEvent ev) {
178 if (!ev.getPropertyName().equals("titleCache") && !ev.getPropertyName().equals("cacheStrategy") && ! isProtectedTitleCache()){
179 titleCache = null;
180 }
181 }
182 };
183 addPropertyChangeListener(listener);
184 }
185
186 /**
187 * By default, we expect most cdm objects to be abstract things
188 * i.e. unable to return a data representation.
189 *
190 * Specific subclasses (e.g. Sequence) can override if necessary.
191 */
192 @Override
193 public byte[] getData() {
194 return null;
195 }
196
197 //******************************** CACHE *****************************************************/
198
199 // @Transient - must not be transient, since this property needs to to be included in all serializations produced by the remote layer
200 @Override
201 public String getTitleCache(){
202 if (protectedTitleCache){
203 return this.titleCache;
204 }
205 // is title dirty, i.e. equal NULL?
206 if (titleCache == null){
207 this.titleCache = generateTitle();
208 this.titleCache = getTruncatedCache(this.titleCache) ;
209 }
210 return titleCache;
211 }
212
213 /**
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
216 */
217 protected boolean regenerateTitleCache() {
218 if (!protectedTitleCache) {
219 this.titleCache = null;
220 getTitleCache();
221 }
222 return protectedTitleCache;
223 }
224
225 @Deprecated
226 @Override
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);
230 }
231
232 @Override
233 public void setTitleCache(String titleCache, boolean protectCache){
234 titleCache = getTruncatedCache(titleCache);
235 this.titleCache = titleCache;
236 this.protectedTitleCache = protectCache;
237 }
238
239
240 /**
241 * @param cache
242 * @return
243 */
244 @Transient
245 protected String getTruncatedCache(String cache) {
246 int maxLength = 800;
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
250 }
251 return cache;
252 }
253
254 //**************************************************************************************
255
256 @Override
257 public LSID getLsid(){
258 return this.lsid;
259 }
260 @Override
261 public void setLsid(LSID lsid){
262 this.lsid = lsid;
263 }
264 @Override
265 public Set<Rights> getRights() {
266 if(rights == null) {
267 this.rights = new HashSet<Rights>();
268 }
269 return this.rights;
270 }
271
272 @Override
273 public void addRights(Rights right){
274 getRights().add(right);
275 }
276 @Override
277 public void removeRights(Rights right){
278 getRights().remove(right);
279 }
280
281
282 @Override
283 public List<Credit> getCredits() {
284 if(credits == null) {
285 this.credits = new ArrayList<Credit>();
286 }
287 return this.credits;
288 }
289
290 @Override
291 public Credit getCredits(Integer index){
292 return getCredits().get(index);
293 }
294
295 @Override
296 public void addCredit(Credit credit){
297 getCredits().add(credit);
298 }
299
300
301 @Override
302 public void addCredit(Credit credit, int index){
303 getCredits().add(index, credit);
304 }
305
306 @Override
307 public void removeCredit(Credit credit){
308 getCredits().remove(credit);
309 }
310
311 @Override
312 public void removeCredit(int index){
313 getCredits().remove(index);
314 }
315
316 // @Override
317 // public Set<Identifier> getIdentifiers(){
318 // if(this.identifiers == null) {
319 // this.identifiers = new HashSet<Identifier>();
320 // }
321 // return this.identifiers;
322 // }
323 // /**
324 // * @param type
325 // * @return a Set of extension value strings
326 // */
327 // public Set<String> getIdentifiers(DefinedTerm type){
328 // return getIdentifiers(type.getUuid());
329 // }
330 // /**
331 // * @param extensionTypeUuid
332 // * @return a Set of extension value strings
333 // */
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());
339 // }
340 // }
341 // return result;
342 // }
343 //
344 // public Identifier addIdentifier(String identifier, DefinedTerm identifierType){
345 // Identifier result = Identifier.NewInstance(this, identifier, identifierType);
346 // return result;
347 // }
348 //
349 // @Override
350 // public void addIdentifier(Identifier identifier){
351 // if (identifier != null){
352 // identifier.setIdentifiedObj(this);
353 // getIdentifiers().add(identifier);
354 // }
355 // }
356 // @Override
357 // public void removeIdentifier(Identifier identifier){
358 // if (identifier != null){
359 // logger.warn("TODO");
360 //// identifier.setExtendedObj(null);
361 // getIdentifiers().remove(identifier);
362 // }
363 // }
364
365 @Override
366 public Set<Extension> getExtensions(){
367 if(extensions == null) {
368 this.extensions = new HashSet<Extension>();
369 }
370 return this.extensions;
371 }
372 /**
373 * @param type
374 * @return a Set of extension value strings
375 */
376 public Set<String> getExtensions(ExtensionType type){
377 return getExtensions(type.getUuid());
378 }
379 /**
380 * @param extensionTypeUuid
381 * @return a Set of extension value strings
382 */
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());
388 }
389 }
390 return result;
391 }
392
393 public void addExtension(String value, ExtensionType extensionType){
394 Extension.NewInstance(this, value, extensionType);
395 }
396
397 @Override
398 public void addExtension(Extension extension){
399 if (extension != null){
400 extension.setExtendedObj(this);
401 getExtensions().add(extension);
402 }
403 }
404 @Override
405 public void removeExtension(Extension extension){
406 if (extension != null){
407 extension.setExtendedObj(null);
408 getExtensions().remove(extension);
409 }
410 }
411
412 @Override
413 public boolean isProtectedTitleCache() {
414 return protectedTitleCache;
415 }
416
417 @Override
418 public void setProtectedTitleCache(boolean protectedTitleCache) {
419 this.protectedTitleCache = protectedTitleCache;
420 }
421
422 @Override
423 public Set<IdentifiableSource> getSources() {
424 if(sources == null) {
425 this.sources = new HashSet<IdentifiableSource>();
426 }
427 return this.sources;
428 }
429
430 @Override
431 public void addSource(IdentifiableSource source) {
432 if (source != null){
433 IdentifiableEntity<?> oldSourcedObj = source.getSourcedObj();
434 if (oldSourcedObj != null && oldSourcedObj != this){
435 oldSourcedObj.getSources().remove(source);
436 }
437 getSources().add(source);
438 source.setSourcedObj(this);
439 }
440 }
441
442 @Override
443 public IdentifiableSource addSource(OriginalSourceType type, String id, String idNamespace, Reference citation, String microCitation) {
444 if (id == null && idNamespace == null && citation == null && microCitation == null){
445 return null;
446 }
447 IdentifiableSource source = IdentifiableSource.NewInstance(type, id, idNamespace, citation, microCitation);
448 addSource(source);
449 return source;
450 }
451
452
453 @Override
454 public IdentifiableSource addImportSource(String id, String idNamespace, Reference<?> citation, String microCitation) {
455 if (id == null && idNamespace == null && citation == null && microCitation == null){
456 return null;
457 }
458 IdentifiableSource source = IdentifiableSource.NewInstance(OriginalSourceType.Import, id, idNamespace, citation, microCitation);
459 addSource(source);
460 return source;
461 }
462
463
464 @Override
465 public void removeSource(IdentifiableSource source) {
466 getSources().remove(source);
467 }
468
469 //******************************** TO STRING *****************************************************/
470
471 /* (non-Javadoc)
472 * @see eu.etaxonomy.cdm.model.common.IIdentifiableEntity#toString()
473 */
474 @Override
475 public String toString() {
476 String result;
477 if (titleCache == null){
478 result = super.toString();
479 }else{
480 result = this.titleCache;
481 }
482 return result;
483 }
484
485
486 public int compareTo(IdentifiableEntity identifiableEntity) {
487
488 int result = 0;
489
490 if (identifiableEntity == null) {
491 throw new NullPointerException("Cannot compare to null.");
492 }
493
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?
497
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;
507
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()){
518 isProtected = true;
519 }
520 ((BotanicalName)identifiableEntity).setProtectedNameCache(false);
521 ((BotanicalName)identifiableEntity).setNameCache(null, false);
522 specifiedNameCache = ((BotanicalName) identifiableEntity).getNameCache();
523 ((BotanicalName)identifiableEntity).setNameCache(oldNameCache, isProtected);
524
525 }
526 }
527
528 } else if(identifiableEntity instanceof TaxonBase) {
529 TaxonBase taxonBase = HibernateProxyHelper.deproxy(identifiableEntity, TaxonBase.class);
530
531 TaxonNameBase<?,?> taxonNameBase = taxonBase.getName();
532
533
534 NonViralName nonViralName = HibernateProxyHelper.deproxy(taxonNameBase, NonViralName.class);
535 specifiedNameCache = nonViralName.getNameCache();
536 specifiedTitleCache = taxonNameBase.getTitleCache();
537
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();
543 }
544 }
545
546 if(this.isInstanceOf(NonViralName.class)) {
547 thisNameCache = HibernateProxyHelper.deproxy(this, NonViralName.class).getNameCache();
548 thisTitleCache = getTitleCache();
549
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()){
555 isProtected = true;
556 }
557 ((BotanicalName)this).setProtectedNameCache(false);
558 ((BotanicalName)this).setNameCache(null, false);
559 thisNameCache = ((BotanicalName) this).getNameCache();
560 ((BotanicalName)this).setNameCache(oldNameCache, isProtected);
561 }
562 }
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();
570 }
571
572 // Compare name cache of taxon names
573
574
575
576 if (!specifiedNameCache.equals("") && !thisNameCache.equals("")) {
577 result = thisNameCache.compareTo(specifiedNameCache);
578 }
579
580 // Compare title cache of taxon names
581
582 if ((result == 0) && (!specifiedTitleCache.equals("") || !thisTitleCache.equals(""))) {
583 result = thisTitleCache.compareTo(specifiedTitleCache);
584 }
585
586 // Compare title cache of taxon references
587
588 if ((result == 0) && (!specifiedReferenceTitleCache.equals("") || !thisReferenceTitleCache.equals(""))) {
589 result = thisReferenceTitleCache.compareTo(specifiedReferenceTitleCache);
590 }
591
592 return result;
593 }
594
595 /**
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).
599 *
600 * @return the cache strategy used for <i>this</i> identifiable entity
601 * @see eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy
602 */
603 public S getCacheStrategy() {
604 return this.cacheStrategy;
605 }
606 /**
607 * @see #getCacheStrategy()
608 */
609
610 public void setCacheStrategy(S cacheStrategy) {
611 this.cacheStrategy = cacheStrategy;
612 }
613
614 @Override
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();
619 }else{
620 return getCacheStrategy().getTitleCache(this);
621 }
622 }
623
624 //****************** CLONE ************************************************/
625
626 /* (non-Javadoc)
627 * @see eu.etaxonomy.cdm.model.common.AnnotatableEntity#clone()
628 */
629 @Override
630 public Object clone() throws CloneNotSupportedException{
631 IdentifiableEntity result = (IdentifiableEntity)super.clone();
632
633 //Extensions
634 result.extensions = new HashSet<Extension>();
635 for (Extension extension : getExtensions() ){
636 Extension newExtension = (Extension)extension.clone();
637 result.addExtension(newExtension);
638 }
639
640 //OriginalSources
641 result.sources = new HashSet<IdentifiableSource>();
642 for (IdentifiableSource source : getSources()){
643 IdentifiableSource newSource = (IdentifiableSource)source.clone();
644 result.addSource(newSource);
645 }
646
647 //Rights
648 result.rights = new HashSet<Rights>();
649 for(Rights rights : getRights()) {
650 result.addRights(rights);
651 }
652
653
654 //Credits
655 result.credits = new ArrayList<Credit>();
656 for(Credit credit : getCredits()) {
657 result.addCredit(credit);
658 }
659
660 //no changes to: lsid, titleCache, protectedTitleCache
661
662 //empty titleCache
663 if (! protectedTitleCache){
664 result.titleCache = null;
665 }
666
667 result.initListener();
668 return result;
669 }
670
671
672 }