Project

General

Profile

Download (20.6 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
})
87
@Audited
88
@MappedSuperclass
89
public abstract class IdentifiableEntity<S extends IIdentifiableEntityCacheStrategy>
90
        extends SourcedEntityBase<IdentifiableSource>
91
        implements IIdentifiableEntity /*, ISourceable<IdentifiableSource> */ {
92

    
93
    private static final long serialVersionUID = 7912083412108359559L;
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", length=800) //see #1592
109
    @Match(value=MatchMode.CACHE, cacheReplaceMode=ReplaceMode.ALL)
110
    @NotEmpty(groups = Level2.class) // implicitly NotNull
111
    @Fields({
112
        @Field(store=Store.YES),
113
        //  If the field is only needed for sorting and nothing else, you may configure it as
114
        //  un-indexed and un-stored, thus avoid unnecessary index growth.
115
        @Field(name = "titleCache__sort", analyze = Analyze.NO, store=Store.NO, index = Index.NO)
116
    })
117
    @SortableField(forField = "titleCache__sort")
118
    @FieldBridge(impl=StripHtmlBridge.class)
119
    protected String titleCache;
120

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

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

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

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

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

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

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

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

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

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

    
201
//******************************** CACHE *****************************************************/
202

    
203
    // @Transient  - must not be transient, since this property needs to to be included in all serializations produced by the remote layer
204
    @Override
205
    public String getTitleCache(){
206
        if (protectedTitleCache){
207
            return this.titleCache;
208
        }
209
        // is title dirty, i.e. equal NULL?
210
        if (titleCache == null){
211
            this.titleCache = generateTitle();
212
            this.titleCache = getTruncatedCache(this.titleCache) ;
213
        }
214
        //removed due to #5849
215
//        if(StringUtils.isBlank(titleCache)){
216
//            titleCache = this.toString();
217
//        }
218
        return titleCache;
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
     * @param cache
237
     * @return
238
     */
239
    @Transient
240
    protected String getTruncatedCache(String cache) {
241
        int maxLength = 800;
242
    	if (cache != null && cache.length() > maxLength){
243
            logger.warn("Truncation of cache: " + this.toString() + "/" + cache);
244
            cache = cache.substring(0, maxLength - 4) + "...";   //TODO do we need -4 or is -3 enough
245
        }
246
        return cache;
247
    }
248

    
249

    
250
    @Override
251
    public boolean isProtectedTitleCache() {
252
        return protectedTitleCache;
253
    }
254

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

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

    
269
    public boolean updateCaches(){
270
        if (this.protectedTitleCache == false){
271
            String oldTitleCache = this.titleCache;
272

    
273
            String newTitleCache = cacheStrategy.getTitleCache(this);
274

    
275
            if ( oldTitleCache == null   || ! oldTitleCache.equals(newTitleCache) ){
276
                this.setTitleCache(null, false);
277
                String newCache = this.getTitleCache();
278

    
279
                if (newCache == null){
280
                    logger.warn("newCache should never be null");
281
                }
282
                if (oldTitleCache == null){
283
                    logger.info("oldTitleCache was illegaly null and has been fixed");
284
                }
285
                return true;
286
            }
287
        }
288
        return false;
289
    }
290

    
291
    /**
292
     * Updates the caches with the given cache strategy
293
     * @param entityCacheStrategy
294
     * @return <code>true</code> if some cache was updated, <code>false</code> otherwise
295
     */
296
    public boolean updateCaches(S entityCacheStrategy){
297
        S oldCacheStrategy = this.getCacheStrategy();
298
        this.cacheStrategy = entityCacheStrategy != null? entityCacheStrategy : this.getCacheStrategy();
299
        boolean result = this.updateCaches();
300
        this.cacheStrategy = oldCacheStrategy;
301
        return result;
302
    }
303

    
304
//**************************************************************************************
305

    
306
    @Override
307
    public LSID getLsid(){
308
        return this.lsid;
309
    }
310
    @Override
311
    public void setLsid(LSID lsid){
312
        this.lsid = lsid;
313
    }
314
    @Override
315
    public Set<Rights> getRights() {
316
        if(rights == null) {
317
            this.rights = new HashSet<>();
318
        }
319
        return this.rights;
320
    }
321

    
322
    @Override
323
    public void addRights(Rights right){
324
        getRights().add(right);
325
    }
326
    @Override
327
    public void removeRights(Rights right){
328
        getRights().remove(right);
329
    }
330

    
331

    
332
    @Override
333
    public List<Credit> getCredits() {
334
        if(credits == null) {
335
            this.credits = new ArrayList<>();
336
        }
337
        return this.credits;
338
    }
339

    
340
    @Override
341
    public Credit getCredits(Integer index){
342
        return getCredits().get(index);
343
    }
344

    
345
    @Override
346
    public void addCredit(Credit credit){
347
        getCredits().add(credit);
348
    }
349

    
350

    
351
    @Override
352
    public void addCredit(Credit credit, int index){
353
        getCredits().add(index, credit);
354
    }
355

    
356
    @Override
357
    public void removeCredit(Credit credit){
358
        getCredits().remove(credit);
359
    }
360

    
361
    @Override
362
    public void removeCredit(int index){
363
        getCredits().remove(index);
364
    }
365

    
366
    @Override
367
    public boolean replaceCredit(Credit newObject, Credit oldObject){
368
        return replaceInList(this.credits, newObject, oldObject);
369
    }
370

    
371

    
372
    @Override
373
    public List<Identifier> getIdentifiers(){
374
        if(this.identifiers == null) {
375
            this.identifiers = new ArrayList<>();
376
        }
377
        return this.identifiers;
378
    }
379
    /**
380
     * @param type
381
     * @return a set of identifier value strings
382
     */
383
    public Set<String> getIdentifiers(DefinedTerm type){
384
       return getIdentifiers(type.getUuid());
385
    }
386
    /**
387
     * @param identifierTypeUuid
388
     * @return a set of identifier value strings
389
     */
390
    public Set<String> getIdentifiers(UUID identifierTypeUuid){
391
        Set<String> result = new HashSet<>();
392
        for (Identifier<?> identifier : getIdentifiers()){
393
            if (identifier.getType().getUuid().equals(identifierTypeUuid)){
394
                result.add(identifier.getIdentifier());
395
            }
396
        }
397
        return result;
398
    }
399

    
400
    @Override
401
    public Identifier addIdentifier(String identifier, DefinedTerm identifierType){
402
    	Identifier<?> result = Identifier.NewInstance(identifier, identifierType);
403
    	addIdentifier(result);
404
    	return result;
405
    }
406

    
407
    @Override
408
    public void addIdentifier(Integer index, Identifier identifier){
409
        if (identifier != null){
410
        	//deduplication
411
        	int oldIndex = getIdentifiers().indexOf(identifier);
412
        	if(oldIndex > -1){
413
        		getIdentifiers().remove(identifier);
414
        		if (index != null && oldIndex < index){
415
        			index--;
416
        		}
417
        	}
418

    
419
        	if (index != null){
420
        	    getIdentifiers().add(index, identifier);
421
        	}else{
422
        	    getIdentifiers().add(identifier);
423
        	}
424
        }
425
    }
426

    
427
    @Override
428
    public void addIdentifier(Identifier identifier){
429
        addIdentifier(null, identifier);
430
    }
431

    
432
    @Override
433
    public void removeIdentifier(Identifier identifier){
434
        if (identifier != null){
435
            getIdentifiers().remove(identifier);
436
        }
437
    }
438
    @Override
439
    public void removeIdentifier(int index){
440
    	getIdentifiers().remove(index);
441
    }
442

    
443
    @Override
444
    public boolean replaceIdentifier(Identifier newObject, Identifier oldObject){
445
        return replaceInList(this.identifiers, newObject, oldObject);
446
    }
447

    
448

    
449
    @Override
450
    public Set<Extension> getExtensions(){
451
        if(extensions == null) {
452
            this.extensions = new HashSet<>();
453
        }
454
        return this.extensions;
455
    }
456
    /**
457
     * @param type
458
     * @return a Set of extension value strings
459
     */
460
    public Set<String> getExtensions(ExtensionType type){
461
       return getExtensions(type.getUuid());
462
    }
463
    /**
464
     * @param extensionTypeUuid
465
     * @return a Set of extension value strings
466
     * @see #hasExtension(UUID, String)
467
     */
468
    public Set<String> getExtensions(UUID extensionTypeUuid){
469
        Set<String> result = new HashSet<>();
470
        for (Extension extension : getExtensions()){
471
            if (extension.getType() != null && extension.getType().getUuid().equals(extensionTypeUuid)){
472
                result.add(extension.getValue());
473
            }
474
        }
475
        return result;
476
    }
477

    
478
    public String getExtensionsConcat(UUID extensionTypeUuid, String separator){
479
        String result = null;
480
        for (Extension extension : getExtensions()){
481
            if (extension.getType().getUuid().equals(extensionTypeUuid)){
482
                result = CdmUtils.concat(separator, result, extension.getValue());
483
            }
484
        }
485
        return result;
486
    }
487

    
488
    /**
489
     * Has this entity an extension of given type with value 'value'.
490
     * If value is <code>null</code> <code>true</code> is returned if
491
     * an Extension exists with given type and 'value' is <code>null</code>.
492
     * @param extensionTypeUuid
493
     * @param value
494
     * @see #hasExtension(ExtensionType, String)
495
     * @see #getExtensions(UUID)
496
     */
497
    public boolean hasExtension(UUID extensionTypeUuid, String value) {
498
        for (String ext : this.getExtensions(extensionTypeUuid)){
499
            if (CdmUtils.nullSafeEqual(ext, value)){
500
                return true;
501
            }
502
        }
503
        return false;
504
    }
505

    
506
    /**
507
     * @see #hasExtension(UUID, String)
508
     */
509
    public boolean hasExtension(ExtensionType extensionType, String value) {
510
        return hasExtension(extensionType.getUuid(), value);
511
    }
512

    
513
    @Override
514
    public void addExtension(String value, ExtensionType extensionType){
515
        Extension.NewInstance(this, value, extensionType);
516
    }
517

    
518
    @Override
519
    public void addExtension(Extension extension){
520
        if (extension != null){
521
            getExtensions().add(extension);
522
        }
523
    }
524
    @Override
525
    public void removeExtension(Extension extension){
526
        if (extension != null){
527
            getExtensions().remove(extension);
528
        }
529
    }
530

    
531
    @Override
532
    public void addSource(IdentifiableSource source) {
533
        if (source != null){
534
            getSources().add(source);
535
        }
536
    }
537

    
538
    @Override
539
    public void addSources(Set<IdentifiableSource> sources) {
540
        if (sources != null){
541
        	for (IdentifiableSource source: sources){
542
	            getSources().add(source);
543
        	}
544
        }
545
    }
546

    
547

    
548
    /**
549
     * {@inheritDoc}
550
     */
551
    @Override
552
    protected IdentifiableSource createNewInstance(OriginalSourceType type, String idInSource, String idNamespace,
553
            Reference reference, String microReference, String originalInfo) {
554
        return IdentifiableSource.NewInstance(type, idInSource, idNamespace, reference, microReference, originalInfo);
555
    }
556

    
557
//******************************** TO STRING *****************************************************/
558

    
559
    @Override
560
    public String toString() {
561
        String result;
562
        if (StringUtils.isBlank(titleCache)){
563
            result = super.toString();
564
        }else{
565
            result = this.titleCache;
566
        }
567
        return result;
568
    }
569

    
570

    
571
    /**
572
     * Returns the {@link eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy cache strategy} used to generate
573
     * several strings corresponding to <i>this</i> identifiable entity
574
     * (in particular taxon name caches and author strings).
575
     *
576
     * @return  the cache strategy used for <i>this</i> identifiable entity
577
     * @see     eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy
578
     */
579
    public S getCacheStrategy() {
580
        return this.cacheStrategy;
581
    }
582
    /**
583
     * @see 	#getCacheStrategy()
584
     */
585

    
586
    public void setCacheStrategy(S cacheStrategy) {
587
        this.cacheStrategy = cacheStrategy;
588
    }
589

    
590
    @Override
591
    public String generateTitle() {
592
        if (getCacheStrategy() == null){
593
            //logger.warn("No CacheStrategy defined for "+ this.getClass() + ": " + this.getUuid());
594
            return this.getClass() + ": " + this.getUuid();
595
        }else{
596
            return getCacheStrategy().getTitleCache(this);
597
        }
598
    }
599

    
600
//****************** CLONE ************************************************/
601

    
602
    @Override
603
    public Object clone() throws CloneNotSupportedException{
604
        IdentifiableEntity<?> result = (IdentifiableEntity<?>)super.clone();
605

    
606
        //Extensions
607
        result.extensions = new HashSet<>();
608
        for (Extension extension : getExtensions() ){
609
            Extension newExtension = (Extension)extension.clone();
610
            result.addExtension(newExtension);
611
        }
612

    
613
        //Identifier
614
        result.identifiers = new ArrayList<>();
615
        for (Identifier<?> identifier : getIdentifiers() ){
616
        	Identifier<?> newIdentifier = (Identifier<?>)identifier.clone();
617
            result.addIdentifier(newIdentifier);
618
        }
619

    
620
        //Rights  - reusable since #5762
621
        result.rights = new HashSet<>();
622
        for(Rights right : getRights()) {
623
            result.addRights(right);
624
        }
625

    
626
        //Credits
627
        result.credits = new ArrayList<>();
628
        for(Credit credit : getCredits()) {
629
            Credit newCredit = (Credit)credit.clone();
630
            result.addCredit(newCredit);
631
        }
632

    
633
        //no changes to: lsid, titleCache, protectedTitleCache
634

    
635
        //empty titleCache
636
        if (! protectedTitleCache){
637
            result.titleCache = null;
638
        }
639

    
640
        result.initListener();
641
        return result;
642
    }
643

    
644

    
645
}
(39-39/83)