Project

General

Profile

Statistics
| Branch: | Tag: | Revision:

cdmlib / cdmlib-model / src / main / java / eu / etaxonomy / cdm / model / common / IdentifiableEntity.java @ bfa30d38

History | View | Annotate | Download (22.2 KB)

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
    public boolean updateCaches(){
271
        if (this.protectedTitleCache == false){
272
            String oldTitleCache = this.titleCache;
273

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

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

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

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

    
305
//**************************************************************************************
306

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

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

    
332

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

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

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

    
351

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

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

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

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

    
372

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

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

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

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

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

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

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

    
449

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

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

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

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

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

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

    
532

    
533
    @Override
534
    public Set<IdentifiableSource> getSources() {
535
        if(sources == null) {
536
            this.sources = new HashSet<>();
537
        }
538
        return this.sources;
539
    }
540

    
541
    @Override
542
    public void addSource(IdentifiableSource source) {
543
        if (source != null){
544
            getSources().add(source);
545
        }
546
    }
547

    
548
    @Override
549
    public void addSources(Set<IdentifiableSource> sources) {
550
        if (sources != null){
551
                for (IdentifiableSource source: sources){
552
                    getSources().add(source);
553
                }
554
        }
555
    }
556

    
557
    @Override
558
    public void removeSources() {
559
       this.sources.clear();
560
    }
561

    
562
    @Override
563
    public IdentifiableSource addSource(OriginalSourceType type, String id, String idNamespace, Reference citation, String microCitation) {
564
        if (id == null && idNamespace == null && citation == null && microCitation == null){
565
            return null;
566
        }
567
        IdentifiableSource source = IdentifiableSource.NewInstance(type, id, idNamespace, citation, microCitation);
568
        addSource(source);
569
        return source;
570
    }
571

    
572

    
573
    @Override
574
    public IdentifiableSource addImportSource(String id, String idNamespace, Reference citation, String microCitation) {
575
        if (id == null && idNamespace == null && citation == null && microCitation == null){
576
            return null;
577
        }
578
        IdentifiableSource source = IdentifiableSource.NewInstance(OriginalSourceType.Import, id, idNamespace, citation, microCitation);
579
        addSource(source);
580
        return source;
581
    }
582

    
583
    @Override
584
    public IdentifiableSource addPrimaryTaxonomicSource(Reference citation, String microCitation) {
585
        if (citation == null && microCitation == null){
586
            return null;
587
        }
588
        IdentifiableSource source = IdentifiableSource.NewPrimarySourceInstance(citation, microCitation);
589
        addSource(source);
590
        return source;
591
    }
592

    
593
    @Override
594
    public IdentifiableSource addPrimaryTaxonomicSource(Reference citation) {
595
        return addPrimaryTaxonomicSource(citation, null);
596
    }
597

    
598

    
599
    @Override
600
    public void removeSource(IdentifiableSource source) {
601
        getSources().remove(source);
602
    }
603

    
604
//******************************** TO STRING *****************************************************/
605

    
606
    @Override
607
    public String toString() {
608
        String result;
609
        if (StringUtils.isBlank(titleCache)){
610
            result = super.toString();
611
        }else{
612
            result = this.titleCache;
613
        }
614
        return result;
615
    }
616

    
617

    
618
    /**
619
     * Returns the {@link eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy cache strategy} used to generate
620
     * several strings corresponding to <i>this</i> identifiable entity
621
     * (in particular taxon name caches and author strings).
622
     *
623
     * @return  the cache strategy used for <i>this</i> identifiable entity
624
     * @see     eu.etaxonomy.cdm.strategy.cache.common.IIdentifiableEntityCacheStrategy
625
     */
626
    public S getCacheStrategy() {
627
        return this.cacheStrategy;
628
    }
629
    /**
630
     * @see         #getCacheStrategy()
631
     */
632

    
633
    public void setCacheStrategy(S cacheStrategy) {
634
        this.cacheStrategy = cacheStrategy;
635
    }
636

    
637
    @Override
638
    public String generateTitle() {
639
        if (getCacheStrategy() == null){
640
            //logger.warn("No CacheStrategy defined for "+ this.getClass() + ": " + this.getUuid());
641
            return this.getClass() + ": " + this.getUuid();
642
        }else{
643
            return getCacheStrategy().getTitleCache(this);
644
        }
645
    }
646

    
647
//****************** CLONE ************************************************/
648

    
649
    @Override
650
    public Object clone() throws CloneNotSupportedException{
651
        IdentifiableEntity<?> result = (IdentifiableEntity<?>)super.clone();
652

    
653
        //Extensions
654
        result.extensions = new HashSet<>();
655
        for (Extension extension : getExtensions() ){
656
            Extension newExtension = (Extension)extension.clone();
657
            result.addExtension(newExtension);
658
        }
659

    
660
        //Identifier
661
        result.identifiers = new ArrayList<>();
662
        for (Identifier<?> identifier : getIdentifiers() ){
663
                Identifier<?> newIdentifier = (Identifier<?>)identifier.clone();
664
            result.addIdentifier(newIdentifier);
665
        }
666

    
667
        //OriginalSources
668
        result.sources = new HashSet<>();
669
        for (IdentifiableSource source : getSources()){
670
            IdentifiableSource newSource = (IdentifiableSource)source.clone();
671
            result.addSource(newSource);
672
        }
673

    
674
        //Rights  - reusable since #5762
675
        result.rights = new HashSet<>();
676
        for(Rights right : getRights()) {
677
            result.addRights(right);
678
        }
679

    
680

    
681
        //Credits
682
        result.credits = new ArrayList<>();
683
        for(Credit credit : getCredits()) {
684
            Credit newCredit = (Credit)credit.clone();
685
            result.addCredit(newCredit);
686
        }
687

    
688
        //no changes to: lsid, titleCache, protectedTitleCache
689

    
690
        //empty titleCache
691
        if (! protectedTitleCache){
692
            result.titleCache = null;
693
        }
694

    
695
        result.initListener();
696
        return result;
697
    }
698

    
699

    
700
}
Add picture from clipboard (Maximum size: 40 MB)