001/*
002 *  Copyright 2010 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.cms.repository;
017
018import java.time.ZonedDateTime;
019import java.util.ArrayList;
020import java.util.Calendar;
021import java.util.Collection;
022import java.util.Date;
023import java.util.HashMap;
024import java.util.LinkedHashSet;
025import java.util.List;
026import java.util.Locale;
027import java.util.Map;
028import java.util.Optional;
029import java.util.Set;
030
031import javax.jcr.Node;
032import javax.jcr.NodeIterator;
033import javax.jcr.RepositoryException;
034import javax.jcr.Value;
035
036import org.xml.sax.ContentHandler;
037import org.xml.sax.SAXException;
038
039import org.ametys.cms.content.references.OutgoingReferences;
040import org.ametys.cms.content.references.OutgoingReferencesHelper;
041import org.ametys.cms.contenttype.ContentType;
042import org.ametys.cms.data.holder.IndexableDataHolder;
043import org.ametys.cms.data.holder.impl.DefaultModelAwareDataHolder;
044import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
045import org.ametys.core.user.UserIdentity;
046import org.ametys.core.util.DateUtils;
047import org.ametys.plugins.explorer.resources.ResourceCollection;
048import org.ametys.plugins.repository.AmetysObject;
049import org.ametys.plugins.repository.AmetysObjectIterable;
050import org.ametys.plugins.repository.AmetysObjectResolver;
051import org.ametys.plugins.repository.AmetysRepositoryException;
052import org.ametys.plugins.repository.CopiableAmetysObject;
053import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
054import org.ametys.plugins.repository.RepositoryConstants;
055import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
056import org.ametys.plugins.repository.UnknownAmetysObjectException;
057import org.ametys.plugins.repository.data.UnknownDataException;
058import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
059import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableModelLessDataHolder;
060import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData;
061import org.ametys.plugins.repository.data.repositorydata.RepositoryData;
062import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData;
063import org.ametys.plugins.repository.dublincore.DCMITypes;
064import org.ametys.plugins.repository.jcr.DefaultAmetysObject;
065import org.ametys.plugins.repository.jcr.DublinCoreHelper;
066import org.ametys.plugins.repository.jcr.JCRTraversableAmetysObject;
067import org.ametys.plugins.repository.lock.LockableAmetysObject;
068import org.ametys.plugins.repository.metadata.MultilingualString;
069import org.ametys.plugins.repository.tag.TaggableAmetysObjectHelper;
070import org.ametys.runtime.model.View;
071import org.ametys.runtime.model.type.DataContext;
072import org.ametys.runtime.model.type.ModelItemTypeConstants;
073
074/**
075 * Default implementation of a {@link Content}, also versionable, lockable and workflow-aware.
076 * @param <F> the actual type of factory.
077 */
078public class DefaultContent<F extends ContentFactory> extends DefaultAmetysObject<F> implements Content, CopiableAmetysObject, JCRTraversableAmetysObject, ReactionableObject, ReportableObject
079{
080    /** Constants for the root outgoing references node */
081    public static final String METADATA_ROOT_OUTGOING_REFERENCES = "root-outgoing-references";
082    
083    /** Constants for the outgoing references node */
084    public static final String METADATA_OUTGOING_REFERENCES = "outgoing-references";
085    
086    /** Constants for the outgoing references path property */
087    public static final String METADATA_OUTGOING_REFERENCES_PATH_PROPERTY = "path";
088    
089    /** Constants for the outgoing reference property */
090    public static final String METADATA_OUTGOING_REFERENCE_PROPERTY = "reference";
091    
092    /** Constants for the outgoing reference nodetype */
093    public static final String METADATA_OUTGOING_REFERENCE_NODETYPE = "reference";
094    
095    /** Constants for language Metadata* */
096    public static final String METADATA_LANGUAGE = "language";
097    
098    /** Constants for author Metadata* */
099    public static final String METADATA_CREATOR = "creator";
100    
101    /** Constants for lastModified Metadata* */
102    public static final String METADATA_CREATION = "creationDate";
103    
104    /** Constants for the first validator metadata */
105    public static final String METADATA_FIRST_VALIDATOR = "firstValidator";
106    
107    /** Constants for firstValidationDate Metadata* */
108    public static final String METADATA_FIRST_VALIDATION = "firstValidationDate";
109    
110    /** Constants for last validator Metadata* */
111    public static final String METADATA_LAST_VALIDATOR = "lastValidator";
112    
113    /** Constants for lastValidationDate Metadata* */
114    public static final String METADATA_LAST_VALIDATION = "lastValidationDate";
115    
116    /** Constants for last major validator Metadata* */
117    public static final String METADATA_LAST_MAJOR_VALIDATOR = "lastMajorValidator";
118    
119    /** Constants for lastMajorValidationDate Metadata* */
120    public static final String METADATA_LAST_MAJORVALIDATION = "lastMajorValidationDate";
121    
122    /** Constants for last contributor Metadata* */
123    public static final String METADATA_CONTRIBUTOR = "contributor";
124    
125    /** Constants for lastModified Metadata* */
126    public static final String METADATA_MODIFIED = "lastModified";
127    
128    /** Constants for contentType Metadata* */
129    public static final String METADATA_CONTENTTYPE = "contentType";
130    
131    /** Constants for contentType Metadata* */
132    public static final String METADATA_MIXINCONTENTTYPES = "mixins";
133    
134    /** Constant for the attachment node name. */
135    public static final String ATTACHMENTS_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":attachments";
136    
137    /** The default locale for content */
138    public static final Locale DEFAULT_CONTENT_LOCALE = Locale.ENGLISH;
139    
140    /** The type identifiers of this content */
141    protected String[] _types;
142    /** The mixin type identifiers of this content */
143    protected String[] _mixins;
144    
145    // Store already retrieved outgoing references, if any
146    private Map<String, OutgoingReferences> _outgoingReferences;
147
148    /**
149     * Creates a JCR-based Content.
150     * @param node the JCR Node backing this Content.
151     * @param parentPath the parent path in the Ametys hierarchy.
152     * @param factory the corresponding {@link ContentFactory}.
153     */
154    public DefaultContent(Node node, String parentPath, F factory)
155    {
156        super(node, parentPath, factory);
157    }
158    
159    @Override
160    public String[] getTypes() throws AmetysRepositoryException
161    {
162        if (_types == null)
163        {
164            _types = getInternalDataHolder().getValueOfType(METADATA_CONTENTTYPE, ModelItemTypeConstants.STRING_TYPE_ID, new String[0]);
165        }
166        
167        return _types;
168    }
169    
170    @Override
171    public void setType(String type) throws AmetysRepositoryException
172    {
173        setTypes(new String[] {type});
174    }
175    
176    @Override
177    public void setTypes(String[] types) throws AmetysRepositoryException
178    {
179        getInternalDataHolder().setValue(DefaultContent.METADATA_CONTENTTYPE, types, ModelItemTypeConstants.STRING_TYPE_ID);
180        _types = types;
181    }
182    
183    @Override
184    public String[] getMixinTypes() throws AmetysRepositoryException
185    {
186        if (_mixins == null)
187        {
188            _mixins = getInternalDataHolder().getValueOfType(METADATA_MIXINCONTENTTYPES, ModelItemTypeConstants.STRING_TYPE_ID, new String[0]);
189        }
190        return _mixins;
191    }
192    
193    @Override
194    public void setMixinTypes(String[] mixins) throws AmetysRepositoryException
195    {
196        getInternalDataHolder().setValue(DefaultContent.METADATA_MIXINCONTENTTYPES, mixins, ModelItemTypeConstants.STRING_TYPE_ID);
197        _mixins = mixins;
198    }
199    
200    @Override
201    public String getLanguage() throws AmetysRepositoryException
202    {
203        return getInternalDataHolder().getValueOfType(METADATA_LANGUAGE, ModelItemTypeConstants.STRING_TYPE_ID);
204    }
205    
206    @Override
207    public void setLanguage(String language) throws AmetysRepositoryException
208    {
209        getInternalDataHolder().setValue(DefaultContent.METADATA_LANGUAGE, language, ModelItemTypeConstants.STRING_TYPE_ID);
210    }
211    
212    public String getTitle(Locale locale) throws UnknownDataException, AmetysRepositoryException
213    {
214        Object value = getValue(ATTRIBUTE_TITLE);
215        if (value == null)
216        {
217            throw new UnknownDataException("Unknown attribute " + ATTRIBUTE_TITLE + " for content " + getId());
218        }
219        else if (value instanceof MultilingualString)
220        {
221            MultilingualString multilingual = (MultilingualString) value;
222            
223            if (locale != null && multilingual.hasLocale(locale))
224            {
225                return multilingual.getValue(locale);
226            }
227            else if (multilingual.hasLocale(DEFAULT_CONTENT_LOCALE))
228            {
229                return multilingual.getValue(DEFAULT_CONTENT_LOCALE);
230            }
231            else if (multilingual.getValues().isEmpty())
232            {
233                throw new UnknownDataException("Unknown attribute " + ATTRIBUTE_TITLE + " for content " + getId());
234            }
235            else
236            {
237                return multilingual.getValues().get(0);
238            }
239        }
240        else
241        {
242            return (String) value;
243        }
244    }
245    
246    public String getTitle() throws UnknownDataException, AmetysRepositoryException
247    {
248        return getTitle(null);
249    }
250    
251    @Override
252    public UserIdentity getCreator() throws UnknownDataException, AmetysRepositoryException
253    {
254        try
255        {
256            RepositoryData repositoryData = new JCRRepositoryData(getNode());
257            RepositoryData creatorReposioryData = repositoryData.getRepositoryData(METADATA_CREATOR);
258            return new UserIdentity(creatorReposioryData.getString("login"), creatorReposioryData.getString("population"));
259        }
260        catch (AmetysRepositoryException e)
261        {
262            throw new AmetysRepositoryException("Error while getting creator property for content " + this, e);
263        }
264    }
265    
266    @Override
267    public ZonedDateTime getCreationDate() throws UnknownDataException, AmetysRepositoryException
268    {
269        RepositoryData repositoryData = new JCRRepositoryData(getNode());
270        Calendar date = repositoryData.getDate(METADATA_CREATION);
271        return DateUtils.asZonedDateTime(date);
272    }
273    
274    @Override
275    public UserIdentity getLastContributor() throws UnknownDataException, AmetysRepositoryException
276    {
277        try
278        {
279            RepositoryData repositoryData = new JCRRepositoryData(getNode());
280            RepositoryData contributorReposioryData = repositoryData.getRepositoryData(METADATA_CONTRIBUTOR);
281            return new UserIdentity(contributorReposioryData.getString("login"), contributorReposioryData.getString("population"));
282        }
283        catch (AmetysRepositoryException e)
284        {
285            throw new AmetysRepositoryException("Error while getting creator property for content " + this, e);
286        }
287    }
288    
289    @Override
290    public ZonedDateTime getLastModified() throws UnknownDataException, AmetysRepositoryException
291    {
292        RepositoryData repositoryData = new JCRRepositoryData(getNode());
293        Calendar date = repositoryData.getDate(METADATA_MODIFIED);
294        return DateUtils.asZonedDateTime(date);
295    }
296    
297    public Optional<UserIdentity> getFirstValidator() throws UnknownDataException, AmetysRepositoryException
298    {
299        RepositoryData repositoryData = new JCRRepositoryData(getNode());
300        if (repositoryData.hasValue(METADATA_FIRST_VALIDATOR))
301        {
302            try
303            {
304                RepositoryData contributorReposioryData = repositoryData.getRepositoryData(METADATA_FIRST_VALIDATOR);
305                return Optional.of(new UserIdentity(contributorReposioryData.getString("login"), contributorReposioryData.getString("population")));
306            }
307            catch (AmetysRepositoryException e)
308            {
309                throw new AmetysRepositoryException("Error while getting creator property for content " + this, e);
310            }
311        }
312        return Optional.empty();
313    }
314    
315    @Override
316    public ZonedDateTime getFirstValidationDate() throws UnknownDataException, AmetysRepositoryException
317    {
318        RepositoryData repositoryData = new JCRRepositoryData(getNode());
319        if (repositoryData.hasValue(METADATA_FIRST_VALIDATION))
320        {
321            Calendar date = repositoryData.getDate(METADATA_FIRST_VALIDATION);
322            return DateUtils.asZonedDateTime(date);
323        }
324        return null;
325    }
326    
327    public Optional<UserIdentity> getLastValidator() throws AmetysRepositoryException
328    {
329        RepositoryData repositoryData = new JCRRepositoryData(getNode());
330        if (repositoryData.hasValue(METADATA_LAST_VALIDATOR))
331        {
332            try
333            {
334                RepositoryData contributorReposioryData = repositoryData.getRepositoryData(METADATA_LAST_VALIDATOR);
335                return Optional.of(new UserIdentity(contributorReposioryData.getString("login"), contributorReposioryData.getString("population")));
336            }
337            catch (AmetysRepositoryException e)
338            {
339                throw new AmetysRepositoryException("Error while getting creator property for content " + this, e);
340            }
341        }
342        return Optional.empty();
343    }
344    
345    @Override
346    public ZonedDateTime getLastValidationDate() throws UnknownDataException, AmetysRepositoryException
347    {
348        RepositoryData repositoryData = new JCRRepositoryData(getNode());
349        if (repositoryData.hasValue(METADATA_LAST_VALIDATION))
350        {
351            Calendar date = repositoryData.getDate(METADATA_LAST_VALIDATION);
352            return DateUtils.asZonedDateTime(date);
353        }
354        return null;
355    }
356    
357    public Optional<UserIdentity> getLastMajorValidator() throws UnknownDataException, AmetysRepositoryException
358    {
359        RepositoryData repositoryData = new JCRRepositoryData(getNode());
360        if (repositoryData.hasValue(METADATA_LAST_MAJOR_VALIDATOR))
361        {
362            try
363            {
364                RepositoryData contributorReposioryData = repositoryData.getRepositoryData(METADATA_LAST_MAJOR_VALIDATOR);
365                return Optional.of(new UserIdentity(contributorReposioryData.getString("login"), contributorReposioryData.getString("population")));
366            }
367            catch (AmetysRepositoryException e)
368            {
369                throw new AmetysRepositoryException("Error while getting creator property for content " + this, e);
370            }
371        }
372        return Optional.empty();
373    }
374    
375    @Override
376    public ZonedDateTime getLastMajorValidationDate() throws AmetysRepositoryException
377    {
378        RepositoryData repositoryData = new JCRRepositoryData(getNode());
379        if (repositoryData.hasValue(METADATA_LAST_MAJORVALIDATION))
380        {
381            Calendar date = repositoryData.getDate(METADATA_LAST_MAJORVALIDATION);
382            return DateUtils.asZonedDateTime(date);
383        }
384        return null;
385    }
386    
387    // Tag management.
388    @Override
389    public Set<String> getTags() throws AmetysRepositoryException
390    {
391        return TaggableAmetysObjectHelper.getTags(this);
392    }
393    
394    @Override
395    public Map<String, OutgoingReferences> getOutgoingReferences() throws AmetysRepositoryException
396    {
397        // small optimization to avoid many JCR operations for outgoing references retrieval if already retrieved for the same Content instance
398        if (_outgoingReferences != null)
399        {
400            return _outgoingReferences;
401        }
402        
403        Map<String, OutgoingReferences> outgoingReferencesByPath = new HashMap<>();
404        
405        try
406        {
407            Node contentNode = getNode();
408            if (contentNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_ROOT_OUTGOING_REFERENCES))
409            {
410                Node rootOutgoingRefsNode = contentNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_ROOT_OUTGOING_REFERENCES);
411                
412                // Loop on outgoing ref node by (metadata) path.
413                NodeIterator outgoingRefsNodeIterator = rootOutgoingRefsNode.getNodes(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_OUTGOING_REFERENCES);
414                while (outgoingRefsNodeIterator.hasNext())
415                {
416                    Node outgoingRefsNode = outgoingRefsNodeIterator.nextNode();
417                    String path = outgoingRefsNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_OUTGOING_REFERENCES_PATH_PROPERTY).getString();
418                    
419                    // Loop on each outgoing ref node values for each reference type and collecting outgoing references.
420                    OutgoingReferences outgoingReferences = new OutgoingReferences();
421                    NodeIterator outgoingReferenceNodeIterator = outgoingRefsNode.getNodes();
422                    while (outgoingReferenceNodeIterator.hasNext())
423                    {
424                        Node outgoingReferenceNode = outgoingReferenceNodeIterator.nextNode();
425                        Value[] referenceValues = outgoingReferenceNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_OUTGOING_REFERENCE_PROPERTY).getValues();
426                        
427                        List<String> referenceValuesAsList = new ArrayList<>(referenceValues.length);
428                        for (Value value : referenceValues)
429                        {
430                            referenceValuesAsList.add(value.getString());
431                        }
432                        
433                        outgoingReferences.put(outgoingReferenceNode.getName(), referenceValuesAsList);
434                    }
435                    
436                    // Updating the outgoing references map
437                    if (!outgoingReferences.isEmpty())
438                    {
439                        if (outgoingReferencesByPath.containsKey(path))
440                        {
441                            outgoingReferencesByPath.get(path).merge(outgoingReferences);
442                        }
443                        else
444                        {
445                            outgoingReferencesByPath.put(path, outgoingReferences);
446                        }
447                    }
448                }
449            }
450        }
451        catch (RepositoryException e)
452        {
453            throw new AmetysRepositoryException(e);
454        }
455        
456        _outgoingReferences = outgoingReferencesByPath;
457        return outgoingReferencesByPath;
458    }
459    
460    @Override
461    public Collection<Content> getReferencingContents() throws AmetysRepositoryException
462    {
463        return _getReferencingContents(Long.MAX_VALUE).referencingContents();
464    }
465    
466    private ReferencingContentsSearch _getReferencingContents(long limit) throws AmetysRepositoryException
467    {
468        Set<Content> contents = new LinkedHashSet<>();
469        long total = 0;
470        boolean hasOtherReferences = false;
471        
472        try
473        {
474            NodeIterator results = OutgoingReferencesHelper.getContentOutgoingReferences(this);
475            total = results.getSize();
476            
477            AmetysObjectResolver resolver = _getFactory()._getAOResolver();
478            while (results.hasNext() && contents.size() < limit)
479            {
480                Node node = results.nextNode();
481                Node contentNode = node.getParent()  // go up towards node 'ametys-internal:outgoing-references
482                                       .getParent()  // go up towards node 'ametys-internal:root-outgoing-references
483                                       .getParent(); // go up towards node of the content
484                Content content = resolver.resolve(contentNode, false);
485                contents.add(content);
486            }
487            
488            hasOtherReferences = results.hasNext();
489        }
490        catch (RepositoryException e)
491        {
492            throw new AmetysRepositoryException("Unable to resolve references for content " + getId(), e);
493        }
494        
495        return new ReferencingContentsSearch(total, contents, hasOtherReferences);
496    }
497    
498    @Override
499    public ReferencingContentsSearch searchReferencingContents(int limit) throws AmetysRepositoryException
500    {
501        return _getReferencingContents(limit);
502    }
503    
504    public boolean hasReferencingContents() throws AmetysRepositoryException
505    {
506        try
507        {
508            return OutgoingReferencesHelper.getContentOutgoingReferences(this).getSize() != 0;
509        }
510        catch (RepositoryException e)
511        {
512            throw new AmetysRepositoryException("A repository exception occured: ", e);
513        }
514    }
515    
516    @Override
517    public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException
518    {
519        return copyTo(parent, name);
520    }
521    
522    @Override
523    public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException
524    {
525        return copyTo(parent, name, 0, true, true);
526    }
527    
528    /**
529     * Copy the current {@link DefaultWorkflowAwareContent} to the given object. Be careful, this method save changes, but do not create a new version (checkpoint)
530     * @param parent The parent of the new object. Can not be null.
531     * @param name Name of the new object. Can be null. If null, the new name will be get from the copied object.
532     * @param initWorkflowActionId The initial workflow action id
533     * @param notifyObservers Set to false to do not fire observer events
534     * @param checkpoint true to check the content in if it is versionable
535     * @return the created object
536     * @throws AmetysRepositoryException if an error occurs.
537     */
538    public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name, int initWorkflowActionId, boolean notifyObservers, boolean checkpoint) throws AmetysRepositoryException
539    {
540        return _getFactory()._getContentDAO().copy(this, parent, name, null, initWorkflowActionId, notifyObservers, checkpoint, false, false, DataContext.newInstance());
541    }
542    
543    /**
544     * Copy the current {@link DefaultWorkflowAwareContent} to the given object. Be careful, this method save changes, but do not create a new version (checkpoint)
545     * @param parent The parent of the new object. Can not be null.
546     * @param name Name of the new object. Can be null. If null, the new name will be get from the copied object.
547     * @param lang Language of the new object. Can be null. If null, the new language will be get from the copied object.
548     * @param initWorkflowActionId The initial workflow action id
549     * @param waitAsyncObservers if true, waits if necessary for the asynchronous observers to complete
550     * @param copyACL true to copy ACL of source content
551     * @param context The context of the data to copy
552     * @return the created object
553     * @throws AmetysRepositoryException if an error occurs.
554     */
555    public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name, String lang, int initWorkflowActionId, boolean waitAsyncObservers, boolean copyACL, DataContext context) throws AmetysRepositoryException
556    {
557        return _getFactory()._getContentDAO().copy(this, parent, name, lang, initWorkflowActionId, true, true, waitAsyncObservers, copyACL, context);
558    }
559
560    // Dublin Core metadata. //
561    
562    @Override
563    public String getDCTitle() throws AmetysRepositoryException
564    {
565        return DublinCoreHelper.getDCTitle(this, getTitle());
566    }
567    
568    @Override
569    public String getDCCreator() throws AmetysRepositoryException
570    {
571        return DublinCoreHelper.getDCCreator(this, UserIdentity.userIdentityToString(getCreator()));
572    }
573
574    @Override
575    public String[] getDCSubject() throws AmetysRepositoryException
576    {
577        return DublinCoreHelper.getDCSubject(this);
578    }
579
580    @Override
581    public String getDCDescription() throws AmetysRepositoryException
582    {
583        String seoDesc = null;
584        try
585        {
586            RepositoryData repositoryData = new JCRRepositoryData(getNode());
587            RepositoryData seoReposioryData = repositoryData.getRepositoryData("seo");
588            seoDesc = seoReposioryData.getString("description");
589        }
590        catch (UnknownDataException me)
591        {
592            seoDesc = null;
593        }
594        
595        return DublinCoreHelper.getDCDescription(this, seoDesc);
596    }
597
598    @Override
599    public String getDCPublisher() throws AmetysRepositoryException
600    {
601        return DublinCoreHelper.getDCPublisher(this, getLastValidator().map(UserIdentity::userIdentityToString).orElse(null));
602    }
603    
604    @Override
605    public String getDCContributor() throws AmetysRepositoryException
606    {
607        return DublinCoreHelper.getDCContributor(this, UserIdentity.userIdentityToString(getLastContributor()));
608    }
609    
610    @Override
611    public Date getDCDate() throws AmetysRepositoryException
612    {
613        Date defaultDCDate = Optional.ofNullable(getLastValidationDate())
614                                     .map(DateUtils::asDate)
615                                     .orElse(null);
616        return DublinCoreHelper.getDCDate(this, defaultDCDate);
617        
618    }
619    
620    @Override
621    public String getDCType() throws AmetysRepositoryException
622    {
623        return DublinCoreHelper.getDCType(this, DCMITypes.TEXT);
624    }
625    
626    @Override
627    public String getDCFormat() throws AmetysRepositoryException
628    {
629        return DublinCoreHelper.getDCFormat(this, "text/html");
630    }
631    
632    @Override
633    public String getDCIdentifier() throws AmetysRepositoryException
634    {
635        return DublinCoreHelper.getDCIdentifier(this, getId());
636    }
637    
638    @Override
639    public String getDCSource() throws AmetysRepositoryException
640    {
641        return DublinCoreHelper.getDCSource(this);
642    }
643    
644    @Override
645    public String getDCLanguage() throws AmetysRepositoryException
646    {
647        return DublinCoreHelper.getDCLanguage(this, getLanguage());
648    }
649    
650    @Override
651    public String getDCRelation() throws AmetysRepositoryException
652    {
653        return DublinCoreHelper.getDCRelation(this);
654    }
655    
656    @Override
657    public String getDCCoverage() throws AmetysRepositoryException
658    {
659        return DublinCoreHelper.getDCCoverage(this, getDCLanguage());
660    }
661    
662    @Override
663    public String getDCRights() throws AmetysRepositoryException
664    {
665        return DublinCoreHelper.getDCRights(this);
666    }
667
668    @Override
669    public ResourceCollection getRootAttachments() throws AmetysRepositoryException
670    {
671        ResourceCollection attachments = null;
672        
673        if (hasChild(ATTACHMENTS_NODE_NAME))
674        {
675            attachments = getChild(ATTACHMENTS_NODE_NAME);
676        }
677        
678        return attachments;
679    }
680    
681    @Override
682    public boolean hasChild(String name) throws AmetysRepositoryException
683    {
684        return _getFactory().hasChild(this, name);
685    }
686    
687    @SuppressWarnings("unchecked")
688    @Override
689    public <A extends AmetysObject> A createChild(String name, String type) throws AmetysRepositoryException, RepositoryIntegrityViolationException
690    {
691        return (A) _getFactory().createChild(this, name, type);
692    }
693    
694    @SuppressWarnings("unchecked")
695    @Override
696    public <A extends AmetysObject> A getChild(String path) throws AmetysRepositoryException, UnknownAmetysObjectException
697    {
698        return (A) _getFactory().getChild(this, path);
699    }
700    
701    @Override
702    public <A extends AmetysObject> AmetysObjectIterable<A> getChildren() throws AmetysRepositoryException
703    {
704        return _getFactory().getChildren(this);
705    }
706
707    public IndexableDataHolder getDataHolder()
708    {
709        // It is necessary to instantiate the repository data because of locks:
710        // when the content is locked, the lock token is released to be taken by anyone
711        // in repository data there is a lock checked boolean that need to be reset in order to take the token
712        // this boolean is only reset at instantiation
713        RepositoryData repositoryData = new JCRRepositoryData(getNode());
714        Collection<ContentType> contentTypes = _getFactory().getContentHelper().getContentTypes(this);
715        return new DefaultModelAwareDataHolder(repositoryData, contentTypes, Optional.empty(), Optional.of(this));
716    }
717    
718    public ModifiableModelLessDataHolder getInternalDataHolder()
719    {
720        // It is necessary to instantiate the repository data because of locks:
721        // when the content is locked, the lock token is released to be taken by anyone
722        // in repository data there is a lock checked boolean that need to be reset in order to take the token
723        // this boolean is only reset at instantiation
724        ModifiableRepositoryData repositoryData = new JCRRepositoryData(getNode(), RepositoryConstants.NAMESPACE_PREFIX_INTERNAL);
725        Optional<LockableAmetysObject> lockableAmetysObject = Optional.of(this)
726                                                                      .filter(LockableAmetysObject.class::isInstance)
727                                                                      .map(LockableAmetysObject.class:: cast);
728        return new DefaultModifiableModelLessDataHolder(_getFactory().getInternalDataTypesExtensionPoint(), repositoryData, lockableAmetysObject);
729    }
730    
731    public Optional<SystemPropertyExtensionPoint> getSystemPropertyExtensionPoint()
732    {
733        return Optional.of(_getFactory().getSystemPropertyExtensionPoint());
734    }
735    
736    @Override
737    public void addReaction(UserIdentity user, ReactionType reactionType)
738    {
739        ReactionableObjectHelper.addReaction(getUnversionedDataHolder(), user, reactionType);
740    }
741
742    @Override
743    public void removeReaction(UserIdentity user, ReactionType reactionType)
744    {
745        ReactionableObjectHelper.removeReaction(getUnversionedDataHolder(), user, reactionType);
746    }
747
748    @Override
749    public List<UserIdentity> getReactionUsers(ReactionType reactionType)
750    {
751        return ReactionableObjectHelper.getReactionUsers(getUnversionedDataHolder(), reactionType);
752    }
753
754    public void addReport()
755    {
756        ReportableObjectHelper.addReport(getUnversionedDataHolder());
757    }
758    
759    public void setReportsCount(long reportsCount)
760    {
761        ReportableObjectHelper.setReportsCount(getUnversionedDataHolder(), reportsCount);
762    }
763
764    public void clearReports()
765    {
766        ReportableObjectHelper.clearReports(getUnversionedDataHolder());
767    }
768
769    public long getReportsCount()
770    {
771        return ReportableObjectHelper.getReportsCount(getUnversionedDataHolder());
772    }
773    
774    public void toSAX(ContentHandler contentHandler, Locale locale, View view, boolean saxWorkflowStep) throws SAXException
775    {
776        _getFactory().getContentSaxer().saxContent(this, contentHandler, locale, view, "content", saxWorkflowStep, false, false, "attributes");
777    }
778}