Merge branch 'release/5.6.0'
[cdmlib.git] / cdmlib-model / src / main / java / eu / etaxonomy / cdm / model / occurrence / SpecimenOrObservationBase.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.occurrence;
11
12 import java.net.URI;
13 import java.util.ArrayList;
14 import java.util.Collection;
15 import java.util.HashMap;
16 import java.util.HashSet;
17 import java.util.Map;
18 import java.util.Set;
19
20 import javax.persistence.Column;
21 import javax.persistence.Entity;
22 import javax.persistence.FetchType;
23 import javax.persistence.Index;
24 import javax.persistence.Inheritance;
25 import javax.persistence.InheritanceType;
26 import javax.persistence.ManyToMany;
27 import javax.persistence.ManyToOne;
28 import javax.persistence.MapKeyJoinColumn;
29 import javax.persistence.OneToMany;
30 import javax.persistence.Table;
31 import javax.persistence.Transient;
32 import javax.validation.constraints.NotNull;
33 import javax.xml.bind.annotation.XmlAccessType;
34 import javax.xml.bind.annotation.XmlAccessorType;
35 import javax.xml.bind.annotation.XmlAttribute;
36 import javax.xml.bind.annotation.XmlElement;
37 import javax.xml.bind.annotation.XmlElementWrapper;
38 import javax.xml.bind.annotation.XmlIDREF;
39 import javax.xml.bind.annotation.XmlRootElement;
40 import javax.xml.bind.annotation.XmlSchemaType;
41 import javax.xml.bind.annotation.XmlType;
42 import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
43
44 import org.apache.log4j.Logger;
45 import org.hibernate.annotations.Cascade;
46 import org.hibernate.annotations.CascadeType;
47 import org.hibernate.annotations.Type;
48 import org.hibernate.envers.Audited;
49 import org.hibernate.search.annotations.Analyze;
50 import org.hibernate.search.annotations.Field;
51 import org.hibernate.search.annotations.FieldBridge;
52 import org.hibernate.search.annotations.Fields;
53 import org.hibernate.search.annotations.IndexedEmbedded;
54 import org.hibernate.search.annotations.SortableField;
55 import org.hibernate.search.annotations.Store;
56 import org.hibernate.search.bridge.builtin.BooleanBridge;
57
58 import eu.etaxonomy.cdm.hibernate.HibernateProxyHelper;
59 import eu.etaxonomy.cdm.hibernate.search.StripHtmlBridge;
60 import eu.etaxonomy.cdm.jaxb.FormattedTextAdapter;
61 import eu.etaxonomy.cdm.jaxb.MultilanguageTextAdapter;
62 import eu.etaxonomy.cdm.model.common.IIntextReferenceTarget;
63 import eu.etaxonomy.cdm.model.common.IMultiLanguageTextHolder;
64 import eu.etaxonomy.cdm.model.common.IPublishable;
65 import eu.etaxonomy.cdm.model.common.IdentifiableEntity;
66 import eu.etaxonomy.cdm.model.common.Language;
67 import eu.etaxonomy.cdm.model.common.LanguageString;
68 import eu.etaxonomy.cdm.model.common.MultilanguageText;
69 import eu.etaxonomy.cdm.model.description.DescriptionBase;
70 import eu.etaxonomy.cdm.model.description.DescriptionElementBase;
71 import eu.etaxonomy.cdm.model.description.IDescribable;
72 import eu.etaxonomy.cdm.model.description.SpecimenDescription;
73 import eu.etaxonomy.cdm.model.description.TaxonDescription;
74 import eu.etaxonomy.cdm.model.description.TaxonNameDescription;
75 import eu.etaxonomy.cdm.model.term.DefinedTerm;
76 import eu.etaxonomy.cdm.model.term.TermType;
77 import eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy;
78 import eu.etaxonomy.cdm.strategy.match.Match;
79 import eu.etaxonomy.cdm.strategy.match.Match.ReplaceMode;
80 import eu.etaxonomy.cdm.strategy.match.MatchMode;
81
82 /**
83 * type figures are observations with at least a figure object in media
84 * @author m.doering
85 * @since 08-Nov-2007 13:06:41
86 */
87 @XmlAccessorType(XmlAccessType.FIELD)
88 @XmlType(name = "SpecimenOrObservationBase", propOrder = {
89 "recordBasis",
90 "identityCache",
91 "protectedIdentityCache",
92 "publish",
93 "preferredStableUri",
94 "sex",
95 "lifeStage",
96 "kindOfUnit",
97 "individualCount",
98 "definition",
99 "descriptions",
100 "determinations",
101 "derivationEvents"
102 })
103 @XmlRootElement(name = "SpecimenOrObservationBase")
104 @Entity
105 @Audited
106 @Inheritance(strategy=InheritanceType.SINGLE_TABLE)
107 @Table(name="SpecimenOrObservationBase", indexes = { @Index(name = "specimenOrObservationBaseTitleCacheIndex", columnList = "titleCache"),
108 @Index(name = "specimenOrObservationBaseIdentityCacheIndex", columnList = "identityCache") })
109 public abstract class SpecimenOrObservationBase<S extends IIdentifiableEntityCacheStrategy<?>>
110 extends IdentifiableEntity<S>
111 implements IMultiLanguageTextHolder, IIntextReferenceTarget, IDescribable<DescriptionBase<S>>, IPublishable {
112
113 private static final long serialVersionUID = 6932680139334408031L;
114 private static final Logger logger = Logger.getLogger(SpecimenOrObservationBase.class);
115
116 /**
117 * An indication of what the unit record describes.
118 *
119 * NOTE: The name of the attribute was chosen against the common naming conventions of the CDM
120 * as it is well known in common standards like ABCD and DarwinCore. According to CDM naming
121 * conventions it would be specimenOrObservationType.
122 *
123 * @see ABCD: DataSets/DataSet/Units/Unit/RecordBasis
124 * @see Darwin Core: http://wiki.tdwg.org/twiki/bin/view/DarwinCore/BasisOfRecord
125 */
126 @XmlAttribute(name ="RecordBasis")
127 @Column(name="recordBasis")
128 @NotNull
129 @Type(type = "eu.etaxonomy.cdm.hibernate.EnumUserType",
130 parameters = {@org.hibernate.annotations.Parameter(name = "enumClass", value = "eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationType")}
131 )
132 @Audited
133 private SpecimenOrObservationType recordBasis;
134
135
136 @XmlElementWrapper(name = "Descriptions")
137 @XmlElement(name = "Description")
138 @OneToMany(mappedBy="describedSpecimenOrObservation", fetch = FetchType.LAZY)
139 @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE})
140 @NotNull
141 private Set<DescriptionBase<S>> descriptions = new HashSet<>();
142
143
144 @XmlElementWrapper(name = "Determinations")
145 @XmlElement(name = "Determination")
146 @OneToMany(mappedBy="identifiedUnit", orphanRemoval=true)
147 @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
148 @IndexedEmbedded(depth = 2)
149 @NotNull
150 private Set<DeterminationEvent> determinations = new HashSet<>();
151
152 @XmlElement(name = "Sex")
153 @XmlIDREF
154 @XmlSchemaType(name = "IDREF")
155 @ManyToOne(fetch = FetchType.LAZY)
156 private DefinedTerm sex;
157
158 @XmlElement(name = "LifeStage")
159 @XmlIDREF
160 @XmlSchemaType(name = "IDREF")
161 @ManyToOne(fetch = FetchType.LAZY)
162 private DefinedTerm lifeStage;
163
164 /**
165 * Part(s) of organism or class of materials represented by this unit.
166 * Example: fruits, seeds, tissue, gDNA, leaves
167 *
168 * @see ABCD: DataSets/DataSet/Units/Unit/KindOfUnit
169 * @see TermType#KindOfUnit
170 */
171 @XmlElement(name = "KindOfUnit")
172 @XmlIDREF
173 @XmlSchemaType(name = "IDREF")
174 @ManyToOne(fetch = FetchType.LAZY)
175 // @IndexedEmbedded(depth=1)
176 private DefinedTerm kindOfUnit;
177
178 @XmlElement(name = "IndividualCount")
179 @Field(analyze = Analyze.NO)
180 private String individualCount;
181
182 /**
183 * The preferred stable identifier (URI) as discussed in
184 * {@link http://dev.e-taxonomy.eu/trac/ticket/5606}
185 */
186 @XmlElement(name = "PreferredStableUri")
187 @Field(analyze = Analyze.NO)
188 @Type(type="uriUserType")
189 private URI preferredStableUri;
190
191 // the verbatim description of this occurrence. Free text usable when no atomised data is available.
192 // in conjunction with titleCache which serves as the "citation" string for this object
193 @XmlElement(name = "Description")
194 @XmlJavaTypeAdapter(MultilanguageTextAdapter.class)
195 @OneToMany(fetch = FetchType.LAZY, orphanRemoval=true)
196 @MapKeyJoinColumn(name="definition_mapkey_id")
197 @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE, CascadeType.DELETE})
198 @IndexedEmbedded
199 @NotNull
200 protected Map<Language,LanguageString> definition = new HashMap<>();
201
202 // events that created derivedUnits from this unit
203 @XmlElementWrapper(name = "DerivationEvents")
204 @XmlElement(name = "DerivationEvent")
205 @XmlIDREF
206 @XmlSchemaType(name = "IDREF")
207 @ManyToMany(fetch=FetchType.LAZY)
208 @Cascade({CascadeType.SAVE_UPDATE,CascadeType.MERGE, CascadeType.DELETE})
209 @NotNull
210 protected Set<DerivationEvent> derivationEvents = new HashSet<>();
211
212 @XmlAttribute(name = "publish")
213 @Field(analyze = Analyze.NO)
214 @FieldBridge(impl=BooleanBridge.class)
215 private boolean publish = true;
216
217 @XmlElement(name = "IdentityCache", required = false)
218 @XmlJavaTypeAdapter(FormattedTextAdapter.class)
219 @Match(value=MatchMode.CACHE, cacheReplaceMode=ReplaceMode.ALL)
220 // @NotEmpty(groups = Level2.class) // implictly NotNull
221 @Fields({
222 @Field(store=Store.YES),
223 // If the field is only needed for sorting and nothing else, you may configure it as
224 // un-indexed and un-stored, thus avoid unnecessary index growth.
225 @Field(name = "identityCache__sort", analyze = Analyze.NO, store=Store.NO, index = org.hibernate.search.annotations.Index.NO)
226 })
227 @SortableField(forField = "identityCache__sort")
228 @FieldBridge(impl=StripHtmlBridge.class)
229 private String identityCache;
230
231
232 //if true identityCache will not be automatically generated/updated
233 @XmlElement(name = "ProtectedIdentityCache")
234 private boolean protectedIdentityCache;
235
236
237 //********************************** CONSTRUCTOR *********************************/
238
239 //for hibernate use only
240 @Deprecated
241 protected SpecimenOrObservationBase(){
242 super();
243 }
244
245 protected SpecimenOrObservationBase(SpecimenOrObservationType recordBasis) {
246 super();
247 if (recordBasis == null){ throw new IllegalArgumentException("RecordBasis must not be null");}
248 this.recordBasis = recordBasis;
249 }
250
251
252 /**
253 * Subclasses should implement setting the default cache strate
254 */
255 protected abstract void initDefaultCacheStrategy();
256
257
258 //************************* GETTER / SETTER ***********************/
259
260
261 /**@see #recordBasis */
262 public SpecimenOrObservationType getRecordBasis() {
263 return recordBasis;
264 }
265 /**@see #recordBasis */
266 public void setRecordBasis(SpecimenOrObservationType recordBasis) {
267 this.recordBasis = recordBasis;
268 }
269
270
271 /**
272 * @return the identityCache
273 */
274 public String getIdentityCache() {
275 return identityCache;
276 }
277 /**
278 * @Deprecated For special use only.
279 * Use {@link #setIdentityCache(String, boolean)} instead
280 */
281 @Deprecated
282 public void setIdentityCache(String identityCache) {
283 this.identityCache = identityCache;
284 }
285
286 public void setIdentityCache(String identityCache, boolean isProtected) {
287 this.protectedIdentityCache = isProtected;
288 setIdentityCache(identityCache);
289 }
290
291 /**
292 * @return the protectedIdentityCache
293 */
294 public boolean isProtectedIdentityCache() {
295 return protectedIdentityCache;
296 }
297 /**
298 * @param protectedIdentityCache the protectedIdentityCache to set
299 */
300 public void setProtectedIdentityCache(boolean protectedIdentityCache) {
301 this.protectedIdentityCache = protectedIdentityCache;
302 }
303
304 /**@see #preferredStableUri */
305 public URI getPreferredStableUri() {
306 return preferredStableUri;
307 }
308 /**@see #preferredStableUri */
309 public void setPreferredStableUri(URI preferredStableUri) {
310 this.preferredStableUri = preferredStableUri;
311 }
312
313
314 /**
315 * Returns the boolean value indicating if this specimen or observation should be withheld
316 * (<code>publish=false</code>) or not (<code>publish=true</code>) during any publication
317 * process to the general public.
318 * This publish flag implementation is preliminary and may be replaced by a more general
319 * implementation of READ rights in future.<BR>
320 * The default value is <code>true</code>.
321 */
322 @Override
323 public boolean isPublish() {
324 return publish;
325 }
326
327 /**
328 * @see #isPublish()
329 * @param publish
330 */
331 @Override
332 public void setPublish(boolean publish) {
333 this.publish = publish;
334 }
335
336 /**
337 * The descriptions this specimen or observation is part of.<BR>
338 * A specimen can not only have it's own {@link SpecimenDescription specimen description }
339 * but can also be part of a {@link TaxonDescription taxon description} or a
340 * {@link TaxonNameDescription taxon name description}.<BR>
341 * @see #getSpecimenDescriptions()
342 * @return
343 */
344 @Override
345 public Set<DescriptionBase<S>> getDescriptions() {
346 if(descriptions == null) {
347 this.descriptions = new HashSet<>();
348 }
349 return this.descriptions;
350 }
351
352 /**
353 * Returns the {@link SpecimenDescription specimen descriptions} this specimen is part of.
354 * @see #getDescriptions()
355 * @return
356 */
357 @Transient
358 public Set<SpecimenDescription> getSpecimenDescriptions() {
359 return getSpecimenDescriptions(true);
360 }
361
362 /**
363 * Returns the {@link SpecimenDescription specimen descriptions} this specimen is part of.
364 * @see #getDescriptions()
365 * @return
366 */
367 @Transient
368 public Set<SpecimenDescription> getSpecimenDescriptions(boolean includeImageGallery) {
369 Set<SpecimenDescription> specimenDescriptions = new HashSet<>();
370 for (DescriptionBase<?> descriptionBase : getDescriptions()){
371 if (descriptionBase.isInstanceOf(SpecimenDescription.class)){
372 if (includeImageGallery || descriptionBase.isImageGallery() == false){
373 specimenDescriptions.add(descriptionBase.deproxy(descriptionBase, SpecimenDescription.class));
374 }
375
376 }
377 }
378 return specimenDescriptions;
379 }
380 /**
381 * Returns the {@link SpecimenDescription specimen descriptions} which act as an image gallery
382 * and which this specimen is part of.
383 * @see #getDescriptions()
384 * @return
385 */
386 @Transient
387 public Set<SpecimenDescription> getSpecimenDescriptionImageGallery() {
388 Set<SpecimenDescription> specimenDescriptions = new HashSet<>();
389 for (DescriptionBase<?> descriptionBase : getDescriptions()){
390 if (descriptionBase.isInstanceOf(SpecimenDescription.class)){
391 if (descriptionBase.isImageGallery() == true){
392 specimenDescriptions.add(descriptionBase.deproxy(descriptionBase, SpecimenDescription.class));
393 }
394 }
395 }
396 return specimenDescriptions;
397 }
398
399 /**
400 * Adds a new description to this specimen or observation
401 * @param description
402 */
403 @Override
404 public void addDescription(DescriptionBase description) {
405 if (description.getDescribedSpecimenOrObservation() != null){
406 description.getDescribedSpecimenOrObservation().removeDescription(description);
407 }
408 descriptions.add(description);
409 description.setDescribedSpecimenOrObservation(this);
410 }
411
412 /**
413 * Removes a specimen from a description (removes a description from this specimen)
414 * @param description
415 */
416 @Override
417 public void removeDescription(DescriptionBase description) {
418 boolean existed = descriptions.remove(description);
419 if (existed){
420 description.setDescribedSpecimenOrObservation(null);
421 }
422 }
423
424
425 public Set<DerivationEvent> getDerivationEvents() {
426 if(derivationEvents == null) {
427 this.derivationEvents = new HashSet<>();
428 }
429 return this.derivationEvents;
430 }
431
432 public void addDerivationEvent(DerivationEvent derivationEvent) {
433 if (! this.derivationEvents.contains(derivationEvent)){
434 this.derivationEvents.add(derivationEvent);
435 derivationEvent.addOriginal(this);
436 }
437 }
438
439 public void removeDerivationEvent(DerivationEvent derivationEvent) {
440 if (this.derivationEvents.contains(derivationEvent)){
441 this.derivationEvents.remove(derivationEvent);
442 derivationEvent.removeOriginal(this);
443 }
444 }
445
446 public Set<DeterminationEvent> getDeterminations() {
447 if(determinations == null) {
448 this.determinations = new HashSet<>();
449 }
450 return this.determinations;
451 }
452
453 public void addDetermination(DeterminationEvent determination) {
454 // FIXME bidirectional integrity. Use protected Determination setter
455 this.determinations.add(determination);
456 }
457
458 public void removeDetermination(DeterminationEvent determination) {
459 // FIXME bidirectional integrity. Use protected Determination setter
460 this.determinations.remove(determination);
461 }
462
463 public DefinedTerm getSex() {
464 return sex;
465 }
466
467 public void setSex(DefinedTerm sex) {
468 this.sex = sex;
469 }
470
471 public DefinedTerm getLifeStage() {
472 return lifeStage;
473 }
474
475 public void setLifeStage(DefinedTerm lifeStage) {
476 this.lifeStage = lifeStage;
477 }
478
479
480 /**
481 * @see #kindOfUnit
482 * @return
483 */
484 public DefinedTerm getKindOfUnit() {
485 return kindOfUnit;
486 }
487
488 /**
489 * @see #kindOfUnit
490 * @param kindOfUnit
491 */
492 public void setKindOfUnit(DefinedTerm kindOfUnit) {
493 this.kindOfUnit = kindOfUnit;
494 }
495
496 public String getIndividualCount() {
497 return individualCount;
498 }
499
500 public void setIndividualCount(String individualCount) {
501 this.individualCount = individualCount;
502 }
503
504 public Map<Language,LanguageString> getDefinition(){
505 return this.definition;
506 }
507
508 /**
509 * adds the {@link LanguageString description} to the {@link MultilanguageText multilanguage text}
510 * used to define <i>this</i> specimen or observation.
511 *
512 * @param description the languageString in with the title string and the given language
513 *
514 * @see #getDefinition()
515 * @see #putDefinition(Language, String)
516 */
517 public void putDefinition(LanguageString description){
518 this.definition.put(description.getLanguage(),description);
519 }
520
521 /**
522 * Creates a {@link LanguageString language string} based on the given text string
523 * and the given {@link Language language} and adds it to the {@link MultilanguageText multilanguage text}
524 * used to define <i>this</i> specimen or observation.
525 *
526 * @param language the language in which the title string is formulated
527 * @param text the definition in a particular language
528 *
529 * @see #getDefinition()
530 * @see #putDefinition(LanguageString)
531 */
532 public void putDefinition(Language language, String text){
533 this.definition.put(language, LanguageString.NewInstance(text, language));
534 }
535
536
537 public void removeDefinition(Language lang){
538 this.definition.remove(lang);
539 }
540
541 /**
542 * for derived units get the single next higher parental/original unit.
543 * If multiple original units exist throw error
544 * @return
545 */
546 @Transient
547 public SpecimenOrObservationBase getOriginalUnit(){
548 logger.warn("GetOriginalUnit not yet implemented");
549 return null;
550 }
551
552
553 public boolean hasCharacterData() {
554 Set<DescriptionBase<S>> descriptions = this.getDescriptions();
555 for (DescriptionBase<?> descriptionBase : descriptions) {
556 if (descriptionBase.isInstanceOf(SpecimenDescription.class)) {
557 SpecimenDescription specimenDescription = HibernateProxyHelper.deproxy(descriptionBase, SpecimenDescription.class);
558 Set<DescriptionElementBase> elements = specimenDescription.getElements();
559 for (DescriptionElementBase descriptionElementBase : elements) {
560 if (descriptionElementBase.isCharacterData()){
561 return true;
562 }
563 }
564 }
565 }
566 return false;
567 }
568
569 /**
570 * Returns a list of all description items which
571 * @return
572 */
573 @Transient
574 public Collection<DescriptionElementBase> characterData() {
575 Collection<DescriptionElementBase> states = new ArrayList<>();
576 Set<DescriptionBase<S>> descriptions = this.getDescriptions();
577 for (DescriptionBase<?> descriptionBase : descriptions) {
578 if (descriptionBase.isInstanceOf(SpecimenDescription.class)) {
579 SpecimenDescription specimenDescription = HibernateProxyHelper.deproxy(descriptionBase, SpecimenDescription.class);
580 Set<DescriptionElementBase> elements = specimenDescription.getElements();
581 for (DescriptionElementBase descriptionElementBase : elements) {
582 if(descriptionElementBase.isCharacterData()){
583 states.add(descriptionElementBase);
584 }
585 }
586 }
587 }
588 return states;
589 }
590
591 @Override
592 public boolean updateCaches(){
593 boolean result = super.updateCaches();
594 // if (this.protectedIdentityCache == false){
595 // String oldIdentityCache = this.identityCache;
596 //
597 // String newIdentityCache = cacheStrategy.getIdentityCache(this);
598 //
599 // if ( oldIdentityCache == null || ! oldIdentityCache.equals(newIdentityCache) ){
600 // this.setIdentityCache(null, false);
601 // String newCache = this.getIdentityCache();
602 //
603 // if (newCache == null){
604 // logger.warn("New identityCache should never be null");
605 // }
606 // if (oldIdentityCache == null){
607 // logger.info("Old abbrevTitleCache should never be null");
608 // }
609 // result = true;
610 // }
611 // }
612 return result;
613 }
614
615 //******************** CLONE **********************************************/
616
617 @Override
618 public Object clone() throws CloneNotSupportedException {
619 SpecimenOrObservationBase<S> result = (SpecimenOrObservationBase<S>)super.clone();
620
621 //defininion (description, languageString)
622 result.definition = cloneLanguageString(this.definition);
623
624 //sex
625 result.setSex(this.sex);
626 //life stage
627 result.setLifeStage(this.lifeStage);
628
629 result.descriptions = new HashSet<>();
630 //Descriptions
631 for(DescriptionBase<S> description : this.descriptions) {
632 result.addDescription((SpecimenDescription)description.clone());
633 }
634
635 result.determinations = new HashSet<>();
636 for(DeterminationEvent determination : this.determinations) {
637 result.addDetermination(determination.clone());
638 }
639
640 result.derivationEvents = new HashSet<>();
641 //DerivationEvent
642 for(DerivationEvent derivationEvent : this.derivationEvents) {
643 //TODO should we clone this?
644 result.addDerivationEvent(derivationEvent);
645 }
646
647 //no changes to: individualCount
648 return result;
649 }
650 }