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