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