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