Project

General

Profile

Download (23.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.net.URI;
16
import java.util.ArrayList;
17
import java.util.Collection;
18
import java.util.HashSet;
19
import java.util.List;
20
import java.util.Set;
21
import java.util.UUID;
22

    
23
import javax.persistence.Column;
24
import javax.persistence.Embedded;
25
import javax.persistence.FetchType;
26
import javax.persistence.ManyToMany;
27
import javax.persistence.MappedSuperclass;
28
import javax.persistence.OneToMany;
29
import javax.persistence.OrderColumn;
30
import javax.persistence.Transient;
31
import javax.validation.constraints.NotNull;
32
import javax.xml.bind.annotation.XmlAccessType;
33
import javax.xml.bind.annotation.XmlAccessorType;
34
import javax.xml.bind.annotation.XmlElement;
35
import javax.xml.bind.annotation.XmlElementWrapper;
36
import javax.xml.bind.annotation.XmlTransient;
37
import javax.xml.bind.annotation.XmlType;
38
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
39

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

    
53
import eu.etaxonomy.cdm.common.CdmUtils;
54
import eu.etaxonomy.cdm.hibernate.search.StripHtmlBridge;
55
import eu.etaxonomy.cdm.jaxb.FormattedTextAdapter;
56
import eu.etaxonomy.cdm.jaxb.LSIDAdapter;
57
import eu.etaxonomy.cdm.model.media.ExternalLink;
58
import eu.etaxonomy.cdm.model.media.Rights;
59
import eu.etaxonomy.cdm.model.reference.ICdmTarget;
60
import eu.etaxonomy.cdm.model.reference.OriginalSourceBase;
61
import eu.etaxonomy.cdm.model.reference.OriginalSourceType;
62
import eu.etaxonomy.cdm.model.reference.Reference;
63
import eu.etaxonomy.cdm.model.term.DefinedTerm;
64
import eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy;
65
import eu.etaxonomy.cdm.strategy.match.Match;
66
import eu.etaxonomy.cdm.strategy.match.Match.ReplaceMode;
67
import eu.etaxonomy.cdm.strategy.match.MatchMode;
68
import eu.etaxonomy.cdm.strategy.merge.Merge;
69
import eu.etaxonomy.cdm.strategy.merge.MergeMode;
70
import eu.etaxonomy.cdm.validation.Level2;
71

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

    
100
    private static final long serialVersionUID = 7912083412108359559L;
101
    private static final Logger logger = Logger.getLogger(IdentifiableEntity.class);
102

    
103
    @XmlTransient
104
    public static final boolean PROTECTED = true;
105
    @XmlTransient
106
    public static final boolean NOT_PROTECTED = false;
107

    
108
    @XmlElement(name = "LSID", type = String.class)
109
    @XmlJavaTypeAdapter(LSIDAdapter.class)
110
    @Embedded
111
    private LSID lsid;
112

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

    
128
    //if true titleCache will not be automatically generated/updated
129
    @XmlElement(name = "ProtectedTitleCache")
130
    protected boolean protectedTitleCache;
131

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

    
141
    @XmlElementWrapper(name = "Credits", nillable = true)
142
    @XmlElement(name = "Credit")
143
    @OrderColumn(name="sortIndex")
144
    @OneToMany(fetch = FetchType.LAZY, orphanRemoval=true)
145
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
146
    //TODO
147
    @Merge(MergeMode.ADD_CLONE)
148
    @NotNull
149
    private List<Credit> credits = new ArrayList<>();
150

    
151
    @XmlElementWrapper(name = "Extensions", nillable = true)
152
    @XmlElement(name = "Extension")
153
    @OneToMany(fetch = FetchType.LAZY, orphanRemoval=true)
154
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
155
    @Merge(MergeMode.ADD_CLONE)
156
    @NotNull
157
    private Set<Extension> extensions = new HashSet<>();
158

    
159
    @XmlElementWrapper(name = "Identifiers", nillable = true)
160
    @XmlElement(name = "Identifier")
161
    @OrderColumn(name="sortIndex")
162
    @OneToMany(fetch = FetchType.LAZY, orphanRemoval=true)
163
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
164
    @Merge(MergeMode.ADD_CLONE)
165
    @NotNull
166
    private List<Identifier> identifiers = new ArrayList<>();
167

    
168
    @XmlElementWrapper(name = "Links", nillable = true)
169
    @XmlElement(name = "Link")
170
    @OneToMany(fetch=FetchType.LAZY, orphanRemoval=true)
171
    @Cascade({CascadeType.SAVE_UPDATE, CascadeType.MERGE, CascadeType.DELETE})
172
    @Merge(MergeMode.ADD_CLONE)
173
    private Set<ExternalLink> links = new HashSet<>();
174

    
175
    @XmlTransient
176
    @Transient
177
    protected S cacheStrategy;
178

    
179
    protected IdentifiableEntity(){
180
        initListener();
181
    }
182

    
183
    @Override
184
    public void initListener(){
185
        PropertyChangeListener listener = new PropertyChangeListener() {
186
            @Override
187
            public void propertyChange(PropertyChangeEvent ev) {
188
                if (! "titleCache".equals(ev.getPropertyName()) && !"cacheStrategy".equals(ev.getPropertyName()) && ! isProtectedTitleCache()){
189
                    titleCache = null;
190
                }
191
            }
192
        };
193
        addPropertyChangeListener(listener);
194
    }
195

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

    
207
//******************************** CACHE *****************************************************/
208

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

    
227
    @Deprecated
228
    @Override
229
    public void setTitleCache(String titleCache){
230
    	//TODO shouldn't we call setTitleCache(String, boolean),but is this conformant with Java Bean Specification?
231
    	this.titleCache = getTruncatedCache(titleCache);
232
    }
233

    
234
    @Override
235
    public void setTitleCache(String titleCache, boolean protectCache){
236
        titleCache = getTruncatedCache(titleCache);
237
        this.titleCache = titleCache;
238
        this.protectedTitleCache = protectCache;
239
    }
240

    
241
    @Override
242
    public String resetTitleCache() {
243
        if(!protectedTitleCache){
244
            titleCache = null;
245
        }
246
        return getTitleCache();
247
    }
248

    
249
    /**
250
     * @param cache
251
     * @return
252
     */
253
    @Transient
254
    protected String getTruncatedCache(String cache) {
255
        int maxLength = 800;
256
    	if (cache != null && cache.length() > maxLength){
257
            logger.warn("Truncation of cache: " + this.toString() + "/" + cache);
258
            cache = cache.substring(0, maxLength - 4) + "...";   //TODO do we need -4 or is -3 enough
259
        }
260
        return cache;
261
    }
262

    
263

    
264
    @Override
265
    public boolean isProtectedTitleCache() {
266
        return protectedTitleCache;
267
    }
268

    
269
    @Override
270
    public void setProtectedTitleCache(boolean protectedTitleCache) {
271
        this.protectedTitleCache = protectedTitleCache;
272
    }
273

    
274
    /**
275
     *
276
     * @return true, if the current state of the titleCache (without generating it new)
277
     * is <code>null</code> or the empty string. This is primarily meant for internal use.
278
     */
279
    public boolean hasEmptyTitleCache(){
280
        return this.titleCache == null || "".equals(this.titleCache);
281
    }
282

    
283
    public boolean updateCaches(){
284
        if (this.protectedTitleCache == false){
285
            String oldTitleCache = this.titleCache;
286

    
287
            @SuppressWarnings("unchecked")
288
            String newTitleCache = cacheStrategy.getTitleCache(this);
289

    
290
            if ( oldTitleCache == null   || ! oldTitleCache.equals(newTitleCache) ){
291
                this.setTitleCache(null, false);
292
                String newCache = this.getTitleCache();
293

    
294
                if (newCache == null){
295
                    logger.warn("newCache should never be null");
296
                }
297
                if (oldTitleCache == null){
298
                    logger.info("oldTitleCache was illegaly null and has been fixed");
299
                }
300
                return true;
301
            }
302
        }
303
        return false;
304
    }
305

    
306
    /**
307
     * Updates the caches with the given cache strategy
308
     * @param entityCacheStrategy
309
     * @return <code>true</code> if some cache was updated, <code>false</code> otherwise
310
     */
311
    public boolean updateCaches(S entityCacheStrategy){
312
        S oldCacheStrategy = this.getCacheStrategy();
313
        this.cacheStrategy = entityCacheStrategy != null? entityCacheStrategy : this.getCacheStrategy();
314
        boolean result = this.updateCaches();
315
        this.cacheStrategy = oldCacheStrategy;
316
        return result;
317
    }
318

    
319
//**************************************************************************************
320

    
321
    @Override
322
    public LSID getLsid(){
323
        return this.lsid;
324
    }
325
    @Override
326
    public void setLsid(LSID lsid){
327
        this.lsid = lsid;
328
    }
329
    @Override
330
    public Set<Rights> getRights() {
331
        if(rights == null) {
332
            this.rights = new HashSet<>();
333
        }
334
        return this.rights;
335
    }
336

    
337
    @Override
338
    public void addRights(Rights right){
339
        getRights().add(right);
340
    }
341
    @Override
342
    public void removeRights(Rights right){
343
        getRights().remove(right);
344
    }
345

    
346

    
347
//********************** External Links **********************************************
348

    
349

    
350
    public Set<ExternalLink> getLinks(){
351
        return this.links;
352
    }
353
    public void setLinks(Set<ExternalLink> links){
354
        this.links = links;
355
    }
356
    public void addLink(ExternalLink link){
357
        if (link != null){
358
            links.add(link);
359
        }
360
    }
361
    public ExternalLink addLinkWebsite(URI uri, String description, Language descriptionLanguage){
362
        ExternalLink link = null;
363
        if (uri != null || description != null || descriptionLanguage != null){
364
            link = ExternalLink.NewWebSiteInstance(uri, description, descriptionLanguage);
365
            links.add(link);
366
        }
367
        return link;
368
    }
369
    public void removeLink(ExternalLink link){
370
        if(links.contains(link)) {
371
            links.remove(link);
372
        }
373
    }
374

    
375
//********************** CREDITS **********************************************
376

    
377
    @Override
378
    public List<Credit> getCredits() {
379
        if(credits == null) {
380
            this.credits = new ArrayList<>();
381
        }
382
        return this.credits;
383
    }
384

    
385
    @Override
386
    public Credit getCredits(Integer index){
387
        return getCredits().get(index);
388
    }
389

    
390
    @Override
391
    public void addCredit(Credit credit){
392
        getCredits().add(credit);
393
    }
394

    
395

    
396
    @Override
397
    public void addCredit(Credit credit, int index){
398
        getCredits().add(index, credit);
399
    }
400

    
401
    @Override
402
    public void removeCredit(Credit credit){
403
        getCredits().remove(credit);
404
    }
405

    
406
    @Override
407
    public void removeCredit(int index){
408
        getCredits().remove(index);
409
    }
410

    
411
    @Override
412
    public boolean replaceCredit(Credit newObject, Credit oldObject){
413
        return replaceInList(this.credits, newObject, oldObject);
414
    }
415

    
416

    
417
    @Override
418
    public List<Identifier> getIdentifiers(){
419
        if(this.identifiers == null) {
420
            this.identifiers = new ArrayList<>();
421
        }
422
        return this.identifiers;
423
    }
424
    /**
425
     * @param type
426
     * @return a set of identifier value strings
427
     */
428
    public Set<String> getIdentifiers(DefinedTerm type){
429
       return getIdentifiers(type == null? null :type.getUuid());
430
    }
431
    /**
432
     * @param identifierTypeUuid
433
     * @return a set of identifier value strings
434
     */
435
    public Set<String> getIdentifiers(UUID identifierTypeUuid){
436
        Set<String> result = new HashSet<>();
437
        for (Identifier<?> identifier : getIdentifiers()){
438
            if ( (identifier.getType()== null && identifierTypeUuid == null)
439
                || (identifier.getType().getUuid().equals(identifierTypeUuid))){
440
                result.add(identifier.getIdentifier());
441
            }
442
        }
443
        return result;
444
    }
445

    
446
    @Override
447
    public Identifier addIdentifier(String identifier, DefinedTerm identifierType){
448
    	Identifier<?> result = Identifier.NewInstance(identifier, identifierType);
449
    	addIdentifier(result);
450
    	return result;
451
    }
452

    
453
    @Override
454
    public void addIdentifier(Integer index, Identifier identifier){
455
        if (identifier != null){
456
        	//deduplication
457
        	int oldIndex = getIdentifiers().indexOf(identifier);
458
        	if(oldIndex > -1){
459
        		getIdentifiers().remove(identifier);
460
        		if (index != null && oldIndex < index){
461
        			index--;
462
        		}
463
        	}
464

    
465
        	if (index != null){
466
        	    getIdentifiers().add(index, identifier);
467
        	}else{
468
        	    getIdentifiers().add(identifier);
469
        	}
470
        }
471
    }
472

    
473
    @Override
474
    public void addIdentifier(Identifier identifier){
475
        addIdentifier(null, identifier);
476
    }
477

    
478
    @Override
479
    public void removeIdentifier(Identifier identifier){
480
        if (identifier != null){
481
            getIdentifiers().remove(identifier);
482
        }
483
    }
484
    @Override
485
    public void removeIdentifier(int index){
486
    	getIdentifiers().remove(index);
487
    }
488

    
489
    @Override
490
    public boolean replaceIdentifier(Identifier newObject, Identifier oldObject){
491
        return replaceInList(this.identifiers, newObject, oldObject);
492
    }
493

    
494

    
495
    @Override
496
    public Set<Extension> getExtensions(){
497
        if(extensions == null) {
498
            this.extensions = new HashSet<>();
499
        }
500
        return this.extensions;
501
    }
502
    public Set<Extension> getFilteredExtensions(UUID extensionTypeUuid){
503
        Set<Extension> result = new HashSet<>();
504
        for (Extension extension : getExtensions()){
505
            if (extension.getType() != null && extension.getType().getUuid().equals(extensionTypeUuid)){
506
                result.add(extension);
507
            }
508
        }
509
        return result;
510
     }
511
    /**
512
     * @param type
513
     * @return a Set of extension value strings
514
     */
515
    public Set<String> getExtensions(ExtensionType type){
516
       return getExtensions(type.getUuid());
517
    }
518
    /**
519
     * @param extensionTypeUuid
520
     * @return a Set of extension value strings
521
     * @see #hasExtension(UUID, String)
522
     */
523
    public Set<String> getExtensions(UUID extensionTypeUuid){
524
        Set<String> result = new HashSet<>();
525
        for (Extension extension : getExtensions()){
526
            if (extension.getType() != null && extension.getType().getUuid().equals(extensionTypeUuid)){
527
                result.add(extension.getValue());
528
            }
529
        }
530
        return result;
531
    }
532

    
533
    /**
534
     * @see #getExtensionsConcat(Collection, String)
535
     */
536
    public String getExtensionsConcat(UUID extensionTypeUuid, String separator){
537
        String result = null;
538
        for (Extension extension : getExtensions()){
539
            if (extension.getType() != null && extension.getType().getUuid().equals(extensionTypeUuid)){
540
                result = CdmUtils.concat(separator, result, extension.getValue());
541
            }
542
        }
543
        return result;
544
    }
545

    
546
    /**
547
     * Return all extensions matching the given extension type as
548
     * concatenated string. If extensionTypeUuids is a sorted collection
549
     * it is given in the correct order.
550
     * @param extensionTypeUuids collection of the extension types to be considered
551
     * @param separator the separator for concatenation
552
     * @return the concatenated extension string
553
     * @see #getExtensionsConcat(Collection, String)
554
     */
555
    public String getExtensionsConcat(Collection<UUID> extensionTypeUuids, String separator){
556
        String result = null;
557
        for (UUID uuid : extensionTypeUuids){
558
            String extension = getExtensionsConcat(uuid, separator);
559
            result = CdmUtils.concat(separator, result, extension);
560
        }
561
        return result;
562
    }
563

    
564
    /**
565
     * Has this entity an extension of given type with value 'value'.
566
     * If value is <code>null</code> <code>true</code> is returned if
567
     * an Extension exists with given type and 'value' is <code>null</code>.
568
     * @param extensionTypeUuid
569
     * @param value
570
     * @see #hasExtension(ExtensionType, String)
571
     * @see #getExtensions(UUID)
572
     */
573
    public boolean hasExtension(UUID extensionTypeUuid, String value) {
574
        for (String ext : this.getExtensions(extensionTypeUuid)){
575
            if (CdmUtils.nullSafeEqual(ext, value)){
576
                return true;
577
            }
578
        }
579
        return false;
580
    }
581

    
582
    /**
583
     * @see #hasExtension(UUID, String)
584
     */
585
    public boolean hasExtension(ExtensionType extensionType, String value) {
586
        return hasExtension(extensionType.getUuid(), value);
587
    }
588

    
589
    @Override
590
    public void addExtension(String value, ExtensionType extensionType){
591
        Extension.NewInstance(this, value, extensionType);
592
    }
593

    
594
    @Override
595
    public void addExtension(Extension extension){
596
        if (extension != null){
597
            getExtensions().add(extension);
598
        }
599
    }
600
    @Override
601
    public void removeExtension(Extension extension){
602
        if (extension != null){
603
            getExtensions().remove(extension);
604
        }
605
    }
606

    
607
    @Override
608
    public void addSource(IdentifiableSource source) {
609
        if (source != null){
610
            getSources().add(source);
611
        }
612
    }
613

    
614
    @Override
615
    public void addSources(Set<IdentifiableSource> sources) {
616
        if (sources != null){
617
        	for (IdentifiableSource source: sources){
618
	            getSources().add(source);
619
        	}
620
        }
621
    }
622

    
623
    @Override
624
    protected IdentifiableSource createNewSource(OriginalSourceType type, String idInSource, String idNamespace,
625
            Reference reference, String microReference, String originalInfo, ICdmTarget target) {
626
        return IdentifiableSource.NewInstance(type, idInSource, idNamespace, reference, microReference, originalInfo, target);
627
    }
628

    
629
//******************************** TO STRING *****************************************************/
630

    
631
    @Override
632
    public String toString() {
633
        String result;
634
        if (isBlank(titleCache)){
635
            result = super.toString();
636
        }else{
637
            result = this.titleCache;
638
        }
639
        return result;
640
    }
641

    
642

    
643
    /**
644
     * Returns the {@link eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy cache strategy} used to generate
645
     * several strings corresponding to <i>this</i> identifiable entity
646
     * (in particular taxon name caches and author strings).
647
     *
648
     * @return  the cache strategy used for <i>this</i> identifiable entity
649
     * @see     eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy
650
     */
651
    public S getCacheStrategy() {
652
        return this.cacheStrategy;
653
    }
654
    /**
655
     * @see 	#getCacheStrategy()
656
     */
657

    
658
    public void setCacheStrategy(S cacheStrategy) {
659
        this.cacheStrategy = cacheStrategy;
660
    }
661

    
662
    @Override
663
    public String generateTitle() {
664
        if (getCacheStrategy() == null){
665
            //logger.warn("No CacheStrategy defined for "+ this.getClass() + ": " + this.getUuid());
666
            return this.getClass() + ": " + this.getUuid();
667
        }else{
668
            return getCacheStrategy().getTitleCache(this);
669
        }
670
    }
671

    
672
//****************** CLONE ************************************************/
673

    
674
    @Override
675
    public IdentifiableEntity<S> clone() throws CloneNotSupportedException{
676

    
677
        @SuppressWarnings("unchecked")
678
        IdentifiableEntity<S> result = (IdentifiableEntity<S>)super.clone();
679

    
680
        //Extensions
681
        result.extensions = new HashSet<>();
682
        for (Extension extension : getExtensions() ){
683
            Extension newExtension = extension.clone();
684
            result.addExtension(newExtension);
685
        }
686

    
687
        //Identifier
688
        result.identifiers = new ArrayList<>();
689
        for (Identifier<?> identifier : getIdentifiers() ){
690
        	Identifier<?> newIdentifier = identifier.clone();
691
            result.addIdentifier(newIdentifier);
692
        }
693

    
694
        //Rights  - reusable since #5762
695
        result.rights = new HashSet<>();
696
        for(Rights right : getRights()) {
697
            result.addRights(right);
698
        }
699

    
700
        //Credits
701
        result.credits = new ArrayList<>();
702
        for(Credit credit : getCredits()) {
703
            Credit newCredit = credit.clone();
704
            result.addCredit(newCredit);
705
        }
706

    
707
        //Links
708
        result.links = new HashSet<>();
709
        for(ExternalLink link : getLinks()) {
710
            ExternalLink newLink = link.clone();
711
            result.addLink(newLink);
712
        }
713

    
714
        //no changes to: lsid, titleCache, protectedTitleCache
715

    
716
        //empty titleCache
717
        if (! protectedTitleCache){
718
            result.titleCache = null;
719
        }
720

    
721
        result.initListener();  //TODO why do we need this, isnt't the listener in constructor enough?
722
        return result;
723
    }
724
}
(30-30/56)