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