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