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