Project

General

Profile

Download (21.2 KB) Statistics
| Branch: | Tag: | Revision:
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.ManyToMany;
25
import javax.persistence.MappedSuperclass;
26
import javax.persistence.OneToMany;
27
import javax.persistence.OrderColumn;
28
import javax.persistence.Transient;
29
import javax.validation.constraints.NotNull;
30
import javax.xml.bind.annotation.XmlAccessType;
31
import javax.xml.bind.annotation.XmlAccessorType;
32
import javax.xml.bind.annotation.XmlElement;
33
import javax.xml.bind.annotation.XmlElementWrapper;
34
import javax.xml.bind.annotation.XmlTransient;
35
import javax.xml.bind.annotation.XmlType;
36
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
37

    
38
import org.apache.commons.lang.StringUtils;
39
import org.apache.log4j.Logger;
40
import org.hibernate.annotations.Cascade;
41
import org.hibernate.annotations.CascadeType;
42
import org.hibernate.envers.Audited;
43
import org.hibernate.search.annotations.Analyze;
44
import org.hibernate.search.annotations.Field;
45
import org.hibernate.search.annotations.FieldBridge;
46
import org.hibernate.search.annotations.Fields;
47
import org.hibernate.search.annotations.Index;
48
import org.hibernate.search.annotations.SortableField;
49
import org.hibernate.search.annotations.Store;
50
import org.hibernate.validator.constraints.NotEmpty;
51

    
52
import eu.etaxonomy.cdm.common.CdmUtils;
53
import eu.etaxonomy.cdm.hibernate.search.StripHtmlBridge;
54
import eu.etaxonomy.cdm.jaxb.FormattedTextAdapter;
55
import eu.etaxonomy.cdm.jaxb.LSIDAdapter;
56
import eu.etaxonomy.cdm.model.media.Rights;
57
import eu.etaxonomy.cdm.model.reference.Reference;
58
import eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy;
59
import eu.etaxonomy.cdm.strategy.match.Match;
60
import eu.etaxonomy.cdm.strategy.match.Match.ReplaceMode;
61
import eu.etaxonomy.cdm.strategy.match.MatchMode;
62
import eu.etaxonomy.cdm.strategy.merge.Merge;
63
import eu.etaxonomy.cdm.strategy.merge.MergeMode;
64
import eu.etaxonomy.cdm.validation.Level2;
65

    
66
/**
67
 * 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.
68
 * All subclasses inherit the ability to store additional properties that are stored as {@link Extension Extensions}, basically a string value with a type term.
69
 * Any number of right statements can be attached as well as multiple {@link OriginalSourceBase} objects.
70
 * 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).
71
 * A Taxon for example that was taken from 2 sources like FaunaEuropaea and IPNI would have two originalSource objects.
72
 * 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.
73
 *
74
 * @author m.doering
75
 * @since 08-Nov-2007 13:06:27
76
 */
77
@XmlAccessorType(XmlAccessType.FIELD)
78
@XmlType(name = "IdentifiableEntity", propOrder = {
79
    "lsid",
80
    "titleCache",
81
    "protectedTitleCache",
82
    "credits",
83
    "extensions",
84
    "identifiers",
85
    "rights",
86
    "sources"
87
})
88
@Audited
89
@MappedSuperclass
90
public abstract class IdentifiableEntity<S extends IIdentifiableEntityCacheStrategy>
91
        extends AnnotatableEntity
92
        implements IIdentifiableEntity /*, ISourceable<IdentifiableSource> */ {
93

    
94
    private static final long serialVersionUID = 7912083412108359559L;
95
    private static final Logger logger = Logger.getLogger(IdentifiableEntity.class);
96

    
97
    @XmlTransient
98
    public static final boolean PROTECTED = true;
99
    @XmlTransient
100
    public static final boolean NOT_PROTECTED = false;
101

    
102
    @XmlElement(name = "LSID", type = String.class)
103
    @XmlJavaTypeAdapter(LSIDAdapter.class)
104
    @Embedded
105
    private LSID lsid;
106

    
107
    @XmlElement(name = "TitleCache", required = true)
108
    @XmlJavaTypeAdapter(FormattedTextAdapter.class)
109
    @Column(name="titleCache", length=800) //see #1592
110
    @Match(value=MatchMode.CACHE, cacheReplaceMode=ReplaceMode.ALL)
111
    @NotEmpty(groups = Level2.class) // implicitly NotNull
112
    @Fields({
113
        @Field(store=Store.YES),
114
        //  If the field is only needed for sorting and nothing else, you may configure it as
115
        //  un-indexed and un-stored, thus avoid unnecessary index growth.
116
        @Field(name = "titleCache__sort", analyze = Analyze.NO, store=Store.NO, index = Index.NO)
117
    })
118
    @SortableField(forField = "titleCache__sort")
119
    @FieldBridge(impl=StripHtmlBridge.class)
120
    protected String titleCache;
121

    
122
    //if true titleCache will not be automatically generated/updated
123
    @XmlElement(name = "ProtectedTitleCache")
124
    protected boolean protectedTitleCache;
125

    
126
    @XmlElementWrapper(name = "Rights", nillable = true)
127
    @XmlElement(name = "Rights")
128
    @ManyToMany(fetch = FetchType.LAZY /*, orphanRemoval=false*/)  //#5762 M:N now
129
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE})
130
    //TODO
