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