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