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}