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;
035import javax.jcr.lock.Lock;
036import javax.jcr.lock.LockManager;
037
038import org.xml.sax.ContentHandler;
039import org.xml.sax.SAXException;
040
041import org.ametys.cms.content.references.OutgoingReferences;
042import org.ametys.cms.content.references.OutgoingReferencesHelper;
043import org.ametys.cms.contenttype.ContentType;
044import org.ametys.cms.data.holder.IndexableDataHolder;
045import org.ametys.cms.data.holder.impl.DefaultModelAwareDataHolder;
046import org.ametys.cms.search.model.SystemPropertyExtensionPoint;
047import org.ametys.core.user.UserIdentity;
048import org.ametys.core.util.DateUtils;
049import org.ametys.plugins.explorer.resources.ResourceCollection;
050import org.ametys.plugins.repository.AmetysObject;
051import org.ametys.plugins.repository.AmetysObjectIterable;
052import org.ametys.plugins.repository.AmetysObjectResolver;
053import org.ametys.plugins.repository.AmetysRepositoryException;
054import org.ametys.plugins.repository.CopiableAmetysObject;
055import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
056import org.ametys.plugins.repository.RepositoryConstants;
057import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
058import org.ametys.plugins.repository.UnknownAmetysObjectException;
059import org.ametys.plugins.repository.data.UnknownDataException;
060import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder;
061import org.ametys.plugins.repository.data.holder.impl.DefaultModifiableModelLessDataHolder;
062import org.ametys.plugins.repository.data.repositorydata.ModifiableRepositoryData;
063import org.ametys.plugins.repository.data.repositorydata.RepositoryData;
064import org.ametys.plugins.repository.data.repositorydata.impl.JCRRepositoryData;
065import org.ametys.plugins.repository.dublincore.DCMITypes;
066import org.ametys.plugins.repository.jcr.DefaultAmetysObject;
067import org.ametys.plugins.repository.jcr.DublinCoreHelper;
068import org.ametys.plugins.repository.jcr.JCRTraversableAmetysObject;
069import org.ametys.plugins.repository.metadata.MultilingualString;
070import org.ametys.plugins.repository.tag.TaggableAmetysObjectHelper;
071import org.ametys.runtime.model.View;
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    private boolean _lockAlreadyChecked;
146    
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        Map<String, OutgoingReferences> outgoingReferencesByPath = new HashMap<>();
398        
399        try
400        {
401            Node contentNode = getNode();
402            if (contentNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_ROOT_OUTGOING_REFERENCES))
403            {
404                Node rootOutgoingRefsNode = contentNode.getNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_ROOT_OUTGOING_REFERENCES);
405                
406                // Loop on outgoing ref node by (metadata) path.
407                NodeIterator outgoingRefsNodeIterator = rootOutgoingRefsNode.getNodes(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_OUTGOING_REFERENCES);
408                while (outgoingRefsNodeIterator.hasNext())
409                {
410                    Node outgoingRefsNode = outgoingRefsNodeIterator.nextNode();
411                    String path = outgoingRefsNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ':' + METADATA_OUTGOING_REFERENCES_PATH_PROPERTY).getString();
412                    
413                    // Loop on each outgoing ref node values for each reference type and collecting outgoing references.
414                    OutgoingReferences outgoingReferences = new OutgoingReferences();
415                    NodeIterator outgoingReferenceNodeIterator = outgoingRefsNode.getNodes();
416                    while (outgoingReferenceNodeIterator.hasNext())
417                    {
418                        Node outgoingReferenceNode = outgoingReferenceNodeIterator.nextNode();
419                        Value[] referenceValues = outgoingReferenceNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ':' + METADATA_OUTGOING_REFERENCE_PROPERTY).getValues();
420                        
421                        List<String> referenceValuesAsList = new ArrayList<>(referenceValues.length);
422                        for (Value value : referenceValues)
423                        {
424                            referenceValuesAsList.add(value.getString());
425                        }
426                        
427                        outgoingReferences.put(outgoingReferenceNode.getName(), referenceValuesAsList);
428                    }
429                    
430                    // Updating the outgoing references map
431                    if (!outgoingReferences.isEmpty())
432                    {
433                        if (outgoingReferencesByPath.containsKey(path))
434                        {
435                            outgoingReferencesByPath.get(path).merge(outgoingReferences);
436                        }
437                        else
438                        {
439                            outgoingReferencesByPath.put(path, outgoingReferences);
440                        }
441                    }
442                }
443            }
444        }
445        catch (RepositoryException e)
446        {
447            throw new AmetysRepositoryException(e);
448        }
449        
450        return outgoingReferencesByPath;
451    }
452    
453    @Override
454    public Collection<Content> getReferencingContents() throws AmetysRepositoryException
455    {
456        return _getReferencingContents(Long.MAX_VALUE).referencingContents();
457    }
458    
459    private ReferencingContentsSearch _getReferencingContents(long limit) throws AmetysRepositoryException
460    {
461        Set<Content> contents = new LinkedHashSet<>();
462        long total = 0;
463        boolean hasOtherReferences = false;
464        
465        try
466        {
467            NodeIterator results = OutgoingReferencesHelper.getContentOutgoingReferences(this);
468            total = results.getSize();
469            
470            AmetysObjectResolver resolver = _getFactory()._getAOResolver();
471            while (results.hasNext() && contents.size() < limit)
472            {
473                Node node = results.nextNode();
474                Node contentNode = node.getParent()  // go up towards node 'ametys-internal:outgoing-references
475                                       .getParent()  // go up towards node 'ametys-internal:root-outgoing-references
476                                       .getParent(); // go up towards node of the content
477                Content content = resolver.resolve(contentNode, false);
478                contents.add(content);
479            }
480            
481            hasOtherReferences = results.hasNext();
482        }
483        catch (RepositoryException e)
484        {
485            throw new AmetysRepositoryException("Unable to resolve references for content " + getId(), e);
486        }
487        
488        return new ReferencingContentsSearch(total, contents, hasOtherReferences);
489    }
490    
491    @Override
492    public ReferencingContentsSearch searchReferencingContents(int limit) throws AmetysRepositoryException
493    {
494        return _getReferencingContents(limit);
495    }
496    
497    public boolean hasReferencingContents() throws AmetysRepositoryException
498    {
499        try
500        {
501            return OutgoingReferencesHelper.getContentOutgoingReferences(this).getSize() != 0;
502        }
503        catch (RepositoryException e)
504        {
505            throw new AmetysRepositoryException("A repository exception occured: ", e);
506        }
507    }
508    
509    @Override
510    public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException
511    {
512        return copyTo(parent, name);
513    }
514    
515    @Override
516    public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException
517    {
518        return copyTo(parent, name, 0, true, true);
519    }
520    
521    /**
522     * Copy the current {@link DefaultWorkflowAwareContent} to the given object. Be careful, this method save changes, but do not create a new version (checkpoint)
523     * @param parent The parent of the new object. Can not be null.
524     * @param name Name of the new object. Can be null. If null, the new name will be get from the copied object.
525     * @param initWorkflowActionId The initial workflow action id
526     * @param notifyObservers Set to false to do not fire observer events
527     * @param checkpoint true to check the content in if it is versionable
528     * @return the created object
529     * @throws AmetysRepositoryException if an error occurs.
530     */
531    public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name, int initWorkflowActionId, boolean notifyObservers, boolean checkpoint) throws AmetysRepositoryException
532    {
533        return _getFactory()._getContentDAO().copy(this, parent, name, null, initWorkflowActionId, notifyObservers, checkpoint, false, false);
534    }
535    
536    /**
537     * Copy the current {@link DefaultWorkflowAwareContent} to the given object. Be careful, this method save changes, but do not create a new version (checkpoint)
538     * @param parent The parent of the new object. Can not be null.
539     * @param name Name of the new object. Can be null. If null, the new name will be get from the copied object.
540     * @param lang Language of the new object. Can be null. If null, the new language will be get from the copied object.
541     * @param initWorkflowActionId The initial workflow action id
542     * @param waitAsyncObservers if true, waits if necessary for the asynchronous observers to complete
543     * @param copyACL true to copy ACL of source content
544     * @return the created object
545     * @throws AmetysRepositoryException if an error occurs.
546     */
547    public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name, String lang, int initWorkflowActionId, boolean waitAsyncObservers, boolean copyACL) throws AmetysRepositoryException
548    {
549        return _getFactory()._getContentDAO().copy(this, parent, name, lang, initWorkflowActionId, true, true, waitAsyncObservers, copyACL);
550    }
551
552    void _checkLock() throws RepositoryException
553    {
554        Node node = getNode();
555        if (!_lockAlreadyChecked && getNode().isLocked())
556        {
557            LockManager lockManager = node.getSession().getWorkspace().getLockManager();
558            
559            Lock lock = lockManager.getLock(node.getPath());
560            Node lockHolder = lock.getNode();
561            
562            lockManager.addLockToken(lockHolder.getProperty(RepositoryConstants.METADATA_LOCKTOKEN).getString());
563            _lockAlreadyChecked = true;
564        }
565    }
566    
567    // Dublin Core metadata. //
568    
569    @Override
570    public String getDCTitle() throws AmetysRepositoryException
571    {
572        return DublinCoreHelper.getDCTitle(this, getTitle());
573    }
574    
575    @Override
576    public String getDCCreator() throws AmetysRepositoryException
577    {
578        return DublinCoreHelper.getDCCreator(this, UserIdentity.userIdentityToString(getCreator()));
579    }
580
581    @Override
582    public String[] getDCSubject() throws AmetysRepositoryException
583    {
584        return DublinCoreHelper.getDCSubject(this);
585    }
586
587    @Override
588    public String getDCDescription() throws AmetysRepositoryException
589    {
590        String seoDesc = null;
591        try
592        {
593            RepositoryData repositoryData = new JCRRepositoryData(getNode());
594            RepositoryData seoReposioryData = repositoryData.getRepositoryData("seo");
595            seoDesc = seoReposioryData.getString("description");
596        }
597        catch (UnknownDataException me)
598        {
599            seoDesc = null;
600        }
601        
602        return DublinCoreHelper.getDCDescription(this, seoDesc);
603    }
604
605    @Override
606    public String getDCPublisher() throws AmetysRepositoryException
607    {
608        return DublinCoreHelper.getDCPublisher(this, getLastValidator().map(UserIdentity::userIdentityToString).orElse(null));
609    }
610    
611    @Override
612    public String getDCContributor() throws AmetysRepositoryException
613    {
614        return DublinCoreHelper.getDCContributor(this, UserIdentity.userIdentityToString(getLastContributor()));
615    }
616    
617    @Override
618    public Date getDCDate() throws AmetysRepositoryException
619    {
620        Date defaultDCDate = Optional.ofNullable(getLastValidationDate())
621                                     .map(DateUtils::asDate)
622                                     .orElse(null);
623        return DublinCoreHelper.getDCDate(this, defaultDCDate);
624        
625    }
626    
627    @Override
628    public String getDCType() throws AmetysRepositoryException
629    {
630        return DublinCoreHelper.getDCType(this, DCMITypes.TEXT);
631    }
632    
633    @Override
634    public String getDCFormat() throws AmetysRepositoryException
635    {
636        return DublinCoreHelper.getDCFormat(this, "text/html");
637    }
638    
639    @Override
640    public String getDCIdentifier() throws AmetysRepositoryException
641    {
642        return DublinCoreHelper.getDCIdentifier(this, getId());
643    }
644    
645    @Override
646    public String getDCSource() throws AmetysRepositoryException
647    {
648        return DublinCoreHelper.getDCSource(this);
649    }
650    
651    @Override
652    public String getDCLanguage() throws AmetysRepositoryException
653    {
654        return DublinCoreHelper.getDCLanguage(this, getLanguage());
655    }
656    
657    @Override
658    public String getDCRelation() throws AmetysRepositoryException
659    {
660        return DublinCoreHelper.getDCRelation(this);
661    }
662    
663    @Override
664    public String getDCCoverage() throws AmetysRepositoryException
665    {
666        return DublinCoreHelper.getDCCoverage(this, getDCLanguage());
667    }
668    
669    @Override
670    public String getDCRights() throws AmetysRepositoryException
671    {
672        return DublinCoreHelper.getDCRights(this);
673    }
674
675    @Override
676    public ResourceCollection getRootAttachments() throws AmetysRepositoryException
677    {
678        ResourceCollection attachments = null;
679        
680        if (hasChild(ATTACHMENTS_NODE_NAME))
681        {
682            attachments = getChild(ATTACHMENTS_NODE_NAME);
683        }
684        
685        return attachments;
686    }
687    
688    @Override
689    public boolean hasChild(String name) throws AmetysRepositoryException
690    {
691        return _getFactory().hasChild(this, name);
692    }
693    
694    @SuppressWarnings("unchecked")
695    @Override
696    public <A extends AmetysObject> A createChild(String name, String type) throws AmetysRepositoryException, RepositoryIntegrityViolationException
697    {
698        return (A) _getFactory().createChild(this, name, type);
699    }
700    
701    @SuppressWarnings("unchecked")
702    @Override
703    public <A extends AmetysObject> A getChild(String path) throws AmetysRepositoryException, UnknownAmetysObjectException
704    {
705        return (A) _getFactory().getChild(this, path);
706    }
707    
708    @Override
709    public <A extends AmetysObject> AmetysObjectIterable<A> getChildren() throws AmetysRepositoryException
710    {
711        return _getFactory().getChildren(this);
712    }
713
714    public IndexableDataHolder getDataHolder()
715    {
716        // It is necessary to instantiate the repository data because of locks:
717        // when the content is locked, the lock token is released to be taken by anyone
718        // in repository data there is a lock checked boolean that need to be reset in order to take the token
719        // this boolean is only reset at instantiation
720        RepositoryData repositoryData = new JCRRepositoryData(getNode());
721        Collection<ContentType> contentTypes = _getFactory().getContentHelper().getContentTypes(this);
722        return new DefaultModelAwareDataHolder(repositoryData, Optional.empty(), Optional.of(this), contentTypes);
723    }
724    
725    public ModifiableModelLessDataHolder getInternalDataHolder()
726    {
727        // It is necessary to instantiate the repository data because of locks:
728        // when the content is locked, the lock token is released to be taken by anyone
729        // in repository data there is a lock checked boolean that need to be reset in order to take the token
730        // this boolean is only reset at instantiation
731        ModifiableRepositoryData repositoryData = new JCRRepositoryData(getNode(), RepositoryConstants.NAMESPACE_PREFIX_INTERNAL);
732        return new DefaultModifiableModelLessDataHolder(_getFactory().getInternalDataTypesExtensionPoint(), repositoryData);
733    }
734    
735    public Optional<SystemPropertyExtensionPoint> getSystemPropertyExtensionPoint()
736    {
737        return Optional.of(_getFactory().getSystemPropertyExtensionPoint());
738    }
739    
740    @Override
741    public void addReaction(UserIdentity user, ReactionType reactionType)
742    {
743        ReactionableObjectHelper.addReaction(getUnversionedDataHolder(), user, reactionType);
744    }
745
746    @Override
747    public void removeReaction(UserIdentity user, ReactionType reactionType)
748    {
749        ReactionableObjectHelper.removeReaction(getUnversionedDataHolder(), user, reactionType);
750    }
751
752    @Override
753    public List<UserIdentity> getReactionUsers(ReactionType reactionType)
754    {
755        return ReactionableObjectHelper.getReactionUsers(getUnversionedDataHolder(), reactionType);
756    }
757
758    public void addReport()
759    {
760        ReportableObjectHelper.addReport(getUnversionedDataHolder());
761    }
762    
763    public void setReportsCount(long reportsCount)
764    {
765        ReportableObjectHelper.setReportsCount(getUnversionedDataHolder(), reportsCount);
766    }
767
768    public void clearReports()
769    {
770        ReportableObjectHelper.clearReports(getUnversionedDataHolder());
771    }
772
773    public long getReportsCount()
774    {
775        return ReportableObjectHelper.getReportsCount(getUnversionedDataHolder());
776    }
777    
778    public void toSAX(ContentHandler contentHandler, Locale locale, View view, boolean saxWorkflowStep) throws SAXException
779    {
780        _getFactory().getContentSaxer().saxContent(this, contentHandler, locale, view, "content", saxWorkflowStep, false, false, "attributes");
781    }
782}