131
    @Merge(MergeMode.ADD_CLONE)
132
    @NotNull
133
    private Set<Rights> rights = new HashSet<>();
134

    
135
    @XmlElementWrapper(name = "Credits", nillable = true)
136
    @XmlElement(name = "Credit")
137
    @OrderColumn(name="sortIndex")
138
    @OneToMany(fetch = FetchType.LAZY, orphanRemoval=true)
139
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
140
    //TODO
141
    @Merge(MergeMode.ADD_CLONE)
142
    @NotNull
143
    private List<Credit> credits = new ArrayList<>();
144

    
145
    @XmlElementWrapper(name = "Extensions", nillable = true)
146
    @XmlElement(name = "Extension")
147
    @OneToMany(fetch = FetchType.LAZY)
148
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
149
    @Merge(MergeMode.ADD_CLONE)
150
    @NotNull
151
    private Set<Extension> extensions = new HashSet<>();
152

    
153
    @XmlElementWrapper(name = "Identifiers", nillable = true)
154
    @XmlElement(name = "Identifier")
155
    @OrderColumn(name="sortIndex")
156
    @OneToMany(fetch = FetchType.LAZY, orphanRemoval=true)
157
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
158
    @Merge(MergeMode.ADD_CLONE)
159
    @NotNull
160
    private List<Identifier> identifiers = new ArrayList<>();
161

    
162
    @XmlElementWrapper(name = "Sources", nillable = true)
163
    @XmlElement(name = "IdentifiableSource")
164
    @OneToMany(fetch = FetchType.LAZY)
165
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
166
    @Merge(MergeMode.ADD_CLONE)
167
    @NotNull
168
    private Set<IdentifiableSource> sources = new HashSet<>();
169

    
170
    @XmlTransient
171
    @Transient
172
    protected S cacheStrategy;
173

    
174
    protected IdentifiableEntity(){
175
        initListener();
176
    }
177

    
178
    @Override
179
    public void initListener(){
180
        PropertyChangeListener listener = new PropertyChangeListener() {
181
            @Override
182
            public void propertyChange(PropertyChangeEvent ev) {
183
                if (! "titleCache".equals(ev.getPropertyName()) && !"cacheStrategy".equals(ev.getPropertyName()) && ! isProtectedTitleCache()){
184
                    titleCache = null;
185
                }
186
            }
187
        };
188
        addPropertyChangeListener(listener);
189
    }
190

    
191
    /**
192
     * By default, we expect most cdm objects to be abstract things
193
     * i.e. unable to return a data representation.
194
     *
195
     * Specific subclasses (e.g. Sequence) can override if necessary.
196
     */
197
    @Override
198
    public byte[] getData() {
199
        return null;
200
    }
201

    
202
//******************************** CACHE *****************************************************/
203

    
204
    // @Transient  - must not be transient, since this property needs to to be included in all serializations produced by the remote layer
205
    @Override
206
    public String getTitleCache(){
207
        if (protectedTitleCache){
208
            return this.titleCache;
209
        }
210
        // is title dirty, i.e. equal NULL?
211
        if (titleCache == null){
212
            this.titleCache = generateTitle();
213
            this.titleCache = getTruncatedCache(this.titleCache) ;
214
        }
215
        //removed due to #5849
216
//        if(StringUtils.isBlank(titleCache)){
217
//            titleCache = this.toString();
218
//        }
219
        return titleCache;
220
    }
221

    
222
    @Deprecated
223
    @Override
224
    public void setTitleCache(String titleCache){
225
    	//TODO shouldn't we call setTitleCache(String, boolean),but is this conformant with Java Bean Specification?
226
    	this.titleCache = getTruncatedCache(titleCache);
227
    }
