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