001/* 002 * Copyright 2016 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.plugins.explorer.resources.actions; 017 018import java.io.ByteArrayInputStream; 019import java.io.IOException; 020import java.io.InputStream; 021import java.io.Reader; 022import java.nio.charset.StandardCharsets; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.Comparator; 027import java.util.Date; 028import java.util.HashMap; 029import java.util.HashSet; 030import java.util.LinkedList; 031import java.util.List; 032import java.util.Map; 033import java.util.Objects; 034import java.util.Set; 035 036import javax.jcr.Node; 037import javax.jcr.RepositoryException; 038import javax.jcr.Session; 039import javax.jcr.lock.Lock; 040import javax.jcr.lock.LockManager; 041 042import org.apache.avalon.framework.component.Component; 043import org.apache.avalon.framework.context.ContextException; 044import org.apache.avalon.framework.context.Contextualizable; 045import org.apache.avalon.framework.service.ServiceException; 046import org.apache.avalon.framework.service.ServiceManager; 047import org.apache.avalon.framework.service.Serviceable; 048import org.apache.cocoon.Constants; 049import org.apache.cocoon.ProcessingException; 050import org.apache.cocoon.environment.Context; 051import org.apache.commons.io.IOUtils; 052import org.apache.commons.io.output.NullOutputStream; 053import org.apache.commons.lang.IllegalClassException; 054import org.apache.commons.lang.StringUtils; 055import org.apache.jackrabbit.util.Text; 056import org.apache.tika.metadata.Metadata; 057 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.util.DateUtils; 066import org.ametys.core.util.I18nUtils; 067import org.ametys.plugins.core.user.UserHelper; 068import org.ametys.plugins.explorer.ExplorerNode; 069import org.ametys.plugins.explorer.ModifiableExplorerNode; 070import org.ametys.plugins.explorer.ObservationConstants; 071import org.ametys.plugins.explorer.cmis.CMISRootResourcesCollection; 072import org.ametys.plugins.explorer.cmis.CMISTreeFactory; 073import org.ametys.plugins.explorer.resources.CommentableResource; 074import org.ametys.plugins.explorer.resources.ModifiableResource; 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.explorer.resources.generators.ResourcesExplorerGenerator; 079import org.ametys.plugins.explorer.resources.jcr.JCRResource; 080import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollectionFactory; 081import org.ametys.plugins.explorer.resources.metadata.TikaProvider; 082import org.ametys.plugins.explorer.resources.metadata.populate.ResourceMetadataPopulator; 083import org.ametys.plugins.explorer.resources.metadata.populate.ResourceMetadataPopulatorExtensionPoint; 084import org.ametys.plugins.explorer.threads.actions.ThreadDAO; 085import org.ametys.plugins.explorer.threads.jcr.JCRPost; 086import org.ametys.plugins.explorer.threads.jcr.JCRPostFactory; 087import org.ametys.plugins.explorer.threads.jcr.JCRThread; 088import org.ametys.plugins.repository.AmetysObject; 089import org.ametys.plugins.repository.AmetysObjectIterable; 090import org.ametys.plugins.repository.AmetysObjectResolver; 091import org.ametys.plugins.repository.AmetysRepositoryException; 092import org.ametys.plugins.repository.ModifiableAmetysObject; 093import org.ametys.plugins.repository.RemovableAmetysObject; 094import org.ametys.plugins.repository.RepositoryConstants; 095import org.ametys.plugins.repository.RepositoryIntegrityViolationException; 096import org.ametys.plugins.repository.TraversableAmetysObject; 097import org.ametys.plugins.repository.UnknownAmetysObjectException; 098import org.ametys.plugins.repository.dublincore.DublinCoreAwareAmetysObject; 099import org.ametys.plugins.repository.jcr.JCRAmetysObject; 100import org.ametys.plugins.repository.jcr.JCRTraversableAmetysObject; 101import org.ametys.plugins.repository.lock.LockHelper; 102import org.ametys.plugins.repository.lock.LockableAmetysObject; 103import org.ametys.plugins.repository.metadata.ModifiableRichText; 104import org.ametys.plugins.repository.version.VersionableAmetysObject; 105import org.ametys.runtime.authentication.AccessDeniedException; 106import org.ametys.runtime.i18n.I18nizableText; 107import org.ametys.runtime.plugin.component.AbstractLogEnabled; 108 109/** 110 * Explorer resources DAO 111 */ 112public class ExplorerResourcesDAO extends AbstractLogEnabled implements Serviceable, Component, Contextualizable 113{ 114 /** Avalon Role */ 115 public static final String ROLE = ExplorerResourcesDAO.class.getName(); 116 117 /** Right id to unlock all resources */ 118 public static final String RIGHTS_RESOURCE_UNLOCK_ALL = "Plugin_Explorer_File_Unlock_All"; 119 120 /** Right id to add a resource */ 121 public static final String RIGHTS_RESOURCE_ADD = "Plugin_Explorer_File_Add"; 122 123 /** Right id to rename a resource */ 124 public static final String RIGHTS_RESOURCE_RENAME = "Plugin_Explorer_File_Rename"; 125 126 /** Right id to delete a resource */ 127 public static final String RIGHTS_RESOURCE_DELETE = "Plugin_Explorer_File_Delete"; 128 129 /** Right id to edit DC metadata of a resource */ 130 public static final String RIGHTS_RESOURCE_EDIT_DC = "Plugin_Explorer_File_Edit_DC_Metadata"; 131 132 /** Right id to comment a resource */ 133 public static final String RIGHTS_RESOURCE_COMMENT = "Plugin_Explorer_File_Comment"; 134 135 /** Right id to moderate comment a resource */ 136 public static final String RIGHTS_RESOURCE_MODERATE_COMMENT = "Plugin_Explorer_File_Moderate_Comments"; 137 138 /** Right id to add CMIS collection */ 139 public static final String RIGHTS_COLLECTION_CMIS_ADD = "Plugin_Explorer_CMIS_Add"; 140 141 /** Right id to add a folder */ 142 public static final String RIGHTS_COLLECTION_ADD = "Plugin_Explorer_Folder_Add"; 143 144 /** Right id to edit a folder */ 145 public static final String RIGHTS_COLLECTION_EDIT = "Plugin_Explorer_Folder_Edit"; 146 147 /** Right id to delete a folder */ 148 public static final String RIGHTS_COLLECTION_DELETE = "Plugin_Explorer_Folder_Delete"; 149 150 /** Ametys resolver */ 151 protected AmetysObjectResolver _resolver; 152 153 /** The rights manager */ 154 protected RightManager _rightManager; 155 156 /** Observer manager. */ 157 protected ObservationManager _observationManager; 158 159 /** The current user provider. */ 160 protected CurrentUserProvider _currentUserProvider; 161 162 /** I18n utils */ 163 protected I18nUtils _i18nUtils; 164 165 /** The avalon context */ 166 protected org.apache.avalon.framework.context.Context _context; 167 168 /** The cocoon context */ 169 protected Context _cocoonContext; 170 171 /** The tika provider. */ 172 protected TikaProvider _tikaProvider; 173 174 /** The metadata populator extension point. */ 175 protected ResourceMetadataPopulatorExtensionPoint _metadataPopulatorEP; 176 177 /** The users manager */ 178 protected UserHelper _userHelper; 179 180 /** The thread DAO */ 181 protected ThreadDAO _threadDAO; 182 183 @Override 184 public void service(ServiceManager manager) throws ServiceException 185 { 186 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 187 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 188 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 189 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 190 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 191 _tikaProvider = (TikaProvider) manager.lookup(TikaProvider.ROLE); 192 _metadataPopulatorEP = (ResourceMetadataPopulatorExtensionPoint) manager.lookup(ResourceMetadataPopulatorExtensionPoint.ROLE); 193 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 194 _threadDAO = (ThreadDAO) manager.lookup(ThreadDAO.ROLE); 195 } 196 197 @Override 198 public void contextualize(org.apache.avalon.framework.context.Context context) throws ContextException 199 { 200 _context = context; 201 _cocoonContext = (Context) context.get(Constants.CONTEXT_ENVIRONMENT_CONTEXT); 202 } 203 204 /** 205 * Get the root nodes for resources 206 * 207 * @return the root nodes 208 */ 209 public List<ExplorerNode> getResourcesRootNodes() 210 { 211 List<ExplorerNode> roots = new ArrayList<>(); 212 roots.add(_resolver.resolveByPath(RepositoryConstants.NAMESPACE_PREFIX + ":resources")); 213 return roots; 214 } 215 216 /** 217 * Retrieves the set of standard data for the explorer root node 218 * 219 * @return The data structured in a map 220 */ 221 @Callable 222 public List<Map<String, Object>> getRootNodesInfo() 223 { 224 List<Map<String, Object>> infos = new ArrayList<>(); 225 226 List<ExplorerNode> rootNodes = getResourcesRootNodes(); 227 for (ExplorerNode rootNode : rootNodes) 228 { 229 infos.add(getDefaultInfoAsRootNode(rootNode)); 230 } 231 return infos; 232 } 233 234 /** 235 * Get the necessary default info for a root node. Can be used to construct 236 * a root node for a tree (client side). 237 * 238 * @param id The root node id 239 * @return A map which contains a set of default info (such as id, 240 * applicationId, path, type etc...) 241 */ 242 @Callable 243 public Map<String, Object> getDefaultInfoAsRootNode(String id) 244 { 245 ExplorerNode node = _resolver.resolveById(id); 246 return getDefaultInfoAsRootNode(node); 247 } 248 249 /** 250 * Get the necessary default info for a root node. Can be used to construct 251 * a root node for a tree (client side). 252 * 253 * @param rootNode The root node 254 * @return A map which contains a set of default info (such as id, 255 * applicationId, path, type etc...) 256 */ 257 public Map<String, Object> getDefaultInfoAsRootNode(ExplorerNode rootNode) 258 { 259 Map<String, Object> result = new HashMap<>(); 260 261 result.put("id", rootNode.getId()); 262 result.put("applicationId", rootNode.getApplicationId()); 263 result.put("name", "resources"); 264 result.put("cls", "root"); 265 result.put("iconCls", "ametysicon-folder249"); 266 267 result.put("text", _i18nUtils.translate(new I18nizableText("plugin.explorer", "PLUGINS_EXPLORER_ROOT_NODE"))); 268 269 result.put("path", "/dummy/resources"); 270 result.put("type", ResourcesExplorerGenerator.RESOURCE_COLLECTION); 271 272 boolean hasResources = false; 273 if (rootNode instanceof ResourceCollection) 274 { 275 hasResources = ((ResourceCollection) rootNode).hasChildResources(); 276 } 277 boolean hasChildNodes = rootNode.hasChildExplorerNodes(); 278 279 if (hasChildNodes) 280 { 281 result.put("hasChildNodes", true); 282 } 283 284 if (hasResources) 285 { 286 result.put("hasResources", true); 287 } 288 289 result.put("isModifiable", false); 290 291 if (rootNode instanceof ModifiableExplorerNode) 292 { 293 result.put("canCreateChild", true); 294 } 295 296 return result; 297 } 298 299 /** 300 * Get the informations on given nodes (resources or collections) 301 * 302 * @param ids The ids of node 303 * @return the nodes information 304 */ 305 @Callable 306 public Map<String, Object> getNodesInfo(List<String> ids) 307 { 308 List<Map<String, Object>> objects = new ArrayList<>(); 309 List<String> objectsNotFound = new ArrayList<>(); 310 311 for (String id : ids) 312 { 313 try 314 { 315 AmetysObject ao = _resolver.resolveById(id); 316 317 if (ao instanceof ExplorerNode) 318 { 319 objects.add(getExplorerNodeProperties((ExplorerNode) ao)); 320 } 321 else if (ao instanceof Resource) 322 { 323 objects.add(getResourceProperties((Resource) ao)); 324 } 325 } 326 catch (UnknownAmetysObjectException e) 327 { 328 objectsNotFound.add(id); 329 } 330 } 331 332 Map<String, Object> result = new HashMap<>(); 333 result.put("objects", objects); 334 result.put("objectsNotFound", objectsNotFound); 335 336 return result; 337 } 338 339 /** 340 * Get the explorer node properties 341 * 342 * @param node The explorer node 343 * @return The properties 344 */ 345 public Map<String, Object> getExplorerNodeProperties(ExplorerNode node) 346 { 347 Map<String, Object> infos = new HashMap<>(); 348 349 ExplorerNode root = node; 350 AmetysObject parent = null; 351 352 while (true) 353 { 354 parent = root.getParent(); 355 if (parent instanceof ExplorerNode) 356 { 357 root = (ExplorerNode) parent; 358 } 359 else 360 { 361 break; 362 } 363 } 364 365 parent = node.getParent(); 366 367 infos.put("rootId", root.getId()); 368 infos.put("rootOwnerType", "explorer"); 369 infos.put("parentId", parent instanceof ExplorerNode ? parent.getId() : null); 370 infos.put("id", node.getId()); 371 infos.put("applicationId", node.getApplicationId()); 372 infos.put("name", node.getName()); 373 infos.put("path", node.getExplorerPath()); 374 infos.put("isModifiable", node instanceof ModifiableAmetysObject); 375 infos.put("canCreateChild", node instanceof ModifiableExplorerNode); 376 377 infos.put("rights", getUserRights(node)); 378 379 return infos; 380 } 381 382 /** 383 * Get the resource properties 384 * 385 * @param resource The resources 386 * @return The properties 387 */ 388 public Map<String, Object> getResourceProperties(Resource resource) 389 { 390 Map<String, Object> infos = new HashMap<>(); 391 392 ResourceCollection parentAO = resource.getParent(); 393 ResourceCollection root = parentAO; 394 while (true) 395 { 396 if (root.getParent() instanceof ResourceCollection) 397 { 398 root = root.getParent(); 399 } 400 else 401 { 402 break; 403 } 404 } 405 infos.put("id", resource.getId()); 406 infos.put("rootId", root.getId()); 407 infos.put("rootOwnerType", "explorer"); 408 infos.put("parentId", parentAO.getId()); 409 infos.put("name", resource.getName()); 410 infos.put("path", resource.getResourcePath()); 411 infos.put("isModifiable", resource instanceof ModifiableAmetysObject); 412 413 infos.put("rights", getUserRights(parentAO)); 414 415 return infos; 416 } 417 418 /** 419 * Get the path of pages which match filter regexp 420 * @param id The id of explorer node to start search 421 * @param value the value to match 422 * @param allowedExtensions The allowed file extensions (lower-case). Can be null or empty to not filter on file extensions 423 * @return the matching paths 424 */ 425 @Callable 426 public List<String> filterResourcesByRegExp(String id, String value, List<String> allowedExtensions) 427 { 428 List<String> matchingPaths = new ArrayList<>(); 429 430 ExplorerNode root = _resolver.resolveById(id); 431 432 String toMatch = org.apache.commons.lang3.StringUtils.stripAccents(value.toLowerCase()).trim(); 433 434 if (root instanceof TraversableAmetysObject) 435 { 436 TraversableAmetysObject traversableObject = (TraversableAmetysObject) root; 437 AmetysObjectIterable< ? extends AmetysObject> children = traversableObject.getChildren(); 438 for (AmetysObject ao : children) 439 { 440 if (ao instanceof Resource) 441 { 442 _getMatchingResource((Resource) ao, toMatch, allowedExtensions, matchingPaths); 443 } 444 else if (ao instanceof ExplorerNode) 445 { 446 _getMatchingExplorerNode ((ExplorerNode) ao, toMatch, allowedExtensions, matchingPaths); 447 } 448 } 449 } 450 451 return matchingPaths; 452 } 453 454 private void _getMatchingExplorerNode(ExplorerNode explorerNode, String value, List<String> allowedExtensions, List<String> matchingPaths) 455 { 456 String title = org.apache.commons.lang3.StringUtils.stripAccents(explorerNode.getName().toLowerCase()); 457 458 if (title.contains(value)) 459 { 460 matchingPaths.add(explorerNode.getExplorerPath()); 461 } 462 463 if (explorerNode instanceof TraversableAmetysObject) 464 { 465 TraversableAmetysObject traversableObject = (TraversableAmetysObject) explorerNode; 466 467 AmetysObjectIterable< ? extends AmetysObject> children = traversableObject.getChildren(); 468 for (AmetysObject ao : children) 469 { 470 if (ao instanceof Resource) 471 { 472 _getMatchingResource((Resource) ao, value, allowedExtensions, matchingPaths); 473 } 474 else if (ao instanceof ExplorerNode) 475 { 476 _getMatchingExplorerNode ((ExplorerNode) ao, value, allowedExtensions, matchingPaths); 477 } 478 } 479 } 480 } 481 482 private void _getMatchingResource(Resource resource, String value, List<String> allowedExtensions, List<String> matchingPaths) 483 { 484 String filename = org.apache.commons.lang3.StringUtils.stripAccents(resource.getName().toLowerCase()); 485 String fileExtension = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1) : ""; 486 if (filename.contains(value) && (allowedExtensions == null || allowedExtensions.size() == 0 || allowedExtensions.contains(fileExtension))) 487 { 488 matchingPaths.add(resource.getResourcePath()); 489 } 490 } 491 492 /** 493 * Get the user rights on the resource collection 494 * 495 * @param node The explorer node 496 * @return The user's rights 497 */ 498 protected Set<String> getUserRights(ExplorerNode node) 499 { 500 return _rightManager.getUserRights(_currentUserProvider.getUser(), node); 501 } 502 503 /** 504 * Check current user right on given explorer node 505 * 506 * @param id The id of the explorer node 507 * @param rightId The if of right to check 508 * @return true if user has right 509 */ 510 @Callable 511 public boolean hasRight(String id, String rightId) 512 { 513 UserIdentity user = _currentUserProvider.getUser(); 514 ExplorerNode node = _resolver.resolveById(id); 515 516 return _rightManager.hasRight(user, rightId, node) == RightResult.RIGHT_ALLOW; 517 } 518 519 /** 520 * Check lock on a Ametys object 521 * 522 * @param ao the Ametys object 523 * @return <code>false</code> if the Ametys object is locked and can not be 524 * edited or deleted 525 */ 526 public boolean checkLock(AmetysObject ao) 527 { 528 if (ao instanceof LockableAmetysObject) 529 { 530 LockableAmetysObject lockableAO = (LockableAmetysObject) ao; 531 if (lockableAO.isLocked()) 532 { 533 if (!LockHelper.isLockOwner(lockableAO, _currentUserProvider.getUser())) 534 { 535 return false; 536 } 537 538 if (ao instanceof JCRAmetysObject) 539 { 540 _addLockToken(((JCRAmetysObject) ao).getNode()); 541 } 542 } 543 544 } 545 546 return true; 547 } 548 549 private void _addLockToken(Node node) 550 { 551 try 552 { 553 if (node.isLocked()) 554 { 555 LockManager lockManager = node.getSession().getWorkspace().getLockManager(); 556 557 Lock lock = lockManager.getLock(node.getPath()); 558 Node lockHolder = lock.getNode(); 559 560 lockManager.addLockToken(lockHolder.getProperty(RepositoryConstants.METADATA_LOCKTOKEN).getString()); 561 } 562 } 563 catch (RepositoryException e) 564 { 565 throw new AmetysRepositoryException("Unable to add lock token", e); 566 } 567 } 568 569 /** 570 * Check the user privilege on object 571 * 572 * @param object the object 573 * @param rightId the right id 574 * @throws IllegalAccessException if the user has no sufficient rights 575 */ 576 public void checkUserRight(AmetysObject object, String rightId) throws IllegalAccessException 577 { 578 ExplorerNode node; 579 if (object instanceof Resource) 580 { 581 node = object.getParent(); 582 } 583 else 584 { 585 node = (ExplorerNode) object; 586 } 587 588 if (_rightManager.hasRight(_currentUserProvider.getUser(), rightId, object) != RightResult.RIGHT_ALLOW) 589 { 590 throw new IllegalAccessException("User '" + _currentUserProvider.getUser() + "' tried to access a privilege feature without convenient right [" + rightId 591 + ", /resources" + node.getExplorerPath() + "]"); 592 } 593 } 594 595 /** 596 * Retrieve the rights for the user 597 * 598 * @param user The user 599 * @param right The right 600 * @param object The object 601 * @return True if the user has the right 602 */ 603 public boolean getUserRight(UserIdentity user, String right, AmetysObject object) 604 { 605 AmetysObject explorerNode = object; 606 while (explorerNode != null && !(explorerNode instanceof ExplorerNode)) 607 { 608 explorerNode = explorerNode.getParent(); 609 } 610 if (explorerNode == null) 611 { 612 return false; 613 } 614 return _rightManager.hasRight(user, right, explorerNode) == RightResult.RIGHT_ALLOW; 615 } 616 617 /** 618 * Add a resource collection 619 * 620 * @param parentId The identifier of the parent in which the resource 621 * collection will be added 622 * @param desiredName The desired name for the resource collection 623 * @param renameIfExists If false, in case of existing name the resource 624 * collection will not be created, if true it will be created and 625 * renamed 626 * @return The result map with id, parentId, name and message keys 627 */ 628 @Callable 629 public Map<String, Object> addResourceCollection(String parentId, String desiredName, Boolean renameIfExists) 630 { 631 Map<String, Object> result = new HashMap<>(); 632 633 assert parentId != null; 634 635 AmetysObject object = _resolver.resolveById(parentId); 636 if (!(object instanceof ModifiableResourceCollection)) 637 { 638 throw new IllegalClassException(ModifiableResourceCollection.class, object.getClass()); 639 } 640 641 List<String> errors = new LinkedList<>(); 642 ResourceCollection rc = addResourceCollection((ModifiableResourceCollection) object, desiredName, renameIfExists, errors); 643 644 if (!errors.isEmpty()) 645 { 646 result.put("message", errors.get(0)); 647 } 648 else 649 { 650 result.put("id", rc.getId()); 651 result.put("parentID", parentId); 652 result.put("name", rc.getName()); 653 } 654 655 return result; 656 } 657 658 /** 659 * Add a resource collection 660 * 661 * @param parent The parent collection in which the resource collection will 662 * be added 663 * @param desiredName The desired name for the resource collection 664 * @param renameIfExists If false, in case of existing name the resource 665 * collection will not be created, if true it will be created and 666 * renamed 667 * @param errors An optional list of possible error messages in case the 668 * creation failed. Possible values are: locked, already-exist. 669 * @return The created resource collection or null if creation failed 670 */ 671 public ResourceCollection addResourceCollection(ModifiableResourceCollection parent, String desiredName, Boolean renameIfExists, List<String> errors) 672 { 673 String originalName = desiredName; 674 675 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COLLECTION_ADD, parent) != RightResult.RIGHT_ALLOW) 676 { 677 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to add folder without convenient right [" + RIGHTS_COLLECTION_ADD + "]"); 678 } 679 680 if (!checkLock(parent)) 681 { 682 getLogger().warn("User '{}' try to modify collection '{}' but it is locked by another user", _currentUserProvider.getUser(), parent.getName()); 683 if (errors != null) 684 { 685 errors.add("locked"); 686 } 687 return null; 688 } 689 690 if (!renameIfExists && parent.hasChild(originalName)) 691 { 692 getLogger().warn("The object '{}' can not be renamed in '{}' : a object of same name already exists.", parent.getName(), originalName); 693 if (errors != null) 694 { 695 errors.add("already-exist"); 696 } 697 return null; 698 } 699 700 int index = 2; 701 String name = originalName; 702 while (parent.hasChild(name)) 703 { 704 name = originalName + " (" + index + ")"; 705 index++; 706 } 707 708 ResourceCollection child = parent.createChild(name, getResourceCollectionType()); 709 parent.saveChanges(); 710 711 // Notify listeners 712 Map<String, Object> eventParams = new HashMap<>(); 713 eventParams.put(ObservationConstants.ARGS_ID, child.getId()); 714 eventParams.put(ObservationConstants.ARGS_PARENT_ID, parent.getId()); 715 eventParams.put(ObservationConstants.ARGS_NAME, child.getName()); 716 eventParams.put(ObservationConstants.ARGS_PATH, child.getPath()); 717 718 _observationManager.notify(new Event(ObservationConstants.EVENT_COLLECTION_CREATED, _currentUserProvider.getUser(), eventParams)); 719 720 return child; 721 } 722 723 /** 724 * Get the type of child resource collection 725 * 726 * @return the type of child resource collection 727 */ 728 public String getResourceCollectionType() 729 { 730 return JCRResourcesCollectionFactory.RESOURCESCOLLECTION_NODETYPE; 731 } 732 733 /** 734 * Rename a resource, or resource collection 735 * 736 * @param id The id of the object to rename 737 * @param name The desired name to set to the object. 738 * @return The result map with id, name and message keys 739 * @throws RepositoryException If there is a repository error 740 */ 741 @Callable 742 public Map<String, Object> renameObject(String id, String name) throws RepositoryException 743 { 744 Map<String, Object> result = new HashMap<>(); 745 746 JCRAmetysObject object = _resolver.resolveById(id); 747 List<String> errors = new LinkedList<>(); 748 JCRAmetysObject newObject = renameObject(object, name, errors); 749 750 if (!errors.isEmpty()) 751 { 752 String error = errors.get(0); 753 result.put("message", error); 754 } 755 else 756 { 757 result.put("id", newObject.getId()); 758 result.put("name", name); 759 } 760 761 return result; 762 } 763 764 /** 765 * Rename a resource, or resource collection 766 * 767 * @param object The object to rename 768 * @param name The desired name to set to the object. 769 * @param errors An optional list of possible error messages in case the 770 * operation failed. Possible values are: locked, already-exist. 771 * @return The new object 772 * @throws RepositoryException If there is a repository error 773 */ 774 public JCRAmetysObject renameObject(JCRAmetysObject object, String name, List<String> errors) throws RepositoryException 775 { 776 assert object != null; 777 assert name != null; 778 779 String legalName = Text.escapeIllegalJcrChars(name); 780 781 // Check node is not the root node 782 if ("ametys-internal:resources".equals(object.getName())) 783 { 784 throw new IllegalStateException("The resources root node can not be renamed !"); 785 } 786 787 String rightId = object instanceof Resource ? RIGHTS_RESOURCE_RENAME : RIGHTS_COLLECTION_EDIT; 788 if (_rightManager.hasRight(_currentUserProvider.getUser(), rightId, object) != RightResult.RIGHT_ALLOW) 789 { 790 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to rename folder or file without convenient right [" + rightId + "]"); 791 } 792 793 Node node = object.getNode(); 794 String oldName = object.getName(); 795 String oldObjectPath = object.getPath(); 796 797 if (!checkLock(object)) 798 { 799 getLogger().warn("User '{}' is trying to rename object '{}' but it is locked by another user", _currentUserProvider.getUser(), object.getName()); 800 if (errors != null) 801 { 802 errors.add("locked"); 803 } 804 return null; 805 } 806 else if (node.getParent().hasNode(legalName)) 807 { 808 getLogger().warn("The object '{}' cannot be renamed in '{}' : an object with the same name already exists.", object.getName(), name); 809 if (errors != null) 810 { 811 errors.add("already-exist"); 812 } 813 return null; 814 } 815 else 816 { 817 String oldResourcePath = object instanceof Resource ? ((Resource) object).getResourcePath() : ((ResourceCollection) object).getExplorerPath(); 818 819 node.getSession().move(node.getPath(), node.getParent().getPath() + '/' + legalName); 820 node.getSession().save(); 821 822 // Resolve the new ametys object 823 JCRAmetysObject newObject = (JCRAmetysObject) _resolver.resolve(node, false); 824 825 // Notify listeners 826 Map<String, Object> eventParams = new HashMap<>(); 827 eventParams.put(ObservationConstants.ARGS_ID, newObject.getId()); 828 eventParams.put(ObservationConstants.ARGS_PARENT_ID, newObject.getParent().getId()); 829 eventParams.put(ObservationConstants.ARGS_NAME, newObject.getName()); 830 eventParams.put(ObservationConstants.ARGS_PATH, newObject.getPath()); 831 eventParams.put("object.old.name", oldName); 832 eventParams.put("object.old.path", oldObjectPath); 833 834 if (object instanceof Resource) 835 { 836 eventParams.put(ObservationConstants.ARGS_RESOURCE_PATH, ((Resource) object).getResourcePath()); 837 eventParams.put("resource.old.path", oldResourcePath); 838 } 839 else 840 { 841 eventParams.put(ObservationConstants.ARGS_EXPLORER_PATH, ((ResourceCollection) object).getExplorerPath()); 842 eventParams.put("explorer.old.path", oldResourcePath); 843 } 844 845 _observationManager.notify(new Event(object instanceof JCRResource ? ObservationConstants.EVENT_RESOURCE_RENAMED : ObservationConstants.EVENT_COLLECTION_RENAMED, 846 _currentUserProvider.getUser(), eventParams)); 847 848 return newObject; 849 } 850 } 851 852 /** 853 * Delete an object 854 * 855 * @param ids The list of identifiers for the objects to delete 856 * @return The result map with a message key in case of an error 857 */ 858 @Callable 859 public Map<String, Object> deleteObject(List<String> ids) 860 { 861 assert ids != null; 862 863 Map<String, Object> result = new HashMap<>(); 864 865 for (String id : ids) 866 { 867 RemovableAmetysObject object = (RemovableAmetysObject) _resolver.resolveById(id); 868 List<String> errors = new LinkedList<>(); 869 deleteObject(object, errors); 870 871 if (!errors.isEmpty()) 872 { 873 String error = errors.get(0); 874 result.put("message", error); 875 result.put("success", false); 876 return result; 877 } 878 } 879 880 result.put("success", true); 881 return result; 882 } 883 884 /** 885 * Delete an object 886 * 887 * @param object The object to delete 888 * @param errors An optional list of possible error messages in case the 889 * creation failed. Possible values are: locked. 890 * @return the parent id of the removed object 891 */ 892 public String deleteObject(RemovableAmetysObject object, List<String> errors) 893 { 894 // Check node is not the root node 895 if ("ametys-internal:resources".equals(object.getName())) 896 { 897 throw new IllegalStateException("The resources root node can not be deleted !"); 898 } 899 900 String rightId = object instanceof Resource ? RIGHTS_RESOURCE_DELETE : RIGHTS_COLLECTION_DELETE; 901 if (_rightManager.hasRight(_currentUserProvider.getUser(), rightId, object) != RightResult.RIGHT_ALLOW) 902 { 903 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to delete file or folder without convenient right [" + rightId + "]"); 904 } 905 906 if (!checkLock(object)) 907 { 908 getLogger().warn("User '{}' is trying to delete object '{}' but it is locked by another user", _currentUserProvider.getUser(), object.getName()); 909 if (errors != null) 910 { 911 errors.add("locked"); 912 } 913 return null; 914 } 915 916 ModifiableResourceCollection parent = object.getParent(); 917 String eventType = object instanceof Resource ? ObservationConstants.EVENT_RESOURCE_DELETED : ObservationConstants.EVENT_COLLECTION_DELETED; 918 String parentId = parent.getId(); 919 920 Map<String, Object> eventParams = new HashMap<>(); 921 eventParams.put(ObservationConstants.ARGS_PARENT_ID, parentId); 922 eventParams.put(ObservationConstants.ARGS_ID, object.getId()); 923 eventParams.put(ObservationConstants.ARGS_NAME, object.getName()); 924 eventParams.put(ObservationConstants.ARGS_PATH, object.getPath()); 925 926 if (object instanceof Resource) 927 { 928 eventParams.put(ObservationConstants.ARGS_RESOURCE_PATH, ((Resource) object).getResourcePath()); 929 } 930 else 931 { 932 eventParams.put(ObservationConstants.ARGS_EXPLORER_PATH, ((ResourceCollection) object).getExplorerPath()); 933 _observationManager.notify(new Event(ObservationConstants.EVENT_COLLECTION_DELETING, _currentUserProvider.getUser(), eventParams)); 934 } 935 936 object.remove(); 937 parent.saveChanges(); 938 939 _observationManager.notify(new Event(eventType, _currentUserProvider.getUser(), eventParams)); 940 941 return parentId; 942 } 943 944 /** 945 * Rename a resource 946 * 947 * @param id The id of the resource to rename 948 * @param name The desired name to set to the resource. 949 * @return The result map with id, name and message keys 950 * @throws RepositoryException If there is a repository error 951 */ 952 @Callable 953 public Map<String, Object> renameResource(String id, String name) throws RepositoryException 954 { 955 Map<String, Object> result = new HashMap<>(); 956 957 JCRResource resource = _resolver.resolveById(id); 958 List<String> errors = new LinkedList<>(); 959 960 Resource newResource = renameResource(resource, name, errors); 961 962 if (!errors.isEmpty()) 963 { 964 String error = errors.get(0); 965 result.put("message", error); 966 } 967 else 968 { 969 result.put("id", newResource.getId()); 970 result.put("name", name); 971 } 972 973 return result; 974 } 975 976 /** 977 * Rename a resource 978 * 979 * @param resource The resource to rename 980 * @param name The desired name to set to the resource. 981 * @param errors An optional list of possible error messages in case the 982 * operation failed. Possible values are: locked, already-exist. 983 * @return The new resource 984 * @throws RepositoryException If there is a repository error 985 */ 986 public JCRResource renameResource(JCRResource resource, String name, List<String> errors) throws RepositoryException 987 { 988 assert resource != null; 989 assert name != null; 990 991 String legalName = Text.escapeIllegalJcrChars(name); 992 993 // FIXME API getNode should use the new rename method 994 String oldName = resource.getName(); 995 String oldPath = resource.getPath(); 996 String oldResourcePath = resource.getResourcePath(); 997 998 Node node = resource.getNode(); 999 1000 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_RESOURCE_RENAME, resource) != RightResult.RIGHT_ALLOW) 1001 { 1002 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to rename file without convenient right [" + RIGHTS_RESOURCE_RENAME + "]"); 1003 } 1004 1005 // Check lock on resource 1006 if (!checkLock(resource)) 1007 { 1008 getLogger().warn("User '{}' is trying to rename resource '{}' but it is locked by another user", _currentUserProvider.getUser(), resource.getName()); 1009 if (errors != null) 1010 { 1011 errors.add("locked"); 1012 } 1013 return null; 1014 } 1015 1016 if (node.getParent().hasNode(legalName)) 1017 { 1018 getLogger().warn("The resource '{}' cannot be renamed in '{}' : an object with the same name already exists.", resource.getName(), name); 1019 if (errors != null) 1020 { 1021 errors.add("already-exist"); 1022 } 1023 return null; 1024 } 1025 1026 String mimeType = _cocoonContext.getMimeType(legalName.toLowerCase()); 1027 mimeType = mimeType == null ? "application/unknown" : mimeType; 1028 resource.setMimeType(mimeType); 1029 1030 node.getSession().move(node.getPath(), node.getParent().getPath() + '/' + legalName); 1031 1032 node.getSession().save(); 1033 1034 // Resolve the new ametys object 1035 JCRResource newObject = (JCRResource) _resolver.resolve(node, false); 1036 1037 // Notify listeners 1038 Map<String, Object> eventParams = new HashMap<>(); 1039 1040 eventParams.put(ObservationConstants.ARGS_ID, newObject.getId()); 1041 eventParams.put(ObservationConstants.ARGS_PARENT_ID, newObject.getParent().getId()); 1042 eventParams.put(ObservationConstants.ARGS_NAME, newObject.getName()); 1043 eventParams.put(ObservationConstants.ARGS_PATH, newObject.getPath()); 1044 eventParams.put("object.old.name", oldName); 1045 eventParams.put("object.old.path", oldPath); 1046 1047 eventParams.put(ObservationConstants.ARGS_RESOURCE_PATH, newObject.getResourcePath()); 1048 eventParams.put("resource.old.path", oldResourcePath); 1049 1050 _observationManager.notify(new Event(ObservationConstants.EVENT_RESOURCE_RENAMED, _currentUserProvider.getUser(), eventParams)); 1051 1052 return newObject; 1053 } 1054 1055 /** 1056 * Copy file resources 1057 * 1058 * @param ids The list of identifiers for the resources to copy 1059 * @param target The id of target to copy into 1060 * @return The result map with a message key in case of an error or with the 1061 * list of uncopied/copied resources 1062 * @throws RepositoryException If there is a repository error 1063 */ 1064 @Callable 1065 public Map<String, Object> copyResource(List<String> ids, String target) throws RepositoryException 1066 { 1067 assert ids != null; 1068 assert target != null; 1069 1070 AmetysObject object = _resolver.resolveById(target); 1071 if (!(object instanceof ModifiableResourceCollection)) 1072 { 1073 throw new IllegalClassException(ModifiableResourceCollection.class, object.getClass()); 1074 } 1075 1076 return copyResource(ids, (ModifiableResourceCollection) object); 1077 } 1078 1079 /** 1080 * Copy file resources 1081 * 1082 * @param ids The list of identifiers for the resources to copy 1083 * @param target The target to copy into 1084 * @return The result map with a message key in case of an error or with the 1085 * list of uncopied/copied resources 1086 * @throws RepositoryException If there is a repository error 1087 */ 1088 public Map<String, Object> copyResource(List<String> ids, ModifiableResourceCollection target) throws RepositoryException 1089 { 1090 Map<String, Object> result = new HashMap<>(); 1091 List<String> uncopiedResources = new ArrayList<>(); 1092 List<String> copiedResourceIds = new ArrayList<>(); 1093 1094 assert ids != null; 1095 assert target != null; 1096 1097 if (!checkLock(target)) 1098 { 1099 getLogger().warn("User '{}' try to copy objet to '{}' but it is locked by another user", _currentUserProvider.getUser(), target.getName()); 1100 result.put("message", "locked"); 1101 return result; 1102 } 1103 1104 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_RESOURCE_ADD, target) != RightResult.RIGHT_ALLOW) 1105 { 1106 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to copy file without convenient right [" + RIGHTS_RESOURCE_ADD + "]"); 1107 } 1108 1109 for (String id : ids) 1110 { 1111 Resource resourceToCopy = _resolver.resolveById(id); 1112 String fileName = resourceToCopy.getName(); 1113 1114 if (target.hasChild(fileName)) 1115 { 1116 getLogger().warn("The resource '" + fileName + "' can not be copied : an object of same name already exists in the target collection"); 1117 result.put("message", "already-exist"); 1118 uncopiedResources.add(fileName); 1119 } 1120 else 1121 { 1122 ModifiableResource resource = createResource(target, fileName); 1123 1124 try (InputStream is = resourceToCopy.getInputStream()) 1125 { 1126 updateResource(resource, is, fileName); 1127 } 1128 catch (IOException e) 1129 { 1130 getLogger().warn("An error occurred while closing the ressource " + resource.getId(), e); 1131 } 1132 1133 copiedResourceIds.add(resource.getId()); 1134 } 1135 } 1136 1137 target.saveChanges(); 1138 1139 for (String id : copiedResourceIds) 1140 { 1141 ModifiableResource resource = _resolver.resolveById(id); 1142 1143 // Create version 1144 checkpoint(resource); 1145 1146 // Notify listeners 1147 Map<String, Object> eventParams = new HashMap<>(); 1148 Map<String, Resource> addedResource = new HashMap<>(); 1149 addedResource.put(resource.getId(), resource); 1150 eventParams.put(ObservationConstants.ARGS_RESOURCES, addedResource); 1151 eventParams.put(ObservationConstants.ARGS_PARENT_ID, target.getId()); 1152 eventParams.put(ObservationConstants.ARGS_PARENT_PATH, target.getPath()); 1153 1154 _observationManager.notify(new Event(ObservationConstants.EVENT_RESOURCE_CREATED, _currentUserProvider.getUser(), eventParams)); 1155 } 1156 1157 if (uncopiedResources.size() > 0) 1158 { 1159 result.put("uncopied-resources", uncopiedResources); 1160 } 1161 1162 if (copiedResourceIds.size() > 0) 1163 { 1164 result.put("copied-resources", copiedResourceIds); 1165 } 1166 1167 return result; 1168 } 1169 1170 /** 1171 * Move objects 1172 * 1173 * @param ids The list of identifiers for the objects to move 1174 * @param targetId The id of target to move into 1175 * @return The result map with a message key in case of an error or with the 1176 * list of unmoved/moved objects 1177 * @throws RepositoryException If there is a repository error 1178 */ 1179 @Callable 1180 public Map<String, Object> moveObject(List<String> ids, String targetId) throws RepositoryException 1181 { 1182 assert ids != null; 1183 assert targetId != null; 1184 1185 AmetysObject target = _resolver.resolveById(targetId); 1186 if (!(target instanceof JCRTraversableAmetysObject)) 1187 { 1188 throw new IllegalClassException(JCRTraversableAmetysObject.class, target.getClass()); 1189 } 1190 1191 return moveObject(ids, (JCRTraversableAmetysObject) target); 1192 } 1193 1194 /** 1195 * Move objects 1196 * 1197 * @param ids The list of identifiers for the objects to move 1198 * @param targetNode The target to move into 1199 * @return The result map with a message key in case of an error or with the 1200 * list of unmoved/moved objects 1201 * @throws RepositoryException If there is a repository error 1202 */ 1203 public Map<String, Object> moveObject(List<String> ids, JCRTraversableAmetysObject targetNode) throws RepositoryException 1204 { 1205 Map<String, Object> result = new HashMap<>(); 1206 List<String> unmovedObjects = new ArrayList<>(); 1207 List<String> movedObjectIds = new ArrayList<>(); 1208 1209 assert ids != null; 1210 assert targetNode != null; 1211 1212 if (!checkLock(targetNode)) 1213 { 1214 getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to move objet to '" + targetNode.getName() + "' but it is locked by another user"); 1215 result.put("message", "locked"); 1216 return result; 1217 } 1218 1219 for (String id : ids) 1220 { 1221 JCRAmetysObject object = (JCRAmetysObject) _resolver.resolveById(id); 1222 1223 if (targetNode.hasChild(object.getName())) 1224 { 1225 getLogger().warn("The object '" + object.getName() + "' can not be moved : a object of same name already exists in the target collection"); 1226 result.put("message", "already-exist"); 1227 unmovedObjects.add(object.getName()); 1228 } 1229 else 1230 { 1231 String oldResourcePath = object instanceof Resource ? ((Resource) object).getResourcePath() : ((ExplorerNode) object).getExplorerPath(); 1232 1233 Map<String, Object> eventParams = new HashMap<>(); 1234 eventParams.put("object.old.path", object.getPath()); 1235 1236 Session session = object.getNode().getSession(); 1237 session.move(object.getNode().getPath(), targetNode.getNode().getPath() + "/" + object.getNode().getName()); 1238 1239 if (object instanceof Resource) 1240 { 1241 eventParams.put(ObservationConstants.ARGS_RESOURCE_PATH, ((Resource) object).getResourcePath()); 1242 eventParams.put("resource.old.path", oldResourcePath); 1243 } 1244 else 1245 { 1246 eventParams.put("explorer.old.path", oldResourcePath); 1247 } 1248 1249 eventParams.put(ObservationConstants.ARGS_PATH, ((ExplorerNode) targetNode).getExplorerPath() + "/" + object.getName()); 1250 eventParams.put(ObservationConstants.ARGS_PARENT_ID, ((ExplorerNode) targetNode).getId()); 1251 eventParams.put(ObservationConstants.ARGS_ID, id); 1252 eventParams.put(ObservationConstants.ARGS_NAME, object.getName()); 1253 1254 session.save(); 1255 movedObjectIds.add(object.getId()); 1256 1257 if (object instanceof Resource) 1258 { 1259 eventParams.put(ObservationConstants.ARGS_RESOURCE_PATH, ((Resource) object).getResourcePath()); 1260 eventParams.put("resource.old.path", oldResourcePath); 1261 _observationManager.notify(new Event(ObservationConstants.EVENT_RESOURCE_MOVED, _currentUserProvider.getUser(), eventParams)); 1262 } 1263 else if (object instanceof ResourceCollection) 1264 { 1265 _observationManager.notify(new Event(ObservationConstants.EVENT_COLLECTION_MOVED, _currentUserProvider.getUser(), eventParams)); 1266 } 1267 else if (object instanceof JCRThread) 1268 { 1269 _observationManager.notify(new Event(ObservationConstants.EVENT_THREAD_MOVED, _currentUserProvider.getUser(), eventParams)); 1270 } 1271 else 1272 { 1273 getLogger().warn("Object " + object.getId() + " of class '" + object.getClass().getName() + "' was moved. This type is unknown."); 1274 } 1275 } 1276 } 1277 1278 if (!unmovedObjects.isEmpty()) 1279 { 1280 result.put("unmoved-objects", unmovedObjects); 1281 } 1282 1283 if (!movedObjectIds.isEmpty()) 1284 { 1285 result.put("moved-objects", movedObjectIds); 1286 } 1287 1288 return result; 1289 } 1290 1291 /** 1292 * Get the history versions of a resource 1293 * 1294 * @param id the id of resource 1295 * @return The versions 1296 * @throws RepositoryException if an error occurred 1297 */ 1298 @Callable 1299 public List<Map<String, Object>> resourceHistory(String id) throws RepositoryException 1300 { 1301 JCRResource resource = _resolver.resolveById(id); 1302 1303 List<Map<String, Object>> versions = new ArrayList<>(); 1304 1305 List<VersionInformation> versionsInfo = new ArrayList<>(); 1306 1307 for (String revision : resource.getAllRevisions()) 1308 { 1309 VersionInformation versionInformation = new VersionInformation(revision, resource.getRevisionTimestamp(revision)); 1310 1311 for (String label : resource.getLabels(revision)) 1312 { 1313 versionInformation.addLabel(label); 1314 } 1315 1316 versionsInfo.add(versionInformation); 1317 } 1318 1319 // Sort by date descendant 1320 Collections.sort(versionsInfo, new Comparator<VersionInformation>() 1321 { 1322 public int compare(VersionInformation o1, VersionInformation o2) 1323 { 1324 try 1325 { 1326 return -o1.getCreatedAt().compareTo(o2.getCreatedAt()); 1327 } 1328 catch (RepositoryException e) 1329 { 1330 throw new RuntimeException("Unable to retrieve a creation date", e); 1331 } 1332 } 1333 }); 1334 1335 for (VersionInformation versionInformation : versionsInfo) 1336 { 1337 Map<String, Object> version = _version2json(resource, versionInformation); 1338 versions.add(version); 1339 } 1340 1341 return versions; 1342 } 1343 1344 /** 1345 * Get the JSON representation of a version of a resource 1346 * @param resource the resource 1347 * @param versionInformation the version information 1348 * @return the version as JSON object 1349 * @throws RepositoryException if failed to get revision 1350 */ 1351 protected Map<String, Object> _version2json(JCRResource resource, VersionInformation versionInformation) throws RepositoryException 1352 { 1353 Map<String, Object> version = new HashMap<>(); 1354 1355 for (String label : versionInformation.getLabels()) 1356 { 1357 version.put(label, label); 1358 } 1359 1360 version.put("rawName", versionInformation.getVersionRawName()); 1361 version.put("name", versionInformation.getVersionName()); 1362 version.put("createdAt", DateUtils.dateToString(versionInformation.getCreatedAt())); 1363 1364 try 1365 { 1366 resource.switchToRevision(versionInformation.getVersionRawName()); 1367 UserIdentity author = resource.getLastContributor(); 1368 version.put("author", _userHelper.user2json(author)); 1369 } 1370 catch (AmetysRepositoryException e) 1371 { 1372 // Do nothing. Can append with old version history (before 4.0) 1373 } 1374 1375 return version; 1376 } 1377 1378 /** 1379 * This action restores an old version of a {@link Resource}. 1380 * 1381 * @param id the id of resource 1382 * @param versionName the name of version to restore 1383 */ 1384 @Callable 1385 public void restoreResource(String id, String versionName) 1386 { 1387 JCRResource resource = _resolver.resolveById(id); 1388 resource.restoreFromRevision(versionName); 1389 1390 resource.setLastContributor(_currentUserProvider.getUser()); 1391 resource.setLastModified(new Date()); 1392 resource.saveChanges(); 1393 resource.checkpoint(); 1394 } 1395 1396 /** 1397 * Determines if a resource with given name already exists 1398 * 1399 * @param parentId the id of parent collection 1400 * @param name the name of resource 1401 * @return true if a resource with same name exists 1402 */ 1403 @Callable 1404 public boolean resourceExists(String parentId, String name) 1405 { 1406 return resourceExists((TraversableAmetysObject) _resolver.resolveById(parentId), name); 1407 } 1408 1409 /** 1410 * Determines if a resource with given name already exists 1411 * 1412 * @param parent the parent collection 1413 * @param name the name of resource 1414 * @return true if a resource with same name exists 1415 */ 1416 public boolean resourceExists(TraversableAmetysObject parent, String name) 1417 { 1418 return parent.hasChild(name); 1419 } 1420 1421 /** 1422 * Set the Dublin Core metadata of a {@link Resource}. 1423 * 1424 * @param resourceId the id of resource 1425 * @param values the DC values 1426 * @throws ProcessingException if an error occurred 1427 */ 1428 @Callable 1429 public void setDCMetadata(String resourceId, Map<String, Object> values) throws ProcessingException 1430 { 1431 setDCMetadata((ModifiableResource) _resolver.resolveById(resourceId), values); 1432 } 1433 1434 /** 1435 * Set the Dublin Core metadata of a {@link Resource}. 1436 * 1437 * @param resource the resource 1438 * @param values the DC values 1439 */ 1440 public void setDCMetadata(ModifiableResource resource, Map<String, Object> values) 1441 { 1442 String title = (String) values.get("dc_title"); 1443 String creator = (String) values.get("dc_creator"); 1444 Object subject = values.get("dc_subject"); 1445 @SuppressWarnings("unchecked") 1446 List<String> subjects = subject instanceof List ? (List<String>) subject : Collections.emptyList(); 1447 String description = (String) values.get("dc_description"); 1448 String publisher = (String) values.get("dc_publisher"); 1449 String contributor = (String) values.get("dc_contributor"); 1450 String dateStr = (String) values.get("dc_date"); 1451 String type = (String) values.get("dc_type"); 1452 String source = (String) values.get("dc_source"); 1453 String language = (String) values.get("dc_language"); 1454 String relation = (String) values.get("dc_relation"); 1455 String coverage = (String) values.get("dc_coverage"); 1456 String rights = (String) values.get("dc_rights"); 1457 1458 try 1459 { 1460 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_RESOURCE_EDIT_DC, resource) != RightResult.RIGHT_ALLOW) 1461 { 1462 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to edit file DC without convenient right [" + RIGHTS_RESOURCE_EDIT_DC + "]"); 1463 } 1464 1465 _updateTitleIfNeeded(resource, title); 1466 _updateCreatorIfNeeded(resource, creator); 1467 _updateDateIfNeeded(resource, dateStr); 1468 _updateDCSubjectIfNeeded(resource, subjects); 1469 _updateDescriptionIfNeeded(resource, description); 1470 _updatePublisherIfNeeded(resource, publisher); 1471 _updateContributorIfNeeded(resource, contributor); 1472 _updateTypeIfNeeded(resource, type); 1473 _updateSourceIfNeeded(resource, source); 1474 _updateLanguageIfNeeded(resource, language); 1475 _updateRelationIfNeeded(resource, relation); 1476 _updateCoverageIfNeeded(resource, coverage); 1477 _updateRightsIfNeeded(resource, rights); 1478 1479 _saveChangesIfNeeded(resource); 1480 1481 } 1482 catch (AmetysRepositoryException e) 1483 { 1484 String errorMsg = String.format("Exception while trying to set Dublin Core metadata on resource %s", resource.getId()); 1485 getLogger().error(errorMsg, e); 1486 throw new AmetysRepositoryException(errorMsg, e); 1487 } 1488 } 1489 1490 private void _updateTitleIfNeeded(ModifiableResource resource, String title) 1491 { 1492 if (!Objects.equals(title, resource.getDCTitle())) 1493 { 1494 resource.setDCTitle(title); 1495 } 1496 } 1497 1498 private void _updateCreatorIfNeeded(ModifiableResource resource, String creator) 1499 { 1500 if (!Objects.equals(creator, resource.getDCCreator())) 1501 { 1502 resource.setDCCreator(creator); 1503 } 1504 } 1505 1506 private void _updateDateIfNeeded(ModifiableResource resource, String dateStr) 1507 { 1508 Date date = null; 1509 if (StringUtils.isNotEmpty(dateStr)) 1510 { 1511 date = DateUtils.parse(dateStr); 1512 } 1513 1514 if (!Objects.equals(date, resource.getDCDate())) 1515 { 1516 resource.setDCDate(date); 1517 } 1518 } 1519 1520 private void _updateRightsIfNeeded(ModifiableResource resource, String rights) 1521 { 1522 if (!Objects.equals(rights, resource.getDCRights())) 1523 { 1524 resource.setDCRights(rights); 1525 } 1526 } 1527 1528 private void _updateCoverageIfNeeded(ModifiableResource resource, String coverage) 1529 { 1530 if (!Objects.equals(coverage, resource.getDCCoverage())) 1531 { 1532 resource.setDCCoverage(coverage); 1533 } 1534 } 1535 1536 private void _updateRelationIfNeeded(ModifiableResource resource, String relation) 1537 { 1538 if (!Objects.equals(relation, resource.getDCRelation())) 1539 { 1540 resource.setDCRelation(relation); 1541 } 1542 } 1543 1544 private void _updateLanguageIfNeeded(ModifiableResource resource, String language) 1545 { 1546 if (!Objects.equals(language, resource.getDCLanguage())) 1547 { 1548 resource.setDCLanguage(language); 1549 } 1550 } 1551 1552 private void _updateSourceIfNeeded(ModifiableResource resource, String source) 1553 { 1554 if (!Objects.equals(source, resource.getDCSource())) 1555 { 1556 resource.setDCSource(source); 1557 } 1558 } 1559 1560 private void _updateTypeIfNeeded(ModifiableResource resource, String type) 1561 { 1562 if (!Objects.equals(type, resource.getDCType())) 1563 { 1564 resource.setDCType(type); 1565 } 1566 } 1567 1568 private void _updateContributorIfNeeded(ModifiableResource resource, String contributor) 1569 { 1570 if (!Objects.equals(contributor, resource.getDCContributor())) 1571 { 1572 resource.setDCContributor(contributor); 1573 } 1574 } 1575 1576 private void _updatePublisherIfNeeded(ModifiableResource resource, String publisher) 1577 { 1578 if (!Objects.equals(publisher, resource.getDCPublisher())) 1579 { 1580 resource.setDCPublisher(publisher); 1581 } 1582 } 1583 1584 private void _updateDescriptionIfNeeded(ModifiableResource resource, String description) 1585 { 1586 if (!Objects.equals(description, resource.getDCDescription())) 1587 { 1588 resource.setDCDescription(description); 1589 } 1590 } 1591 1592 private void _updateDCSubjectIfNeeded(ModifiableResource resource, List<String> subjects) 1593 { 1594 String[] trimSubject = subjects.stream().map(StringUtils::trim).toArray(String[]::new); 1595 if (!Objects.deepEquals(trimSubject, resource.getDCSubject())) 1596 { 1597 resource.setDCSubject(trimSubject); 1598 } 1599 } 1600 1601 private void _saveChangesIfNeeded(ModifiableResource resource) 1602 { 1603 if (resource.needsSave()) 1604 { 1605 resource.setLastContributor(_currentUserProvider.getUser()); 1606 resource.setLastModified(new Date()); 1607 1608 ModifiableResourceCollection parent = resource.getParent(); 1609 parent.saveChanges(); 1610 1611 if (resource instanceof VersionableAmetysObject) 1612 { 1613 // Create first version 1614 ((VersionableAmetysObject) resource).checkpoint(); 1615 } 1616 1617 // Notify listeners of resource update. 1618 Map<String, Object> eventParams = new HashMap<>(); 1619 eventParams.put(ObservationConstants.ARGS_ID, resource.getId()); 1620 eventParams.put(ObservationConstants.ARGS_PARENT_ID, parent.getId()); 1621 eventParams.put(ObservationConstants.ARGS_NAME, resource.getName()); 1622 eventParams.put(ObservationConstants.ARGS_PATH, resource.getPath()); 1623 1624 _observationManager.notify(new Event(ObservationConstants.EVENT_RESOURCE_UPDATED, _currentUserProvider.getUser(), eventParams)); 1625 } 1626 } 1627 1628 /** 1629 * Get the DublinCore metadata of a {@link DublinCoreAwareAmetysObject} 1630 * 1631 * @param id the id of resource 1632 * @return the DC metadata 1633 */ 1634 @Callable 1635 public Map<String, Object> getDCMetadata(String id) 1636 { 1637 AmetysObject object = _resolver.resolveById(id); 1638 if (object instanceof DublinCoreAwareAmetysObject) 1639 { 1640 DublinCoreAwareAmetysObject dcObject = (DublinCoreAwareAmetysObject) object; 1641 1642 Map<String, Object> metadata = new HashMap<>(); 1643 1644 metadata.put("id", id); 1645 1646 Map<String, Object> values = new HashMap<>(); 1647 1648 values.put("dc_title", dcObject.getDCTitle()); 1649 values.put("dc_creator", dcObject.getDCCreator()); 1650 values.put("dc_subject", dcObject.getDCSubject()); 1651 values.put("dc_description", dcObject.getDCDescription()); 1652 values.put("dc_type", dcObject.getDCType()); 1653 values.put("dc_publisher", dcObject.getDCPublisher()); 1654 values.put("dc_contributor", dcObject.getDCContributor()); 1655 values.put("dc_date", DateUtils.dateToString(dcObject.getDCDate())); 1656 values.put("dc_format", dcObject.getDCFormat()); 1657 values.put("dc_identifier", dcObject.getDCIdentifier()); 1658 values.put("dc_source", dcObject.getDCSource()); 1659 values.put("dc_language", dcObject.getDCLanguage()); 1660 values.put("dc_relation", dcObject.getDCRelation()); 1661 values.put("dc_coverage", dcObject.getDCCoverage()); 1662 values.put("dc_rights", dcObject.getDCRights()); 1663 1664 metadata.put("values", values); 1665 return metadata; 1666 } 1667 else 1668 { 1669 throw new IllegalArgumentException("Object of id " + id + " is not Dublin Core aware."); 1670 } 1671 } 1672 1673 /** 1674 * Creates a new CMIS folder (see {@link CMISRootResourcesCollection}) 1675 * 1676 * @param parentId the id of parent folder 1677 * @param originalName the original name if CMIS folder 1678 * @param url The url of CMIS repository 1679 * @param login The user's login to access CMIS repository 1680 * @param password The user's password to access CMIS repository 1681 * @param repoId The id of CMIS repository 1682 * @param renameIfExists true to automatically renamed CMIS folder if 1683 * requested name already exists 1684 * @return the result map with id of created node 1685 */ 1686 @Callable 1687 public Map<String, Object> addCMISCollection(String parentId, String originalName, String url, String login, String password, String repoId, boolean renameIfExists) 1688 { 1689 Map<String, Object> result = new HashMap<>(); 1690 1691 AmetysObject object = _resolver.resolveById(parentId); 1692 if (!(object instanceof ModifiableResourceCollection)) 1693 { 1694 throw new IllegalClassException(ModifiableResourceCollection.class, object.getClass()); 1695 } 1696 1697 ModifiableResourceCollection collection = (ModifiableResourceCollection) object; 1698 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COLLECTION_CMIS_ADD, collection) != RightResult.RIGHT_ALLOW) 1699 { 1700 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to add CMIS collection without convenient right [" + RIGHTS_COLLECTION_CMIS_ADD + "]"); 1701 } 1702 1703 if (!checkLock(collection)) 1704 { 1705 getLogger().warn("User '" + _currentUserProvider.getUser() + "' try to modify collection '" + object.getName() + "' but it is locked by another user"); 1706 result.put("message", "locked"); 1707 return result; 1708 } 1709 1710 if (!renameIfExists && collection.hasChild(originalName)) 1711 { 1712 getLogger().warn("The object '" + object.getName() + "' can not be renamed in '" + originalName + "' : a object of same name already exists."); 1713 result.put("message", "already-exist"); 1714 return result; 1715 } 1716 1717 int index = 1; 1718 String name = originalName; 1719 while (collection.hasChild(name)) 1720 { 1721 name = originalName + " (" + index + ")"; 1722 index++; 1723 } 1724 1725 CMISRootResourcesCollection child = collection.createChild(name, CMISTreeFactory.CMIS_ROOT_COLLECTION_NODETYPE); 1726 1727 child.setRepositoryUrl(url); 1728 child.setRepositoryId(repoId); 1729 child.setUser(login); 1730 child.setPassword(password); 1731 1732 collection.saveChanges(); 1733 1734 result.put("id", child.getId()); 1735 result.put("parentID", parentId); 1736 result.put("name", name); 1737 1738 // Notify listeners 1739 Map<String, Object> eventParams = new HashMap<>(); 1740 eventParams.put(ObservationConstants.ARGS_ID, child.getId()); 1741 eventParams.put(ObservationConstants.ARGS_PARENT_ID, object.getId()); 1742 eventParams.put(ObservationConstants.ARGS_NAME, child.getName()); 1743 eventParams.put(ObservationConstants.ARGS_PATH, child.getPath()); 1744 1745 _observationManager.notify(new Event(ObservationConstants.EVENT_COLLECTION_CREATED, _currentUserProvider.getUser(), eventParams)); 1746 1747 return result; 1748 } 1749 1750 /** 1751 * Edits a CMIS folder (see {@link CMISRootResourcesCollection}) 1752 * 1753 * @param id the id of CMIS folder 1754 * @param url The url of CMIS repository 1755 * @param login The user's login to access CMIS repository 1756 * @param password The user's password to access CMIS repository 1757 * @param repoId The id of CMIS repository 1758 * @return the result map with id of edited node 1759 */ 1760 @Callable 1761 public Map<String, Object> editCMISCollection(String id, String url, String login, String password, String repoId) 1762 { 1763 Map<String, Object> result = new HashMap<>(); 1764 1765 CMISRootResourcesCollection object = _resolver.resolveById(id); 1766 1767 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COLLECTION_CMIS_ADD, object) != RightResult.RIGHT_ALLOW) 1768 { 1769 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to edit CMIS collection without convenient right [" + RIGHTS_COLLECTION_CMIS_ADD + "]"); 1770 } 1771 1772 object.setRepositoryUrl(url); 1773 object.setRepositoryId(repoId); 1774 object.setUser(login); 1775 object.setPassword(password); 1776 1777 object.saveChanges(); 1778 1779 // Notify listeners 1780 Map<String, Object> eventParams = new HashMap<>(); 1781 eventParams.put(ObservationConstants.ARGS_ID, object.getId()); 1782 1783 _observationManager.notify(new Event(ObservationConstants.EVENT_CMIS_COLLECTION_UPDATED, _currentUserProvider.getUser(), eventParams)); 1784 1785 result.put("id", object.getId()); 1786 1787 return result; 1788 } 1789 1790 /** 1791 * Determines if a object is a {@link CMISRootResourcesCollection} 1792 * 1793 * @param id The id of object 1794 * @return true if it is a CMIS root folder 1795 */ 1796 @Callable 1797 public boolean isCMISCollection(String id) 1798 { 1799 ResourceCollection collection = _resolver.resolveById(id); 1800 return collection instanceof CMISRootResourcesCollection; 1801 } 1802 1803 /** 1804 * Get the CMIS properties of collection 1805 * 1806 * @param id The id of CMIS collection 1807 * @return the CMIS properties to access CMIS repository 1808 */ 1809 @Callable 1810 public Map<String, String> getCMISProperties(String id) 1811 { 1812 Map<String, String> properties = new HashMap<>(); 1813 1814 CMISRootResourcesCollection cmis = _resolver.resolveById(id); 1815 1816 properties.put("id", cmis.getId()); 1817 properties.put("name", cmis.getName()); 1818 properties.put("url", cmis.getRepositoryUrl()); 1819 properties.put("login", cmis.getUser()); 1820 properties.put("password", cmis.getPassword()); 1821 properties.put("repoId", cmis.getRepositoryId()); 1822 1823 return properties; 1824 } 1825 1826 /** 1827 * Updates resource input stream and metadata 1828 * 1829 * @param resource The resource 1830 * @param is The resource input stream 1831 * @param fileName The file name 1832 */ 1833 public void updateResource(ModifiableResource resource, InputStream is, String fileName) 1834 { 1835 UserIdentity author = _currentUserProvider.getUser(); 1836 1837 String mimeType = _cocoonContext.getMimeType(fileName.toLowerCase()); 1838 mimeType = mimeType == null ? "application/unknown" : mimeType; 1839 1840 resource.setData(is, mimeType, new Date(), author); 1841 resource.setLastModified(new Date()); 1842 1843 extractResourceMetadata(resource, mimeType); 1844 } 1845 1846 /** 1847 * Extract the resource's metadata and populate the object accordingly. 1848 * 1849 * @param resource the resource to populate. 1850 * @param mimeType the resource MIME type. 1851 */ 1852 public void extractResourceMetadata(ModifiableResource resource, String mimeType) 1853 { 1854 try (InputStream is = resource.getInputStream()) 1855 { 1856 Metadata metadata = new Metadata(); 1857 1858 try (Reader reader = _tikaProvider.getTika().parse(is, metadata)) 1859 { 1860 IOUtils.copy(reader, NullOutputStream.NULL_OUTPUT_STREAM, StandardCharsets.UTF_8); 1861 1862 Collection<ResourceMetadataPopulator> populators = _metadataPopulatorEP.getPopulators(mimeType); 1863 for (ResourceMetadataPopulator populator : populators) 1864 { 1865 populator.populate(resource, metadata); 1866 } 1867 } 1868 } 1869 catch (Exception e) 1870 { 1871 getLogger().error("Error populating the metadata of resource " + resource.getId(), e); 1872 } 1873 } 1874 1875 /** 1876 * Creates a new version 1877 * 1878 * @param resource the resource 1879 */ 1880 public void checkpoint(ModifiableResource resource) 1881 { 1882 if (resource instanceof VersionableAmetysObject) 1883 { 1884 // Create first version 1885 ((VersionableAmetysObject) resource).checkpoint(); 1886 } 1887 } 1888 1889 /** 1890 * Creates a {@link ModifiableResource} under current 1891 * {@link ModifiableResourceCollection}. 1892 * 1893 * @param collection the parent {@link ModifiableResourceCollection} 1894 * @param name the name of the child resource 1895 * @return the new resource created. 1896 * @throws AmetysRepositoryException if an error occurs. 1897 * @throws RepositoryIntegrityViolationException if an object with the same 1898 * name already exists and same name siblings is not allowed. 1899 */ 1900 public ModifiableResource createResource(ModifiableResourceCollection collection, String name) 1901 { 1902 return collection.createChild(name, collection.getResourceType()); 1903 } 1904 1905 // -------------------------------- COMMENTS ------------------------------- 1906 /** 1907 * Add a comment to a resource 1908 * 1909 * @param resourceId The id of the resource 1910 * @param comment The comment 1911 * @return the result 1912 */ 1913 @Callable 1914 public Map<String, Object> addComment(String resourceId, String comment) 1915 { 1916 Map<String, Object> result = new HashMap<>(); 1917 1918 AmetysObject ao = _resolver.resolveById(resourceId); 1919 UserIdentity currentUser = _currentUserProvider.getUser(); 1920 1921 if (!(ao instanceof CommentableResource)) 1922 { 1923 throw new IllegalClassException(CommentableResource.class, ao.getClass()); 1924 } 1925 1926 if (!checkLock(ao)) 1927 { 1928 getLogger().warn("User '{}' is trying to add a comment on resource '{}' but it is locked by another user", currentUser, resourceId); 1929 result.put("message", "locked"); 1930 return result; 1931 } 1932 1933 // Check comment rights 1934 CommentableResource resource = (CommentableResource) ao; 1935 1936 if (!_canAddComment(currentUser, resource)) 1937 { 1938 getLogger().warn("User '{}' is trying to add a comment on resource '{}' without the convient right.", currentUser, resourceId); 1939 result.put("message", "rights"); 1940 return result; 1941 } 1942 1943 org.ametys.plugins.explorer.threads.Thread comments = resource.getComments(true); 1944 1945 if (!(comments instanceof JCRThread)) 1946 { 1947 throw new IllegalClassException(JCRThread.class, comments.getClass()); 1948 } 1949 1950 JCRThread jcrComments = (JCRThread) comments; 1951 1952 JCRPost post = jcrComments.createChild(JCRPostFactory.POST_NODENAME, JCRPostFactory.POST_NODETYPE); 1953 1954 _setComment(post, comment); 1955 1956 post.setAuthor(currentUser); 1957 Date now = new Date(); 1958 post.setCreationDate(now); 1959 post.setLastModified(now); 1960 1961 jcrComments.markAsRead(currentUser); 1962 jcrComments.saveChanges(); 1963 1964 // Notify listeners 1965 Map<String, Object> eventParams = new HashMap<>(); 1966 eventParams.put(ObservationConstants.ARGS_ID, resourceId); 1967 eventParams.put(ObservationConstants.ARGS_POST, post); 1968 _observationManager.notify(new Event(ObservationConstants.EVENT_RESOURCE_COMMENTED, currentUser, eventParams)); 1969 1970 result.put("id", resourceId); 1971 result.put("commentId", post.getId()); 1972 1973 return result; 1974 } 1975 1976 /** 1977 * Indicates if an user can add a comment of a given resource 1978 * 1979 * @param user An user identity 1980 * @param resource The resource to comment 1981 * @return True if the user can add a comment 1982 */ 1983 protected boolean _canAddComment(UserIdentity user, CommentableResource resource) 1984 { 1985 ExplorerNode node = resource.getParent(); 1986 return _rightManager.hasRight(user, "Plugin_Explorer_File_Comment", node) == RightResult.RIGHT_ALLOW 1987 || _rightManager.hasRight(user, "Plugin_Explorer_File_Moderate_Comments", node) == RightResult.RIGHT_ALLOW; 1988 } 1989 1990 /** 1991 * Edit the comment of a resource 1992 * 1993 * @param resourceId The id of the resource 1994 * @param commentId The id of comment to edit 1995 * @param comment The comment 1996 * @return the result 1997 */ 1998 @Callable 1999 public Map<String, Object> editComment(String resourceId, String commentId, String comment) 2000 { 2001 Map<String, Object> result = new HashMap<>(); 2002 2003 AmetysObject ao = _resolver.resolveById(resourceId); 2004 UserIdentity currentUser = _currentUserProvider.getUser(); 2005 2006 if (!(ao instanceof CommentableResource)) 2007 { 2008 throw new IllegalClassException(CommentableResource.class, ao.getClass()); 2009 } 2010 2011 if (!checkLock(ao)) 2012 { 2013 getLogger().warn("User '{}' is trying to edit comment '{}' on resource '{}' but it is locked by another user", currentUser, commentId, resourceId); 2014 result.put("message", "locked"); 2015 return result; 2016 } 2017 2018 CommentableResource resource = (CommentableResource) ao; 2019 JCRPost post = _resolver.resolveById(commentId); 2020 2021 // Check comment rights 2022 if (!_canEditComment(currentUser, resource, post)) 2023 { 2024 getLogger().warn("User '{}' is trying to add a comment on resource '{}' without the convient right.", currentUser, resourceId); 2025 result.put("message", "rights"); 2026 return result; 2027 } 2028 2029 _setComment(post, comment); 2030 post.setLastModified(new Date()); 2031 post.saveChanges(); 2032 2033 result.put("id", resourceId); 2034 result.put("commentId", commentId); 2035 2036 return result; 2037 } 2038 2039 /** 2040 * Indicates if an user can edit a comment of a given resource 2041 * 2042 * @param user An user identity 2043 * @param resource The resource 2044 * @param comment The comment to edit 2045 * @return True if the user can edit the comment 2046 */ 2047 protected boolean _canEditComment(UserIdentity user, CommentableResource resource, JCRPost comment) 2048 { 2049 ExplorerNode node = resource.getParent(); 2050 // Comment right + owner OR Moderate right 2051 return _rightManager.hasRight(user, "Plugin_Explorer_File_Comment", node) == RightResult.RIGHT_ALLOW && comment.getAuthor().equals(user) 2052 || _rightManager.hasRight(user, "Plugin_Explorer_File_Moderate_Comments", node) == RightResult.RIGHT_ALLOW; 2053 } 2054 2055 /** 2056 * Update the content of a comment 2057 * 2058 * @param comment The comment to update 2059 * @param content The content as string 2060 */ 2061 protected void _setComment(JCRPost comment, String content) 2062 { 2063 try 2064 { 2065 ModifiableRichText richText = comment.getContent(); 2066 2067 richText.setMimeType("text/plain"); 2068 richText.setLastModified(new Date()); 2069 richText.setInputStream(new ByteArrayInputStream(content.getBytes("UTF-8"))); 2070 } 2071 catch (IOException e) 2072 { 2073 throw new AmetysRepositoryException("Failed to set post rich text", e); 2074 } 2075 } 2076 2077 /** 2078 * Get the content of a comment as a String 2079 * 2080 * @param post the post 2081 * @return The content as String 2082 * @throws AmetysRepositoryException if failed to parse comment 2083 */ 2084 protected String _getComment(JCRPost post) throws AmetysRepositoryException 2085 { 2086 try 2087 { 2088 ModifiableRichText richText = post.getContent(); 2089 return IOUtils.toString(richText.getInputStream(), "UTF-8"); 2090 } 2091 catch (IOException e) 2092 { 2093 throw new AmetysRepositoryException("Failed to get post rich text", e); 2094 } 2095 } 2096 2097 /** 2098 * Get the content of a comment to edit as a String 2099 * 2100 * @param post the post 2101 * @return The content as String 2102 * @throws AmetysRepositoryException if failed to parse comment 2103 */ 2104 protected String _getCommentForEditing(JCRPost post) throws AmetysRepositoryException 2105 { 2106 return _getComment(post); 2107 } 2108 2109 /** 2110 * Delete the comment of a resource 2111 * 2112 * @param resourceId The id of the resource 2113 * @param commentId The id of comment to delete 2114 * @return the result 2115 */ 2116 @Callable 2117 public Map<String, Object> deleteComment(String resourceId, String commentId) 2118 { 2119 Map<String, Object> result = new HashMap<>(); 2120 2121 AmetysObject ao = _resolver.resolveById(resourceId); 2122 UserIdentity currentUser = _currentUserProvider.getUser(); 2123 2124 if (!(ao instanceof CommentableResource)) 2125 { 2126 throw new IllegalClassException(CommentableResource.class, ao.getClass()); 2127 } 2128 2129 if (!checkLock(ao)) 2130 { 2131 getLogger().warn("User '{}' is trying to delete comment '{}' on resource '{}' but it is locked by another user", currentUser, commentId, resourceId); 2132 result.put("message", "locked"); 2133 return result; 2134 } 2135 2136 CommentableResource resource = (CommentableResource) ao; 2137 JCRPost post = _resolver.resolveById(commentId); 2138 2139 // Check comment rights 2140 if (!_canDeleteComment(currentUser, resource, post)) 2141 { 2142 getLogger().warn("User '{}' is trying to add a comment on resource '{}' without the convient right.", currentUser, resourceId); 2143 result.put("message", "rights"); 2144 return result; 2145 } 2146 2147 org.ametys.plugins.explorer.threads.Thread comments = resource.getComments(false); 2148 2149 if (!(comments instanceof JCRThread)) 2150 { 2151 throw new IllegalClassException(JCRThread.class, comments.getClass()); 2152 } 2153 2154 JCRThread jcrComments = (JCRThread) comments; 2155 2156 post.remove(); 2157 jcrComments.saveChanges(); 2158 2159 result.put("id", resourceId); 2160 result.put("commentId", commentId); 2161 2162 return result; 2163 } 2164 2165 /** 2166 * Indicates if an user can delete a comment of a given resource 2167 * 2168 * @param user An user identity 2169 * @param resource The resource 2170 * @param comment The comment to edit 2171 * @return True if the user can edit the comment 2172 */ 2173 protected boolean _canDeleteComment(UserIdentity user, CommentableResource resource, JCRPost comment) 2174 { 2175 ExplorerNode node = resource.getParent(); 2176 // Comment right + owner OR Moderate right 2177 return _rightManager.hasRight(user, "Plugin_Explorer_File_Comment", node) == RightResult.RIGHT_ALLOW && comment.getAuthor().equals(user) 2178 || _rightManager.hasRight(user, "Plugin_Explorer_File_Moderate_Comments", node) == RightResult.RIGHT_ALLOW; 2179 } 2180 2181 /** 2182 * Get the comments of a resource 2183 * 2184 * @param resourceId The id of the resource 2185 * @param isEdition true to get the comments in edit mode 2186 * @return The comments 2187 */ 2188 @Callable 2189 public List<Map<String, Object>> getComments(String resourceId, boolean isEdition) 2190 { 2191 List<Map<String, Object>> comments = new ArrayList<>(); 2192 2193 AmetysObject ao = _resolver.resolveById(resourceId); 2194 2195 if (!(ao instanceof CommentableResource)) 2196 { 2197 throw new IllegalClassException(CommentableResource.class, ao.getClass()); 2198 } 2199 2200 org.ametys.plugins.explorer.threads.Thread thread = ((CommentableResource) ao).getComments(false); 2201 if (thread != null) 2202 { 2203 if (!(thread instanceof JCRThread)) 2204 { 2205 throw new IllegalClassException(JCRThread.class, thread.getClass()); 2206 } 2207 2208 JCRThread jcrComments = (JCRThread) thread; 2209 2210 AmetysObjectIterable<AmetysObject> children = jcrComments.getChildren(); 2211 for (AmetysObject child : children) 2212 { 2213 if (child instanceof JCRPost) 2214 { 2215 JCRPost comment = (JCRPost) child; 2216 comments.add(_comment2json(comment, isEdition)); 2217 } 2218 } 2219 } 2220 2221 return comments; 2222 } 2223 2224 /** 2225 * Get a comment of a resource 2226 * 2227 * @param commentId The id of the comment 2228 * @param isEdition true to get the comment in edit mode 2229 * @return The comments 2230 */ 2231 @Callable 2232 public Map<String, Object> getComment(String commentId, boolean isEdition) 2233 { 2234 // FIXME check read access on resource? 2235 JCRPost comment = _resolver.resolveById(commentId); 2236 return _comment2json(comment, isEdition); 2237 } 2238 2239 /** 2240 * Get JSOn representation of a comment 2241 * @param comment The comment 2242 * @param isEdition true to get the comment in edit mode 2243 * @return the comment as JSON 2244 */ 2245 protected Map<String, Object> _comment2json(JCRPost comment, boolean isEdition) 2246 { 2247 Map<String, Object> result = new HashMap<>(); 2248 2249 result.put("id", comment.getId()); 2250 result.put("content", isEdition ? _getCommentForEditing(comment) : _getComment(comment)); 2251 result.put("author", _userHelper.user2json(comment.getAuthor())); 2252 result.put("creationDate", DateUtils.dateToString(comment.getCreationDate())); 2253 result.put("lastModifiedDate", DateUtils.dateToString(comment.getLastModified())); 2254 2255 UserIdentity author = comment.getAuthor(); 2256 boolean isOwner = author.equals(_currentUserProvider.getUser()); 2257 result.put("isOwner", isOwner); 2258 2259 return result; 2260 } 2261 2262 /** 2263 * Bean for version information 2264 * 2265 */ 2266 protected static class VersionInformation 2267 { 2268 private String _rawName; 2269 2270 private String _name; 2271 2272 private Date _creationDate; 2273 2274 private Set<String> _labels = new HashSet<>(); 2275 2276 /** 2277 * Creates a {@link VersionInformation}. 2278 * 2279 * @param rawName the revision name. 2280 * @param creationDate the revision creation date. 2281 * @throws RepositoryException if an error occurs. 2282 */ 2283 public VersionInformation(String rawName, Date creationDate) throws RepositoryException 2284 { 2285 _creationDate = creationDate; 2286 _rawName = rawName; 2287 // 1.0 > v0, 1.1 > v1, ... 2288 _name = String.valueOf(Integer.parseInt(_rawName.substring(2)) + 1); 2289 } 2290 2291 /** 2292 * Retrieves the version name. 2293 * 2294 * @return the version name. 2295 */ 2296 public String getVersionName() 2297 { 2298 return _name; 2299 } 2300 2301 /** 2302 * Retrieves the version raw name. 2303 * 2304 * @return the version raw name. 2305 */ 2306 public String getVersionRawName() 2307 { 2308 return _rawName; 2309 } 2310 2311 /** 2312 * Retrieves the creation date. 2313 * 2314 * @return the creation date. 2315 * @throws RepositoryException if an error occurs. 2316 */ 2317 public Date getCreatedAt() throws RepositoryException 2318 { 2319 return _creationDate; 2320 } 2321 2322 /** 2323 * Retrieves the labels associated with this version. 2324 * 2325 * @return the labels. 2326 */ 2327 public Set<String> getLabels() 2328 { 2329 return _labels; 2330 } 2331 2332 /** 2333 * Add a label to this version. 2334 * 2335 * @param label the label. 2336 */ 2337 public void addLabel(String label) 2338 { 2339 _labels.add(label); 2340 } 2341 } 2342}