228

    
229
    @Override
230
    public void setTitleCache(String titleCache, boolean protectCache){
231
        titleCache = getTruncatedCache(titleCache);
232
        this.titleCache = titleCache;
233
        this.protectedTitleCache = protectCache;
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
    @Override
252
    public boolean isProtectedTitleCache() {
253
        return protectedTitleCache;
254
    }
255

    
256
    @Override
257
    public void setProtectedTitleCache(boolean protectedTitleCache) {
258
        this.protectedTitleCache = protectedTitleCache;
259
    }
260

    
261
    /**
262
     *
263
     * @return true, if the current state of the titleCache (without generating it new)
264
     * is <code>null</code> or the empty string. This is primarily meant for internal use.
265
     */
266
    public boolean hasEmptyTitleCache(){
267
        return this.titleCache == null || "".equals(this.titleCache);
268
    }
269

    
270
    /**
271
     * Returns true if any of the caches is not protected. Needs to be overriden
272
     * by subclass if other caches exist.
273
     */
274
    public boolean hasUnprotectedCache(){
275
        return !this.protectedTitleCache;
276
    }
277

    
278
//**************************************************************************************
279

    
280
    @Override
281
    public LSID getLsid(){
282
        return this.lsid;
283
    }
284
    @Override
285
    public void setLsid(LSID lsid){
286
        this.lsid = lsid;
287
    }
288
    @Override
289
    public Set<Rights> getRights() {
290
        if(rights == null) {
291
            this.rights = new HashSet<>();
292
        }
293
        return this.rights;
294
    }
295

    
296
    @Override
297
    public void addRights(Rights right){
298
        getRights().add(right);
299
    }
300
    @Override
301
    public void removeRights(Rights right){
302
        getRights().remove(right);
303
    }
304

    
305

    
306
    @Override
307
    public List<Credit> getCredits() {
308
        if(credits == null) {
309
            this.credits = new ArrayList<>();
310
        }
311
        return this.credits;
312
    }
313

    
314
    @Override
315
    public Credit getCredits(Integer index){
316
        return getCredits().get(index);
317
    }
318

    
319
    @Override
320
    public void addCredit(Credit credit){
321
        getCredits().add(credit);
322
    }
323

    
324

    
325
    @Override
326
    public void addCredit(Credit credit, int index){
327
        getCredits().add(index, credit);
328
    }
329

    
330
    @Override
331
    public void removeCredit(Credit credit){
332
        getCredits().remove(credit);
333
    }
334

    
335
    @Override
336
    public void removeCredit(int index){
337
        getCredits().remove(index);
338
    }
339

    
340
    @Override
341
    public boolean replaceCredit(Credit newObject, Credit oldObject){
342
        return replaceInList(this.credits, newObject, oldObject);
343
    }
344

    
345

    
346
    @Override
347
    public List<Identifier> getIdentifiers(){
348
        if(this.identifiers == null) {
349
            this.identifiers = new ArrayList<>();
350
        }
351
        return this.identifiers;
352
    }
353
    /**
354
     * @param type
355
     * @return a set of identifier value strings
356
     */
357
    public Set<String> getIdentifiers(DefinedTerm type){
358
       return getIdentifiers(type.getUuid());
359
    }
360
    /**
361
     * @param identifierTypeUuid
362
     * @return a set of identifier value strings
363
     */
364
    public Set<String> getIdentifiers(UUID identifierTypeUuid){
365
        Set<String> result = new HashSet<>();
366
        for (Identifier<?> identifier : getIdentifiers()){
367
            if (identifier.getType().getUuid().equals(identifierTypeUuid)){
368
                result.add(identifier.getIdentifier());
369
            }
370
        }
371
        return result;
372
    }
373

    
374
    @Override
375
    public Identifier addIdentifier(String identifier, DefinedTerm identifierType){
376
    	Identifier<?> result = Identifier.NewInstance(identifier, identifierType);
377
    	addIdentifier(result);
378
    	return result;
379
    }
380

    
381
    @Override
382
    public void addIdentifier(Integer index, Identifier identifier){
383
        if (identifier != null){
384
        	//deduplication
385
        	int oldIndex = getIdentifiers().indexOf(identifier);
386
        	if(oldIndex > -1){
387
        		getIdentifiers().remove(identifier);
388
        		if (index != null && oldIndex < index){
389
        			index--;
390
        		}
391
        	}
392

    
393
        	if (index != null){
394
        	    getIdentifiers().add(index, identifier);
395
        	}else{
396
        	    getIdentifiers().add(identifier);
397
        	}
398
        }
399
    }
400

    
401
    @Override
402
    public void addIdentifier(Identifier identifier){
403
        addIdentifier(null, identifier);
404
    }
405

    
406
    @Override
407
    public void removeIdentifier(Identifier identifier){
408
        if (identifier != null){
409
            getIdentifiers().remove(identifier);
410
        }
411
    }
412
    @Override
413
    public void removeIdentifier(int index){
414
    	getIdentifiers().remove(index);
415
    }
416

    
417
    @Override
418
    public boolean replaceIdentifier(Identifier newObject, Identifier oldObject){
419
        return replaceInList(this.identifiers, newObject, oldObject);
420
    }
421

    
422

    
423
    @Override
424
    public Set<Extension> getExtensions(){
425
        if(extensions == null) {
426
            this.extensions = new HashSet<>();
427
        }
428
        return this.extensions;
429
    }
430
    /**
431
     * @param type
432
     * @return a Set of extension value strings
433
     */
434
    public Set<String> getExtensions(ExtensionType type){
435
       return getExtensions(type.getUuid());
436
    }
437
    /**
438
     * @param extensionTypeUuid
439
     * @return a Set of extension value strings
440
     * @see #hasExtension(UUID, String)
441
     */
442
    public Set<String> getExtensions(UUID extensionTypeUuid){
443
        Set<String> result = new HashSet<>();
444
        for (Extension extension : getExtensions()){
445
            if (extension.getType() != null && extension.getType().getUuid().equals(extensionTypeUuid)){
446
                result.add(extension.getValue());
447
            }
448
        }
449
        return result;
450
    }
451

    
452
    public String getExtensionsConcat(UUID extensionTypeUuid, String separator){
453
        String result = null;
454
        for (Extension extension : getExtensions()){
455
            if (extension.getType().getUuid().equals(extensionTypeUuid)){
456
                result = CdmUtils.concat(separator, result, extension.getValue());
457
            }
458
        }
459
        return result;
460
    }
461

    
462
    /**
463
     * Has this entity an extension of given type with value 'value'.
464
     * If value is <code>null</code> <code>true</code> is returned if
465
     * an Extension exists with given type and 'value' is <code>null</code>.
466
     * @param extensionTypeUuid
467
     * @param value
468
     * @see #hasExtension(ExtensionType, String)
469
     * @see #getExtensions(UUID)
470
     */
471
    public boolean hasExtension(UUID extensionTypeUuid, String value) {
472
        for (String ext : this.getExtensions(extensionTypeUuid)){
473
            if (CdmUtils.nullSafeEqual(ext, value)){
474
                return true;
475
            }
476
        }
477
        return false;
478
    }
479

    
480
    /**
481
     * @see #hasExtension(UUID, String)
482
     */
483
    public boolean hasExtension(ExtensionType extensionType, String value) {
484
        return hasExtension(extensionType.getUuid(), value);
485
    }
486

    
487
    @Override
488
    public void addExtension(String value, ExtensionType extensionType){
489
        Extension.NewInstance(this, value, extensionType);
490
    }
491

    
492
    @Override
493
    public void addExtension(Extension extension){
494
        if (extension != null){
495
            getExtensions().add(extension);
496
        }
497
    }
498
    @Override
499
    public void removeExtension(Extension extension){
500
        if (extension != null){
501
            getExtensions().remove(extension);
502
        }
503
    }
504

    
505

    
506
    @Override
507
    public Set<IdentifiableSource> getSources() {
508
        if(sources == null) {
509
            this.sources = new HashSet<>();
510
        }
511
        return this.sources;
512
    }
513

    
514
    @Override
515
    public void addSource(IdentifiableSource source) {
516
        if (source != null){
517
            getSources().add(source);
518
        }
519
    }
520

    
521
    @Override
522
    public void addSources(Set<IdentifiableSource> sources) {
523
        if (sources != null){
524
        	for (IdentifiableSource source: sources){
525
	            getSources().add(source);
526
        	}
527
        }
528
    }
529

    
530
    @Override
531
    public void removeSources() {
532
       this.sources.clear();
533
    }
534

    
535
    @Override
536
    public IdentifiableSource addSource(OriginalSourceType type, String id, String idNamespace, Reference citation, String microCitation) {
537
        if (id == null && idNamespace == null && citation == null && microCitation == null){
538
            return null;
539
        }
540
        IdentifiableSource source = IdentifiableSource.NewInstance(type, id, idNamespace, citation, microCitation);
541
        addSource(source);
542
        return source;
543
    }
544

    
545

    
546
    @Override
547
    public IdentifiableSource addImportSource(String id, String idNamespace, Reference citation, String microCitation) {
548
        if (id == null && idNamespace == null && citation == null && microCitation == null){
549
            return null;
550
        }
551
        IdentifiableSource source = IdentifiableSource.NewInstance(OriginalSourceType.Import, id, idNamespace, citation, microCitation);
552
        addSource(source);
553
        return source;
554
    }
555

    
556
    @Override
557
    public IdentifiableSource addPrimaryTaxonomicSource(Reference citation, String microCitation) {
558
        if (citation == null && microCitation == null){
559
            return null;
560
        }
561
        IdentifiableSource source = IdentifiableSource.NewPrimarySourceInstance(citation, microCitation);
562
        addSource(source);
563
        return source;
564
    }
565

    
566
    @Override
567
    public IdentifiableSource addPrimaryTaxonomicSource(Reference citation) {
568
        return addPrimaryTaxonomicSource(citation, null);
569
    }
570

    
571

    
572
    @Override
573
    public void removeSource(IdentifiableSource source) {
574
        getSources().remove(source);
575
    }
576

    
577
//******************************** TO STRING *****************************************************/
578

    
579
    @Override
580
    public String toString() {
581
        String result;
582
        if (StringUtils.isBlank(titleCache)){
583
            result = super.toString();
584
        }else{
585
            result = this.titleCache;
586
        }
587
        return result;
588
    }
589

    
590

    
591
    /**
592
     * Returns the {@link eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy cache strategy} used to generate
593
     * several strings corresponding to <i>this</i> identifiable entity
594
     * (in particular taxon name caches and author strings).
595
     *
596
     * @return  the cache strategy used for <i>this</i> identifiable entity
597
     * @see     eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy
598
     */
599
    public S getCacheStrategy() {
600
        return this.cacheStrategy;
601
    }
602
    /**
603
     * @see 	#getCacheStrategy()
604
     */
605

    
606
    public void setCacheStrategy(S cacheStrategy) {
607
        this.cacheStrategy = cacheStrategy;
608
    }
609

    
610
    @Override
611
    public String generateTitle() {
612
        if (getCacheStrategy() == null){
613
            //logger.warn("No CacheStrategy defined for "+ this.getClass() + ": " + this.getUuid());
614
            return this.getClass() + ": " + this.getUuid();
615
        }else{
616
            return getCacheStrategy().getTitleCache(this);
617
        }
618
    }
619

    
620
//****************** CLONE ************************************************/
621

    
622
    @Override
623
    public Object clone() throws CloneNotSupportedException{
624
        IdentifiableEntity<?> result = (IdentifiableEntity<?>)super.clone();
625

    
626
        //Extensions
627
        result.extensions = new HashSet<>();
628
        for (Extension extension : getExtensions() ){
629
            Extension newExtension = (Extension)extension.clone();
630
            result.addExtension(newExtension);
631
        }
632

    
633
        //Identifier
634
        result.identifiers = new ArrayList<>();
635
        for (Identifier<?> identifier : getIdentifiers() ){
636
        	Identifier<?> newIdentifier = (Identifier<?>)identifier.clone();
637
            result.addIdentifier(newIdentifier);
638
        }
639

    
640
        //OriginalSources
641
        result.sources = new HashSet<>();
642
        for (IdentifiableSource source : getSources()){
643
            IdentifiableSource newSource = (IdentifiableSource)source.clone();
644
            result.addSource(newSource);
645
        }
646

    
647
        //Rights  - reusable since #5762
648
        result.rights = new HashSet<>();
649
        for(Rights right : getRights()) {
650
            result.addRights(right);
651
        }
652

    
653

    
654
        //Credits
655
        result.credits = new ArrayList<>();
656
        for(Credit credit : getCredits()) {
657
            Credit newCredit = (Credit)credit.clone();
658
            result.addCredit(newCredit);
659
        }
660

    
661
        //no changes to: lsid, titleCache, protectedTitleCache
662

    
663
        //empty titleCache
664
        if (! protectedTitleCache){
665
            result.titleCache = null;
666
        }
667

    
668
        result.initListener();
669
        return result;
670
    }
671

    
672

    
673
}
(38-38/81)