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    protected 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 notifyObservers Set to false to do not fire observer events
550     * @param checkpoint true to check the content in if it is versionable
551     * @param waitAsyncObservers if true, waits if necessary for the asynchronous observers to complete
552     * @param copyACL true to copy ACL of source content
553     * @param context The context of the data to copy
554     * @return the created object
555     * @throws AmetysRepositoryException if an error occurs.
556     */
557    public ModifiableContent copyTo(ModifiableTraversableAmetysObject parent, String name, String lang, int initWorkflowActionId, boolean notifyObservers, boolean checkpoint, boolean waitAsyncObservers, boolean copyACL, DataContext context) throws AmetysRepositoryException
558    {
559        return _getFactory()._getContentDAO().copy(this, parent, name, lang, initWorkflowActionId, notifyObservers, checkpoint, waitAsyncObservers, copyACL, context);
560    }
561
562    // Dublin Core metadata. //
563    
564    @Override
565    public String getDCTitle() throws AmetysRepositoryException
566    {
567        return DublinCoreHelper.getDCTitle(this, getTitle());
568    }
569    
570    @Override
571    public String getDCCreator() throws AmetysRepositoryException
572    {
573        return DublinCoreHelper.getDCCreator(this, UserIdentity.userIdentityToString(getCreator()));
574    }
575
576    @Override
577    public String[] getDCSubject() throws AmetysRepositoryException
578    {
579        return DublinCoreHelper.getDCSubject(this);
580    }
581
582    @Override
583    public String getDCDescription() throws AmetysRepositoryException
584    {
585        String seoDesc = null;
586        try
587        {
588            RepositoryData repositoryData = new JCRRepositoryData(getNode());
589            RepositoryData seoReposioryData = repositoryData.getRepositoryData("seo");
590            seoDesc = seoReposioryData.getString("description");
591        }
592        catch (UnknownDataException me)
593        {
594            seoDesc = null;
595        }
596        
597        return DublinCoreHelper.getDCDescription(this, seoDesc);
598    }
599
600    @Override
601    public String getDCPublisher() throws AmetysRepositoryException
602    {
603        return DublinCoreHelper.getDCPublisher(this, getLastValidator().map(UserIdentity::userIdentityToString).orElse(null));
604    }
605    
606    @Override
607    public String getDCContributor() throws AmetysRepositoryException
608    {
609        return DublinCoreHelper.getDCContributor(this, UserIdentity.userIdentityToString(getLastContributor()));
610    }
611    
612    @Override
613    public Date getDCDate() throws AmetysRepositoryException
614    {
615        Date defaultDCDate = Optional.ofNullable(getLastValidationDate())
616                                     .map(DateUtils::asDate)
617                                     .orElse(null);
618        return DublinCoreHelper.getDCDate(this, defaultDCDate);
619        
620    }
621    
622    @Override
623    public String getDCType() throws AmetysRepositoryException
624    {
625        return DublinCoreHelper.getDCType(this, DCMITypes.TEXT);
626    }
627    
628    @Override
629    public String getDCFormat() throws AmetysRepositoryException
630    {
631        return DublinCoreHelper.getDCFormat(this, "text/html");
632    }
633    
634    @Override
635    public String getDCIdentifier() throws AmetysRepositoryException
636    {
637        return DublinCoreHelper.getDCIdentifier(this, getId());
638    }
639    
640    @Override
641    public String getDCSource() throws AmetysRepositoryException
642    {
643        return DublinCoreHelper.getDCSource(this);
644    }
645    
646    @Override
647    public String getDCLanguage() throws AmetysRepositoryException
648    {
649        return DublinCoreHelper.getDCLanguage(this, getLanguage());
650    }
651    
652    @Override
653    public String getDCRelation() throws AmetysRepositoryException
654    {
655        return DublinCoreHelper.getDCRelation(this);
656    }
657    
658    @Override
659    public String getDCCoverage() throws AmetysRepositoryException
660    {
661        return DublinCoreHelper.getDCCoverage(this, getDCLanguage());
662    }
663    
664    @Override
665    public String getDCRights() throws AmetysRepositoryException
666    {
667        return DublinCoreHelper.getDCRights(this);
668    }
669
670    @Override
671    public ResourceCollection getRootAttachments() throws AmetysRepositoryException
672    {
673        ResourceCollection attachments = null;
674        
675        if (hasChild(ATTACHMENTS_NODE_NAME))
676        {
677            attachments = getChild(ATTACHMENTS_NODE_NAME);
678        }
679        
680        return attachments;
681    }
682    
683    @Override
684    public boolean hasChild(String name) throws AmetysRepositoryException
685    {
686        return _getFactory().hasChild(this, name);
687    }
688    
689    @SuppressWarnings("unchecked")
690    @Override
691    public <A extends AmetysObject> A createChild(String name, String type) throws AmetysRepositoryException, RepositoryIntegrityViolationException
692    {
693        return (A) _getFactory().createChild(this, name, type);
694    }
695    
696    @SuppressWarnings("unchecked")
697    @Override
698    public <A extends AmetysObject> A getChild(String path) throws AmetysRepositoryException, UnknownAmetysObjectException
699    {
700        return (A) _getFactory().getChild(this, path);
701    }
702    
703    @Override
704    public <A extends AmetysObject> AmetysObjectIterable<A> getChildren() throws AmetysRepositoryException
705    {
706        return _getFactory().getChildren(this);
707    }
708
709    public IndexableDataHolder getDataHolder()
710    {
711        // It is necessary to instantiate the repository data because of locks:
712        // when the content is locked, the lock token is released to be taken by anyone
713        // in repository data there is a lock checked boolean that need to be reset in order to take the token
714        // this boolean is only reset at instantiation
715        RepositoryData repositoryData = new JCRRepositoryData(getNode());
716        Collection<ContentType> contentTypes = _getFactory().getContentHelper().getContentTypes(this);
717        return new DefaultModelAwareDataHolder(repositoryData, contentTypes, Optional.empty(), Optional.of(this));
718    }
719    
720    public ModifiableModelLessDataHolder getInternalDataHolder()
721    {
722        // It is necessary to instantiate the repository data because of locks:
723        // when the content is locked, the lock token is released to be taken by anyone
724        // in repository data there is a lock checked boolean that need to be reset in order to take the token
725        // this boolean is only reset at instantiation
726        ModifiableRepositoryData repositoryData = new JCRRepositoryData(getNode(), RepositoryConstants.NAMESPACE_PREFIX_INTERNAL);
727        Optional<LockableAmetysObject> lockableAmetysObject = Optional.of(this)
728                                                                      .filter(LockableAmetysObject.class::isInstance)
729                                                                      .map(LockableAmetysObject.class:: cast);
730        return new DefaultModifiableModelLessDataHolder(_getFactory().getInternalDataTypesExtensionPoint(), repositoryData, lockableAmetysObject);
731    }
732    
733    public Optional<SystemPropertyExtensionPoint> getSystemPropertyExtensionPoint()
734    {
735        return Optional.of(_getFactory().getSystemPropertyExtensionPoint());
736    }
737    
738    @Override
739    public void addReaction(UserIdentity user, ReactionType reactionType)
740    {
741        ReactionableObjectHelper.addReaction(getUnversionedDataHolder(), user, reactionType);
742    }
743
744    @Override
745    public void removeReaction(UserIdentity user, ReactionType reactionType)
746    {
747        ReactionableObjectHelper.removeReaction(getUnversionedDataHolder(), user, reactionType);
748    }
749
750    @Override
751    public List<UserIdentity> getReactionUsers(ReactionType reactionType)
752    {
753        return ReactionableObjectHelper.getReactionUsers(getUnversionedDataHolder(), reactionType);
754    }
755
756    public void addReport()
757    {
758        ReportableObjectHelper.addReport(getUnversionedDataHolder());
759    }
760    
761    public void setReportsCount(long reportsCount)
762    {
763        ReportableObjectHelper.setReportsCount(getUnversionedDataHolder(), reportsCount);
764    }
765
766    public void clearReports()
767    {
768        ReportableObjectHelper.clearReports(getUnversionedDataHolder());
769    }
770
771    public long getReportsCount()
772    {
773        return ReportableObjectHelper.getReportsCount(getUnversionedDataHolder());
774    }
775    
776    public void toSAX(ContentHandler contentHandler, Locale locale, View view, boolean saxWorkflowStep) throws SAXException
777    {
778        _getFactory().getContentSaxer().saxContent(this, contentHandler, locale, view, "content", saxWorkflowStep, false, false, "attributes");
779    }
780}