001/* 002 * Copyright 2015 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.time.ZonedDateTime; 019import java.util.ArrayList; 020import java.util.Date; 021import java.util.HashMap; 022import java.util.HashSet; 023import java.util.List; 024import java.util.Map; 025import java.util.Set; 026import java.util.concurrent.ExecutionException; 027import java.util.concurrent.Future; 028 029import javax.jcr.Node; 030import javax.jcr.RepositoryException; 031import javax.jcr.Session; 032 033import org.apache.avalon.framework.component.Component; 034import org.apache.avalon.framework.context.Context; 035import org.apache.avalon.framework.context.ContextException; 036import org.apache.avalon.framework.context.Contextualizable; 037import org.apache.avalon.framework.logger.AbstractLogEnabled; 038import org.apache.avalon.framework.service.ServiceException; 039import org.apache.avalon.framework.service.ServiceManager; 040import org.apache.avalon.framework.service.Serviceable; 041import org.apache.cocoon.components.ContextHelper; 042import org.apache.cocoon.environment.Request; 043import org.apache.commons.lang3.StringUtils; 044import org.apache.commons.lang3.tuple.Pair; 045 046import org.ametys.cms.ObservationConstants; 047import org.ametys.cms.content.ContentHelper; 048import org.ametys.cms.content.archive.ArchiveConstants; 049import org.ametys.cms.content.referencetable.HierarchicalReferenceTablesHelper; 050import org.ametys.cms.contenttype.ContentType; 051import org.ametys.cms.contenttype.ContentTypeExtensionPoint; 052import org.ametys.cms.contenttype.ContentTypesHelper; 053import org.ametys.cms.contenttype.MetadataSet; 054import org.ametys.cms.data.ContentDataHelper; 055import org.ametys.cms.data.type.ModelItemTypeConstants; 056import org.ametys.cms.lock.LockContentManager; 057import org.ametys.cms.repository.ReactionableObject.ReactionType; 058import org.ametys.cms.tag.CMSTag; 059import org.ametys.cms.tag.TagProviderExtensionPoint; 060import org.ametys.cms.workflow.AbstractContentWorkflowComponent; 061import org.ametys.cms.workflow.ContentWorkflowHelper; 062import org.ametys.cms.workflow.EditContentFunction; 063import org.ametys.core.observation.Event; 064import org.ametys.core.observation.ObservationManager; 065import org.ametys.core.right.RightManager; 066import org.ametys.core.right.RightManager.RightResult; 067import org.ametys.core.ui.Callable; 068import org.ametys.core.user.CurrentUserProvider; 069import org.ametys.core.user.UserIdentity; 070import org.ametys.core.user.UserManager; 071import org.ametys.core.util.DateUtils; 072import org.ametys.plugins.core.user.UserHelper; 073import org.ametys.plugins.explorer.ExplorerNode; 074import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 075import org.ametys.plugins.explorer.resources.Resource; 076import org.ametys.plugins.explorer.resources.ResourceCollection; 077import org.ametys.plugins.repository.AmetysObject; 078import org.ametys.plugins.repository.AmetysObjectIterable; 079import org.ametys.plugins.repository.AmetysObjectResolver; 080import org.ametys.plugins.repository.AmetysRepositoryException; 081import org.ametys.plugins.repository.CopiableAmetysObject; 082import org.ametys.plugins.repository.ModifiableAmetysObject; 083import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 084import org.ametys.plugins.repository.RemovableAmetysObject; 085import org.ametys.plugins.repository.TraversableAmetysObject; 086import org.ametys.plugins.repository.UnknownAmetysObjectException; 087import org.ametys.plugins.repository.data.holder.ModifiableModelLessDataHolder; 088import org.ametys.plugins.repository.lock.LockAwareAmetysObject; 089import org.ametys.plugins.repository.lock.LockHelper; 090import org.ametys.plugins.repository.lock.LockableAmetysObject; 091import org.ametys.plugins.repository.provider.RequestAttributeWorkspaceSelector; 092import org.ametys.plugins.repository.version.ModifiableDataAwareVersionableAmetysObject; 093import org.ametys.plugins.repository.version.VersionableAmetysObject; 094import org.ametys.plugins.workflow.AbstractWorkflowComponent; 095import org.ametys.plugins.workflow.component.CheckRightsCondition; 096import org.ametys.plugins.workflow.store.AbstractJackrabbitWorkflowStore; 097import org.ametys.plugins.workflow.store.AmetysObjectWorkflowStore; 098import org.ametys.plugins.workflow.support.WorkflowProvider; 099import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow; 100import org.ametys.runtime.authentication.AccessDeniedException; 101 102import com.opensymphony.workflow.WorkflowException; 103import com.opensymphony.workflow.spi.Step; 104 105/** 106 * DAO for manipulating contents 107 * 108 */ 109public class ContentDAO extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 110{ 111 /** Avalon Role */ 112 public static final String ROLE = ContentDAO.class.getName(); 113 114 /** Deletion status */ 115 private static final String __CONTENT_DELETION_STATUS_DELETED = "deleted"; 116 private static final String __CONTENT_DELETION_STATUS_UNDELETED = "undeleted"; 117 private static final String __CONTENT_DELETION_STATUS_REFERENCED = "referenced"; 118 private static final String __CONTENT_DELETION_STATUS_UNAUTHORIZED = "unauthorized"; 119 private static final String __CONTENT_DELETION_STATUS_LOCKED = "locked"; 120 121 /** Ametys resolver */ 122 protected AmetysObjectResolver _resolver; 123 /** Ametys observation manger */ 124 protected ObservationManager _observationManager; 125 /** Component to get current user */ 126 protected CurrentUserProvider _currentUserProvider; 127 /** Component to get tags */ 128 protected TagProviderExtensionPoint _tagProvider; 129 130 /** Workflow component */ 131 protected WorkflowProvider _workflowProvider; 132 /** Workflow helper component */ 133 protected ContentWorkflowHelper _contentWorkflowHelper; 134 /** Component to manager lock */ 135 protected LockContentManager _lockManager; 136 /** Content-type extension point */ 137 protected ContentTypeExtensionPoint _contentTypeEP; 138 /** Content helper */ 139 protected ContentHelper _contentHelper; 140 /** Content types helper */ 141 protected ContentTypesHelper _cTypesHelper; 142 /** Rights manager */ 143 protected RightManager _rightManager; 144 /** Cocoon context */ 145 protected Context _context; 146 /** The user manager */ 147 protected UserManager _usersManager; 148 /** Helper for users */ 149 protected UserHelper _userHelper; 150 /** The helper component for hierarchical simple contents */ 151 protected HierarchicalReferenceTablesHelper _hierarchicalSimpleContentsHelper; 152 /** The modifiable content helper */ 153 protected ModifiableContentHelper _modifiableContentHelper; 154 155 /** The mode for tag edition */ 156 public enum TagMode 157 { 158 /** Value will replace existing one */ 159 REPLACE, 160 /** Value will be added to existing one */ 161 INSERT, 162 /** Value will be removed from existing one */ 163 REMOVE 164 } 165 166 public void contextualize(Context context) throws ContextException 167 { 168 _context = context; 169 } 170 171 @Override 172 public void service(ServiceManager smanager) throws ServiceException 173 { 174 _resolver = (AmetysObjectResolver) smanager.lookup(AmetysObjectResolver.ROLE); 175 _observationManager = (ObservationManager) smanager.lookup(ObservationManager.ROLE); 176 _currentUserProvider = (CurrentUserProvider) smanager.lookup(CurrentUserProvider.ROLE); 177 _usersManager = (UserManager) smanager.lookup(UserManager.ROLE); 178 _userHelper = (UserHelper) smanager.lookup(UserHelper.ROLE); 179 _tagProvider = (TagProviderExtensionPoint) smanager.lookup(TagProviderExtensionPoint.ROLE); 180 _workflowProvider = (WorkflowProvider) smanager.lookup(WorkflowProvider.ROLE); 181 _rightManager = (RightManager) smanager.lookup(RightManager.ROLE); 182 _contentTypeEP = (ContentTypeExtensionPoint) smanager.lookup(ContentTypeExtensionPoint.ROLE); 183 _lockManager = (LockContentManager) smanager.lookup(LockContentManager.ROLE); 184 _contentWorkflowHelper = (ContentWorkflowHelper) smanager.lookup(ContentWorkflowHelper.ROLE); 185 _cTypesHelper = (ContentTypesHelper) smanager.lookup(ContentTypesHelper.ROLE); 186 _contentHelper = (ContentHelper) smanager.lookup(ContentHelper.ROLE); 187 _modifiableContentHelper = (ModifiableContentHelper) smanager.lookup(ModifiableContentHelper.ROLE); 188 _hierarchicalSimpleContentsHelper = (HierarchicalReferenceTablesHelper) smanager.lookup(HierarchicalReferenceTablesHelper.ROLE); 189 } 190 191 /** 192 * Delete contents and force the deletion of invert relations. 193 * @param contentsId The ids of contents to delete 194 * @return the deleted and undeleted contents 195 */ 196 @Callable 197 public Map<String, Object> forceDeleteContents(List<String> contentsId) 198 { 199 Map<String, Object> results = _initializeResultsMap(); 200 201 for (String contentId : contentsId) 202 { 203 Content content = _resolver.resolveById(contentId); 204 Map<String, Object> contentParams = _transformContentToParams(content); 205 206 String contentDeletionStatus = _getContentDeletionStatus(content, "CMS_Rights_DeleteContent"); 207 208 // The content is referenced, remove referencies 209 // Then remove referencies if you can 210 // Then check that the content is not referenced any more (should not happen) 211 if (contentDeletionStatus != null && contentDeletionStatus.equals(__CONTENT_DELETION_STATUS_REFERENCED) && _removeReferences(content) && !_isContentReferenced(content)) 212 { 213 contentDeletionStatus = null; 214 } 215 216 // The content has no constraints 217 if (contentDeletionStatus == null) 218 { 219 contentDeletionStatus = _reallyDeleteContent(content); 220 } 221 222 String key = contentDeletionStatus + "-contents"; 223 @SuppressWarnings("unchecked") 224 List<Map<String, Object>> statusedContents = (List<Map<String, Object>>) results.get(key); 225 statusedContents.add(contentParams); 226 } 227 228 return results; 229 } 230 231 private boolean _removeReferences(Content content) 232 { 233 int actionId = EditContentFunction.INVERT_EDIT_ACTION_ID; 234 235 // Group references by referencer 236 Map<ModifiableDefaultContent, Set<String>> referencesByContent = new HashMap<>(); 237 for (Pair<String, Content> referencingPair : _contentHelper.getReferencingContents(content)) 238 { 239 Set<String> references = referencesByContent.computeIfAbsent((ModifiableDefaultContent) referencingPair.getValue(), __ -> new HashSet<>()); 240 references.add(referencingPair.getKey()); 241 } 242 243 // Control that each referencer has the invert action available 244 for (ModifiableDefaultContent referencingContent : referencesByContent.keySet()) 245 { 246 if (!_contentWorkflowHelper.isAvailableAction(referencingContent, actionId)) 247 { 248 getLogger().warn("The action " + actionId + " is not available for the referencing content " + referencingContent.getId() + ". Impossible to delete the content " + content.getId() + "."); 249 return false; 250 } 251 } 252 253 // Get the references 254 for (ModifiableDefaultContent referencingContent : referencesByContent.keySet()) 255 { 256 // Break the references 257 for (String referencingPath : referencesByContent.get(referencingContent)) 258 { 259 if (referencingContent.isMultiple(referencingPath)) 260 { 261 String[] values = ContentDataHelper.getContentIdsStreamFromMultipleContentData(referencingContent, referencingPath) 262 .filter(id -> !id.equals(content.getId())) 263 .toArray(String[]::new); 264 referencingContent.setValue(referencingPath, values); 265 } 266 else 267 { 268 referencingContent.removeValue(referencingPath); 269 } 270 } 271 272 // edit 273 Map<String, Object> contextParameters = new HashMap<>(); 274 contextParameters.put("quit", true); 275 276 Map<String, Object> inputs = new HashMap<>(); 277 inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters); 278 inputs.put(CheckRightsCondition.FORCE, true); 279 280 try 281 { 282 _contentWorkflowHelper.doAction(referencingContent, EditContentFunction.INVERT_EDIT_ACTION_ID, inputs); 283 } 284 catch (WorkflowException e) 285 { 286 getLogger().error("An error occured while trying to update the workflow of the referencer " + referencingContent.toString(), e); 287 } 288 } 289 290 return true; 291 } 292 293 /** 294 * Delete contents 295 * @param contentsId The ids of contents to delete 296 * @return the deleted and undeleted contents 297 */ 298 @Callable 299 public Map<String, Object> deleteContents(List<String> contentsId) 300 { 301 return deleteContents(contentsId, false); 302 } 303 304 /** 305 * Delete contents 306 * @param contentsId The ids of contents to delete 307 * @param ignoreRights true to ignore user rights 308 * @return the deleted and undeleted contents 309 */ 310 public Map<String, Object> deleteContents(List<String> contentsId, boolean ignoreRights) 311 { 312 return deleteContents(contentsId, ignoreRights ? null : "CMS_Rights_DeleteContent"); 313 } 314 315 /** 316 * Delete contents 317 * @param contentsId The ids of contents to delete 318 * @param deleteRightId The deletion right's id to check. Can be null to ignore rights 319 * @return the deleted and undeleted contents 320 */ 321 public Map<String, Object> deleteContents(List<String> contentsId, String deleteRightId) 322 { 323 Map<String, Object> results = _initializeResultsMap(); 324 325 for (String contentId : contentsId) 326 { 327 Content content = _resolver.resolveById(contentId); 328 Map<String, Object> contentParams = _transformContentToParams(content); 329 330 String contentDeletionStatus = _getContentDeletionStatus(content, deleteRightId); 331 332 // The content has no constraints 333 if (contentDeletionStatus == null) 334 { 335 contentDeletionStatus = _reallyDeleteContent(content); 336 } 337 338 String key = contentDeletionStatus + "-contents"; 339 @SuppressWarnings("unchecked") 340 List<Map<String, Object>> statusedContents = (List<Map<String, Object>>) results.get(key); 341 statusedContents.add(contentParams); 342 } 343 344 return results; 345 } 346 347 private Map<String, Object> _initializeResultsMap() 348 { 349 Map<String, Object> results = new HashMap<>(); 350 351 results.put(__CONTENT_DELETION_STATUS_DELETED + "-contents", new ArrayList<Map<String, Object>>()); 352 results.put(__CONTENT_DELETION_STATUS_UNDELETED + "-contents", new ArrayList<Map<String, Object>>()); 353 results.put(__CONTENT_DELETION_STATUS_REFERENCED + "-contents", new ArrayList<Map<String, Object>>()); 354 results.put(__CONTENT_DELETION_STATUS_UNAUTHORIZED + "-contents", new ArrayList<Map<String, Object>>()); 355 results.put(__CONTENT_DELETION_STATUS_LOCKED + "-contents", new ArrayList<Map<String, Object>>()); 356 357 return results; 358 } 359 360 private String _reallyDeleteContent(Content content) 361 { 362 try 363 { 364 // All checks have been done, the content can be deleted 365 Map<String, Object> eventParams = _getEventParametersForDeletion(content); 366 367 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETING, _currentUserProvider.getUser(), eventParams)); 368 369 RemovableAmetysObject removableContent = (RemovableAmetysObject) content; 370 ModifiableAmetysObject parent = removableContent.getParent(); 371 372 // Remove the content. 373 removableContent.remove(); 374 375 parent.saveChanges(); 376 377 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_DELETED, _currentUserProvider.getUser(), eventParams)); 378 379 return __CONTENT_DELETION_STATUS_DELETED; 380 } 381 catch (AmetysRepositoryException e) 382 { 383 getLogger().error("Unable to delete content '" + content.getId() + "'", e); 384 385 return __CONTENT_DELETION_STATUS_UNDELETED; 386 } 387 } 388 389 private Map<String, Object> _transformContentToParams(Content content) 390 { 391 String contentName = content.getName(); 392 String contentTitle = StringUtils.defaultString(_contentHelper.getTitle(content), contentName); 393 394 Map<String, Object> contentParams = new HashMap<>(); 395 contentParams.put("id", content.getId()); 396 contentParams.put("title", contentTitle); 397 contentParams.put("name", contentName); 398 399 return contentParams; 400 } 401 402 private String _getContentDeletionStatus(Content content, String deleteRightId) 403 { 404 if (!(content instanceof RemovableAmetysObject)) 405 { 406 throw new IllegalArgumentException("The content [" + content.getId() + "] is not a RemovableAmetysObject, it can't be deleted."); 407 } 408 409 if (deleteRightId != null && !canDelete(content, deleteRightId)) 410 { 411 // User has no sufficient right 412 return __CONTENT_DELETION_STATUS_UNAUTHORIZED; 413 } 414 415 if (content instanceof LockableAmetysObject) 416 { 417 // If the content is locked, try to unlock it. 418 LockableAmetysObject lockableContent = (LockableAmetysObject) content; 419 if (lockableContent.isLocked()) 420 { 421 boolean canUnlockAll = _rightManager.hasRight(_currentUserProvider.getUser(), "CMS_Rights_UnlockAll", "/cms") == RightResult.RIGHT_ALLOW; 422 if (LockHelper.isLockOwner(lockableContent, _currentUserProvider.getUser()) || canUnlockAll) 423 { 424 lockableContent.unlock(); 425 } 426 else 427 { 428 return __CONTENT_DELETION_STATUS_LOCKED; 429 } 430 } 431 } 432 433 if (_isContentReferenced(content)) 434 { 435 // Indicate that the content is referenced. 436 return __CONTENT_DELETION_STATUS_REFERENCED; 437 } 438 439 return null; 440 } 441 442 /** 443 * Test if content is still referenced before removing it 444 * @param content The content to remove 445 * @return true if content is still referenced 446 */ 447 protected boolean _isContentReferenced (Content content) 448 { 449 return content.hasReferencingContents(); 450 } 451 452 /** 453 * Get parameters for content deleted {@link Event} 454 * @param content the removed content 455 * @return the event's parameters 456 */ 457 protected Map<String, Object> _getEventParametersForDeletion (Content content) 458 { 459 Map<String, Object> eventParams = new HashMap<>(); 460 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 461 eventParams.put(ObservationConstants.ARGS_CONTENT_NAME, content.getName()); 462 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 463 return eventParams; 464 } 465 466 /** 467 * Get the contents properties 468 * @param contentIds The ids of contents 469 * @param workspaceName The workspace name. Can be null to get contents in current workspace. 470 * @return The contents' properties 471 */ 472 @Callable 473 public Map<String, Object> getContentsProperties (List<String> contentIds, String workspaceName) 474 { 475 Map<String, Object> result = new HashMap<>(); 476 477 List<Map<String, Object>> contents = new ArrayList<>(); 478 List<String> contentsNotFound = new ArrayList<>(); 479 480 Request request = ContextHelper.getRequest(_context); 481 String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 482 try 483 { 484 if (StringUtils.isNotEmpty(workspaceName)) 485 { 486 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName); 487 } 488 489 for (String contentId : contentIds) 490 { 491 try 492 { 493 Content content = _resolver.resolveById(contentId); 494 contents.add(getContentProperties(content)); 495 } 496 catch (UnknownAmetysObjectException e) 497 { 498 contentsNotFound.add(contentId); 499 } 500 } 501 502 result.put("contents", contents); 503 result.put("contentsNotFound", contentsNotFound); 504 } 505 finally 506 { 507 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace); 508 } 509 510 return result; 511 } 512 513 /** 514 * Get the content properties 515 * @param contentId The id of content 516 * @param workspaceName The workspace name. Can be null to get content in current workspace. 517 * @return The content's properties 518 */ 519 @Callable 520 public Map<String, Object> getContentProperties (String contentId, String workspaceName) 521 { 522 Request request = ContextHelper.getRequest(_context); 523 String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 524 try 525 { 526 if (StringUtils.isNotEmpty(workspaceName)) 527 { 528 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName); 529 } 530 531 Content content = _resolver.resolveById(contentId); 532 return getContentProperties(content); 533 } 534 finally 535 { 536 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace); 537 } 538 } 539 540 /** 541 * Get the content properties 542 * @param content The content 543 * @return The content properties 544 */ 545 public Map<String, Object> getContentProperties (Content content) 546 { 547 Map<String, Object> infos = new HashMap<>(); 548 549 infos.put("id", content.getId()); 550 infos.put("name", content.getName()); 551 infos.put("title", _contentHelper.getTitle(content)); 552 553 if (ModelItemTypeConstants.MULTILINGUAL_STRING_ELEMENT_TYPE_ID.equals(content.getType(Content.ATTRIBUTE_TITLE).getId())) 554 { 555 infos.put("titleVariants", _contentHelper.getTitleVariants(content)); 556 } 557 infos.put("path", content.getPath()); 558 infos.put("types", content.getTypes()); 559 infos.put("mixins", content.getMixinTypes()); 560 String lang = content.getLanguage(); 561 if (lang != null) 562 { 563 infos.put("lang", lang); 564 } 565 infos.put("creator", _userHelper.user2json(content.getCreator())); 566 infos.put("lastContributor", _userHelper.user2json(content.getLastContributor())); 567 infos.put("creationDate", DateUtils.dateToString(content.getCreationDate())); 568 infos.put("lastModified", DateUtils.dateToString(content.getLastModified())); 569 infos.put("isSimple", _contentHelper.isSimple(content)); 570 infos.put("isReferenceTable", _contentHelper.isReferenceTable(content)); 571 infos.put("parent", _hierarchicalSimpleContentsHelper.getParent(content)); 572 573 if (content instanceof WorkflowAwareContent) 574 { 575 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 576 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 577 578 infos.put("workflowName", workflow.getWorkflowName(waContent.getWorkflowId())); 579 580 List<Integer> workflowSteps = new ArrayList<>(); 581 582 List<Step> currentSteps = workflow.getCurrentSteps(waContent.getWorkflowId()); 583 for (Step step : currentSteps) 584 { 585 workflowSteps.add(step.getStepId()); 586 } 587 infos.put("workflowSteps", workflowSteps); 588 589 int[] availableActions = _contentWorkflowHelper.getAvailableActions(waContent); 590 infos.put("availableActions", availableActions); 591 } 592 593 if (content instanceof ModifiableContent) 594 { 595 infos.put("isModifiable", true); 596 } 597 598 if (content instanceof LockAwareAmetysObject) 599 { 600 LockAwareAmetysObject lockableContent = (LockAwareAmetysObject) content; 601 if (lockableContent.isLocked()) 602 { 603 infos.put("locked", true); 604 infos.put("lockOwner", _userHelper.user2json(lockableContent.getLockOwner())); 605 infos.put("canUnlock", _lockManager.canUnlock(lockableContent)); 606 } 607 } 608 609 infos.put("rights", getUserRights(content)); 610 611 Map<String, Object> additionalData = new HashMap<>(); 612 613 String[] contenttypes = content.getTypes(); 614 for (String cTypeId : contenttypes) 615 { 616 ContentType cType = _contentTypeEP.getExtension(cTypeId); 617 if (cType != null) 618 { 619 additionalData.putAll(cType.getAdditionalData(content)); 620 } 621 } 622 623 if (!additionalData.isEmpty()) 624 { 625 infos.put("additionalData", additionalData); 626 } 627 628 infos.put("isTaggable", content instanceof TaggableAmetysObject); 629 630 if (content instanceof ModifiableDataAwareVersionableAmetysObject) 631 { 632 ModifiableModelLessDataHolder unversionedDataHolder = ((ModifiableDataAwareVersionableAmetysObject) content).getUnversionedDataHolder(); 633 if (unversionedDataHolder.hasValue(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE)) 634 { 635 ZonedDateTime scheduledDate = unversionedDataHolder.getValue(ArchiveConstants.META_ARCHIVE_SCHEDULED_DATE); 636 infos.put("scheduledArchivingDate", DateUtils.zonedDateTimeToString(scheduledDate)); 637 } 638 } 639 640 if (content instanceof ReportableObject) 641 { 642 infos.put("reportsCount", ((ReportableObject) content).getReportsCount()); 643 } 644 645 return infos; 646 } 647 648 /** 649 * Get the content's properties for description 650 * @param contentId The id of content 651 * @param workspaceName The workspace name. Can be null to get content in current workspace. 652 * @return The content's properties for description 653 */ 654 @Callable 655 public Map<String, Object> getContentDescription (String contentId, String workspaceName) 656 { 657 Request request = ContextHelper.getRequest(_context); 658 String currentWorkspace = RequestAttributeWorkspaceSelector.getForcedWorkspace(request); 659 try 660 { 661 if (StringUtils.isNotEmpty(workspaceName)) 662 { 663 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, workspaceName); 664 } 665 666 Content content = _resolver.resolveById(contentId); 667 return getContentDescription(content); 668 } 669 finally 670 { 671 RequestAttributeWorkspaceSelector.setForcedWorkspace(request, currentWorkspace); 672 } 673 } 674 675 /** 676 *Get the content's properties for description 677 * @param content The content 678 * @return The content's properties for description 679 */ 680 public Map<String, Object> getContentDescription (Content content) 681 { 682 Map<String, Object> infos = new HashMap<>(); 683 684 infos.put("id", content.getId()); 685 infos.put("name", content.getName()); 686 infos.put("title", _contentHelper.getTitle(content)); 687 infos.put("types", content.getTypes()); 688 infos.put("mixins", content.getMixinTypes()); 689 infos.put("lang", content.getLanguage()); 690 infos.put("creator", _userHelper.user2json(content.getCreator())); 691 infos.put("lastContributor", _userHelper.user2json(content.getLastContributor())); 692 infos.put("lastModified", DateUtils.dateToString(content.getLastModified())); 693 infos.put("iconGlyph", _cTypesHelper.getIconGlyph(content)); 694 infos.put("iconDecorator", _cTypesHelper.getIconDecorator(content)); 695 infos.put("smallIcon", _cTypesHelper.getSmallIcon(content)); 696 infos.put("mediumIcon", _cTypesHelper.getMediumIcon(content)); 697 infos.put("largeIcon", _cTypesHelper.getLargeIcon(content)); 698 699 return infos; 700 } 701 702 /** 703 * Get the metadata sets of a content 704 * @param contentId the content's id 705 * @param edition Set to true to get edition metadata set. False otherwise. 706 * @param includeInternal Set to true to include internal metadata sets. 707 * @return the metadata sets 708 */ 709 @Callable 710 public List<Map<String, Object>> getContentMetadataSets (String contentId, boolean edition, boolean includeInternal) 711 { 712 List<Map<String, Object>> metadataSets = new ArrayList<>(); 713 714 Content content = _resolver.resolveById(contentId); 715 String contentTypeId = _cTypesHelper.getContentTypeIdForRendering(content); 716 717 ContentType cType = _contentTypeEP.getExtension(contentTypeId); 718 719 Set<String> metadataSetNames = edition ? cType.getEditionMetadataSetNames(includeInternal) : cType.getViewMetadataSetNames(includeInternal); 720 for (String metadataSetName : metadataSetNames) 721 { 722 MetadataSet metadataSet = edition ? cType.getMetadataSetForEdition(metadataSetName) : cType.getMetadataSetForView(metadataSetName); 723 724 Map<String, Object> viewInfos = new HashMap<>(); 725 viewInfos.put("name", metadataSetName); 726 viewInfos.put("label", metadataSet.getLabel()); 727 viewInfos.put("description", metadataSet.getDescription()); 728 metadataSets.add(viewInfos); 729 } 730 731 return metadataSets; 732 } 733 734 /** 735 * Get the user rights on content 736 * @param content The content 737 * @return The user's rights 738 */ 739 protected Set<String> getUserRights (Content content) 740 { 741 UserIdentity user = _currentUserProvider.getUser(); 742 return _rightManager.getUserRights(user, content); 743 } 744 745 /** 746 * Get the tags of contents 747 * @param contentIds The content's ids 748 * @return the tags 749 */ 750 @Callable 751 public Set<String> getTags (List<String> contentIds) 752 { 753 Set<String> tags = new HashSet<>(); 754 755 for (String contentId : contentIds) 756 { 757 Content content = _resolver.resolveById(contentId); 758 tags.addAll(content.getTags()); 759 } 760 761 return tags; 762 } 763 764 /** 765 * Tag a list of contents with the given tags 766 * @param contentIds The ids of contents to tag 767 * @param tagNames The tags 768 * @param contextualParameters The contextual parameters 769 * @return the result 770 */ 771 @Callable 772 public Map<String, Object> tag (List<String> contentIds, List<String> tagNames, Map<String, Object> contextualParameters) 773 { 774 return tag(contentIds, tagNames, TagMode.REPLACE.toString(), contextualParameters); 775 } 776 777 /** 778 * Tag a list of contents 779 * @param contentIds The ids of contents to tag 780 * @param tagNames The tags 781 * @param mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags. 782 * @param contextualParameters The contextual parameters 783 * @return the result 784 */ 785 @Callable 786 public Map<String, Object> tag (List<String> contentIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters) 787 { 788 Map<String, Object> result = new HashMap<>(); 789 790 result.put("notaggable-contents", new ArrayList<Map<String, Object>>()); 791 result.put("invalid-tags", new ArrayList<String>()); 792 result.put("allright-contents", new ArrayList<Map<String, Object>>()); 793 result.put("locked-contents", new ArrayList<Map<String, Object>>()); 794 795 for (String contentId : contentIds) 796 { 797 Content content = _resolver.resolveById(contentId); 798 799 Map<String, Object> content2json = new HashMap<>(); 800 content2json.put("id", content.getId()); 801 content2json.put("title", _contentHelper.getTitle(content)); 802 803 if (content instanceof TaggableAmetysObject) 804 { 805 TaggableAmetysObject mContent = (TaggableAmetysObject) content; 806 807 boolean wasLocked = false; 808 809 if (content instanceof LockableAmetysObject) 810 { 811 LockableAmetysObject lockableContent = (LockableAmetysObject) content; 812 UserIdentity user = _currentUserProvider.getUser(); 813 if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, user)) 814 { 815 @SuppressWarnings("unchecked") 816 List<Map<String, Object>> lockedContents = (List<Map<String, Object>>) result.get("locked-contents"); 817 content2json.put("lockOwner", lockableContent.getLockOwner()); 818 lockedContents.add(content2json); 819 820 // Stop process 821 continue; 822 } 823 824 if (lockableContent.isLocked()) 825 { 826 wasLocked = true; 827 lockableContent.unlock(); 828 } 829 } 830 831 TagMode tagMode = TagMode.valueOf(mode); 832 833 Set<String> oldTags = mContent.getTags(); 834 _removeAllTagsInReplaceMode(mContent, tagMode, oldTags); 835 836 // Then set new tags 837 for (String tagName : tagNames) 838 { 839 if (_isTagValid(tagName, contextualParameters)) 840 { 841 if (TagMode.REMOVE.equals(tagMode)) 842 { 843 mContent.untag(tagName); 844 } 845 else if (TagMode.REPLACE.equals(tagMode) || !oldTags.contains(tagName)) 846 { 847 mContent.tag(tagName); 848 } 849 850 } 851 else 852 { 853 @SuppressWarnings("unchecked") 854 List<String> invalidTags = (List<String>) result.get("invalid-tags"); 855 invalidTags.add(tagName); 856 } 857 } 858 859 ((ModifiableAmetysObject) content).saveChanges(); 860 861 if (wasLocked) 862 { 863 // Relock content if it was locked before tagging 864 ((LockableAmetysObject) content).lock(); 865 } 866 867 content2json.put("tags", content.getTags()); 868 @SuppressWarnings("unchecked") 869 List<Map<String, Object>> allRightPages = (List<Map<String, Object>>) result.get("allright-contents"); 870 allRightPages.add(content2json); 871 872 if (!oldTags.equals(content.getTags())) 873 { 874 // Notify observers that the content has been tagged 875 Map<String, Object> eventParams = new HashMap<>(); 876 eventParams.put(org.ametys.cms.ObservationConstants.ARGS_CONTENT, content); 877 eventParams.put(org.ametys.cms.ObservationConstants.ARGS_CONTENT_ID, content.getId()); 878 eventParams.put("content.tags", content.getTags()); 879 eventParams.put("content.old.tags", oldTags); 880 _observationManager.notify(new Event(org.ametys.cms.ObservationConstants.EVENT_CONTENT_TAGGED, _currentUserProvider.getUser(), eventParams)); 881 } 882 } 883 else 884 { 885 @SuppressWarnings("unchecked") 886 List<Map<String, Object>> notaggableContents = (List<Map<String, Object>>) result.get("notaggable-contents"); 887 notaggableContents.add(content2json); 888 } 889 } 890 891 return result; 892 } 893 894 private void _removeAllTagsInReplaceMode(TaggableAmetysObject mContent, TagMode tagMode, Set<String> oldTags) 895 { 896 if (TagMode.REPLACE.equals(tagMode)) 897 { 898 // First delete old tags 899 for (String tagName : oldTags) 900 { 901 mContent.untag(tagName); 902 } 903 } 904 } 905 906 /** 907 * Is the tag a content tag 908 * @param tagName The tag name 909 * @param contextualParameters The contextual parameters 910 * @return true if the tag is a valid content tag 911 */ 912 public boolean _isTagValid (String tagName, Map<String, Object> contextualParameters) 913 { 914 CMSTag tag = _tagProvider.getTag(tagName, contextualParameters); 915 return tag.getTarget().getName().equals("CONTENT"); 916 } 917 918 /** 919 * Copy a content. 920 * @param originalContent the original content. 921 * @param parent the object in which to create a content. 922 * @param name the content name. 923 * @param initWorkflowActionId The initial workflow action id 924 * @return the copied content. 925 * @throws AmetysRepositoryException If an error occured 926 */ 927 public ModifiableContent copy(DefaultContent originalContent, ModifiableTraversableAmetysObject parent, String name, int initWorkflowActionId) throws AmetysRepositoryException 928 { 929 return copy(originalContent, parent, name, null, initWorkflowActionId); 930 } 931 932 /** 933 * Copy a content. 934 * @param originalContent the original content. 935 * @param parent the object in which to create a content. 936 * @param name the content name. 937 * @param lang the content language. If null, the content language will be the same of the original content 938 * @param initWorkflowActionId The initial workflow action id 939 * @return the copied content. 940 * @throws AmetysRepositoryException If an error occured 941 */ 942 public ModifiableContent copy(DefaultContent originalContent, ModifiableTraversableAmetysObject parent, String name, String lang, int initWorkflowActionId) throws AmetysRepositoryException 943 { 944 return copy(originalContent, parent, name, lang, initWorkflowActionId, true, false, false); 945 } 946 947 /** 948 * Copy a content. 949 * @param originalContent the original content. 950 * @param parent the object in which to create a content. 951 * @param name the content name. 952 * @param lang the content language. If null, the content language will be the same of the original content 953 * @param initWorkflowActionId The initial workflow action id 954 * @param notifyObservers Set to false to do not fire observer events 955 * @param waitAsyncObservers true to wait for asynchronous observers to complete 956 * @param copyACL true to copy ACL of source content 957 * @return the copied content. 958 * @throws AmetysRepositoryException If an error occured 959 */ 960 public ModifiableContent copy(DefaultContent originalContent, ModifiableTraversableAmetysObject parent, String name, String lang, int initWorkflowActionId, boolean notifyObservers, boolean waitAsyncObservers, boolean copyACL) throws AmetysRepositoryException 961 { 962 try 963 { 964 String originalName = name == null ? originalContent.getName() : name; 965 String contentName = originalName; 966 int index = 2; 967 while (parent.hasChild(contentName)) 968 { 969 contentName = originalName + "-" + (index++); 970 } 971 972 String originalContentType = originalContent.getNode().getPrimaryNodeType().getName(); 973 974 ModifiableContent content = parent.createChild(contentName, originalContentType); 975 976 String targetLanguage = lang == null ? originalContent.getLanguage() : lang; 977 if (targetLanguage != null) 978 { 979 content.setLanguage(targetLanguage); 980 } 981 982 content.setTypes(originalContent.getTypes()); 983 984 _modifiableContentHelper.copyTitle(originalContent, content); 985 986 if (originalContent instanceof WorkflowAwareContent) 987 { 988 WorkflowAwareContent waOriginalContent = (WorkflowAwareContent) originalContent; 989 AmetysObjectWorkflow originalContentWorkflow = _workflowProvider.getAmetysObjectWorkflow(waOriginalContent); 990 String workflowName = originalContentWorkflow.getWorkflowName(waOriginalContent.getWorkflowId()); 991 992 // Initialize new content workflow 993 WorkflowAwareContent waContent = (WorkflowAwareContent) content; 994 AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow(waContent); 995 996 HashMap<String, Object> inputs = new HashMap<>(); 997 // Provide the content key 998 inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, waContent); 999 1000 long workflowId = workflow.initialize(workflowName, initWorkflowActionId, inputs); 1001 waContent.setWorkflowId(workflowId); 1002 1003 // Set the current step ID property 1004 Step currentStep = (Step) workflow.getCurrentSteps(workflowId).iterator().next(); 1005 waContent.setCurrentStepId(currentStep.getStepId()); 1006 1007 Node workflowEntryNode = null; 1008 Node node = waContent.getNode(); 1009 Session session = node.getSession(); 1010 1011 1012 try 1013 { 1014 AbstractJackrabbitWorkflowStore workflowStore = (AbstractJackrabbitWorkflowStore) workflow.getConfiguration().getWorkflowStore(); 1015 1016 if (workflowStore instanceof AmetysObjectWorkflowStore) 1017 { 1018 AmetysObjectWorkflowStore ametysObjectWorkflowStore = (AmetysObjectWorkflowStore) workflowStore; 1019 ametysObjectWorkflowStore.bindAmetysObject(waContent); 1020 } 1021 1022 workflowEntryNode = workflowStore.getEntryNode(session, workflowId); 1023 workflowEntryNode.setProperty("ametys-internal:initialActionId", initWorkflowActionId); 1024 } 1025 catch (RepositoryException e) 1026 { 1027 throw new AmetysRepositoryException("Unable to link the workflow to the content", e); 1028 } 1029 } 1030 1031 // Copy metadata 1032 originalContent.getMetadataHolder().copyTo(content.getMetadataHolder()); 1033 1034 // Copy attachments 1035 _copyAttachments(originalContent, content); 1036 1037 if (copyACL) 1038 { 1039 _copyACL(originalContent, content); 1040 } 1041 1042 if (_currentUserProvider.getUser() != null) 1043 { 1044 content.setCreator(_currentUserProvider.getUser()); 1045 content.setLastModified(new Date()); 1046 content.setCreationDate(new Date()); 1047 } 1048 1049 parent.saveChanges(); 1050 1051 // Create a new version 1052 if (content instanceof VersionableAmetysObject) 1053 { 1054 ((VersionableAmetysObject) content).checkpoint(); 1055 } 1056 1057 if (notifyObservers) 1058 { 1059 _notifyContentCopied(content, waitAsyncObservers); 1060 } 1061 1062 return content; 1063 } 1064 catch (WorkflowException e) 1065 { 1066 throw new AmetysRepositoryException(e); 1067 } 1068 catch (RepositoryException e) 1069 { 1070 throw new AmetysRepositoryException(e); 1071 } 1072 } 1073 1074 /** 1075 * Copy the attachments of a content 1076 * @param srcContent The source content 1077 * @param targetContent The target content 1078 */ 1079 protected void _copyAttachments(Content srcContent, Content targetContent) 1080 { 1081 ResourceCollection srcRootAttachments = srcContent.getRootAttachments(); 1082 ResourceCollection targetRootAttachments = targetContent.getRootAttachments(); 1083 1084 AmetysObjectIterable<AmetysObject> children = srcRootAttachments.getChildren(); 1085 for (AmetysObject child : children) 1086 { 1087 if (child instanceof CopiableAmetysObject) 1088 { 1089 try 1090 { 1091 ((CopiableAmetysObject) child).copyTo((ModifiableTraversableAmetysObject) targetRootAttachments, child.getName()); 1092 } 1093 catch (AmetysRepositoryException e) 1094 { 1095 getLogger().error("Failed to copy attachments at path " + child.getPath() + " from content " + srcContent + " to content " + targetContent, e); 1096 } 1097 } 1098 } 1099 } 1100 1101 /** 1102 * Copy the ACL of a content 1103 * @param srcContent The source content 1104 * @param targetContent The target content 1105 */ 1106 protected void _copyACL(Content srcContent, Content targetContent) 1107 { 1108 if (srcContent instanceof DefaultContent && targetContent instanceof DefaultContent) 1109 { 1110 Node srcNode = ((DefaultContent) srcContent).getNode(); 1111 Node targetNode = ((DefaultContent) targetContent).getNode(); 1112 1113 try 1114 { 1115 String aclNodeName = "ametys-internal:acl"; 1116 if (srcNode.hasNode(aclNodeName)) 1117 { 1118 Node aclNode = srcNode.getNode(aclNodeName); 1119 aclNode.getSession().getWorkspace().copy(aclNode.getPath(), targetNode.getPath() + "/" + aclNodeName); 1120 } 1121 } 1122 catch (RepositoryException e) 1123 { 1124 getLogger().error("Failed to copy ACL from content " + srcContent + " to content " + targetContent, e); 1125 } 1126 } 1127 } 1128 1129 /** 1130 * Notify observers that the content has been created 1131 * @param content The content added 1132 * @param waitAsyncObservers true to wait for asynchonous observers to finish 1133 * @throws WorkflowException If an error occurred 1134 */ 1135 protected void _notifyContentCopied(Content content, boolean waitAsyncObservers) throws WorkflowException 1136 { 1137 Map<String, Object> eventParams = new HashMap<>(); 1138 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 1139 eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId()); 1140 1141 List<Future> futures = _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_ADDED, _currentUserProvider.getUser(), eventParams)); 1142 futures.addAll(_observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_WORKFLOW_CHANGED, _currentUserProvider.getUser(), eventParams))); 1143 1144 if (waitAsyncObservers) 1145 { 1146 // Wait for asynchonous observers to finish 1147 for (Future future : futures) 1148 { 1149 try 1150 { 1151 future.get(); 1152 } 1153 catch (ExecutionException | InterruptedException e) 1154 { 1155 getLogger().error(String.format("Error while waiting for async observer to complete")); 1156 } 1157 } 1158 } 1159 } 1160 1161 /** 1162 * Returns the content's attachments root node 1163 * @param id the content's id 1164 * @return The attachments' root node informations 1165 */ 1166 @Callable 1167 public Map<String, Object> getAttachmentsRootNode (String id) 1168 { 1169 Map<String, Object> result = new HashMap<>(); 1170 1171 Content content = _resolver.resolveById(id); 1172 1173 result.put("title", _contentHelper.getTitle(content)); 1174 result.put("contentId", content.getId()); 1175 1176 TraversableAmetysObject attachments = content.getRootAttachments(); 1177 1178 if (attachments != null) 1179 { 1180 result.put("id", attachments.getId()); 1181 if (attachments instanceof ModifiableAmetysObject) 1182 { 1183 result.put("isModifiable", true); 1184 } 1185 if (attachments instanceof ModifiableResourceCollection) 1186 { 1187 result.put("canCreateChild", true); 1188 } 1189 1190 boolean hasChildNodes = false; 1191 boolean hasResources = false; 1192 1193 for (AmetysObject child : attachments.getChildren()) 1194 { 1195 if (child instanceof Resource) 1196 { 1197 hasResources = true; 1198 } 1199 else if (child instanceof ExplorerNode) 1200 { 1201 hasChildNodes = true; 1202 } 1203 } 1204 1205 if (hasChildNodes) 1206 { 1207 result.put("hasChildNodes", true); 1208 } 1209 1210 if (hasResources) 1211 { 1212 result.put("hasResources", true); 1213 } 1214 1215 return result; 1216 } 1217 1218 throw new IllegalArgumentException("Content with id '" + id + "' does not support attachments."); 1219 } 1220 1221 /** 1222 * Determines if the current user has right to delete the content 1223 * @param content The content 1224 * @return true if current user is authorized to delete the content 1225 */ 1226 public boolean canDelete(Content content) 1227 { 1228 return canDelete(content, "CMS_Rights_DeleteContent"); 1229 } 1230 1231 /** 1232 * Determines if the current user has right to delete the content 1233 * @param content The content 1234 * @param deleteRightId The right's id to check for deletion 1235 * @return true if current user is authorized to delete the content 1236 */ 1237 public boolean canDelete(Content content, String deleteRightId) 1238 { 1239 UserIdentity user = _currentUserProvider.getUser(); 1240 if (_rightManager.hasRight(user, deleteRightId, content) == RightResult.RIGHT_ALLOW) 1241 { 1242 return true; 1243 } 1244 1245 return false; 1246 } 1247 1248 /** 1249 * Add or remove a reaction on a content 1250 * @param contentId The content id 1251 * @param reactionName the reaction name (ex: LIKE) 1252 * @param remove true to remove the reaction, false to add reaction 1253 * @return the result with the current actors of this reaction 1254 */ 1255 @Callable 1256 public Map<String, Object> react(String contentId, String reactionName, boolean remove) 1257 { 1258 Map<String, Object> result = new HashMap<>(); 1259 1260 Content content = _resolver.resolveById(contentId); 1261 1262 if (_rightManager.currentUserHasReadAccess(content)) 1263 { 1264 ReactionType reactionType = ReactionType.valueOf(reactionName); 1265 UserIdentity actor = _currentUserProvider.getUser(); 1266 1267 boolean updated = remove ? unreact(content, actor, reactionType) : react(content, actor, reactionType); 1268 result.put("updated", updated); 1269 result.put("contentId", contentId); 1270 result.put("actors", _userHelper.userIdentities2json(((ReactionableObject) content).getReactionUsers(reactionType))); 1271 } 1272 else 1273 { 1274 result.put("unauthorized", true); 1275 result.put("updated", false); 1276 } 1277 1278 return result; 1279 } 1280 1281 /** 1282 * Add a reaction on a {@link Content}. 1283 * @param content the content 1284 * @param userIdentity the issuer of reaction 1285 * @param reactionType the reaction type 1286 * @return true if a change was made 1287 */ 1288 public boolean react(Content content, UserIdentity userIdentity, ReactionType reactionType) 1289 { 1290 return _addOrRemoveReaction(content, userIdentity, reactionType, false); 1291 } 1292 1293 /** 1294 * Remove reaction if exists on a {@link Content}. 1295 * @param content the content 1296 * @param userIdentity the issuer of reaction 1297 * @param reactionType the reaction type 1298 * @return true if a change was made 1299 */ 1300 public boolean unreact(Content content, UserIdentity userIdentity, ReactionType reactionType) 1301 { 1302 return _addOrRemoveReaction(content, userIdentity, reactionType, true); 1303 } 1304 1305 private boolean _addOrRemoveReaction(Content content, UserIdentity userIdentity, ReactionType reactionType, boolean remove) 1306 { 1307 if (content instanceof ReactionableObject) 1308 { 1309 boolean hasChanges = false; 1310 1311 List<UserIdentity> reactionIssuers = ((ReactionableObject) content).getReactionUsers(reactionType); 1312 if (!remove && !reactionIssuers.contains(userIdentity)) 1313 { 1314 ((ReactionableObject) content).addReaction(userIdentity, reactionType); 1315 hasChanges = true; 1316 } 1317 else if (remove && reactionIssuers.contains(userIdentity)) 1318 { 1319 ((ReactionableObject) content).removeReaction(userIdentity, reactionType); 1320 hasChanges = true; 1321 } 1322 1323 if (hasChanges) 1324 { 1325 ((DefaultContent) content).saveChanges(); 1326 1327 Map<String, Object> eventParams = new HashMap<>(); 1328 eventParams.put(ObservationConstants.ARGS_CONTENT, content); 1329 eventParams.put(ObservationConstants.ARGS_REACTION_TYPE, reactionType); 1330 eventParams.put(ObservationConstants.ARGS_REACTION_ISSUER, userIdentity); 1331 _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_REACTION_CHANGED, userIdentity, eventParams)); 1332 1333 return true; 1334 } 1335 } 1336 1337 return false; 1338 } 1339 1340 /** 1341 * Add a report on a content 1342 * @param content The content 1343 * @throws IllegalArgumentException if the content is not a {@link ReportableObject} 1344 * @throws AccessDeniedException if the current user has not read access on the given content 1345 */ 1346 public void report(Content content) throws IllegalArgumentException, AccessDeniedException 1347 { 1348 if (_rightManager.currentUserHasReadAccess(content)) 1349 { 1350 if (content instanceof ReportableObject) 1351 { 1352 ((ReportableObject) content).addReport(); 1353 ((DefaultContent) content).saveChanges(); 1354 } 1355 else 1356 { 1357 throw new IllegalArgumentException("Unable to report the content '" + content.getId() + "'. Current user is not authorized to see this content."); 1358 } 1359 } 1360 else 1361 { 1362 throw new AccessDeniedException("Unable to report the content '" + content.getId() + "'. Current user is not authorized to see this content."); 1363 } 1364 } 1365}