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.plugins.workspaces.documents; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.util.ArrayList; 021import java.util.Collection; 022import java.util.Collections; 023import java.util.HashMap; 024import java.util.LinkedList; 025import java.util.List; 026import java.util.Map; 027import java.util.Objects; 028import java.util.Optional; 029import java.util.Set; 030import java.util.function.Function; 031import java.util.stream.Collectors; 032 033import javax.jcr.RepositoryException; 034 035import org.apache.avalon.framework.service.ServiceException; 036import org.apache.avalon.framework.service.ServiceManager; 037import org.apache.cocoon.components.ContextHelper; 038import org.apache.cocoon.environment.Request; 039import org.apache.cocoon.servlet.multipart.Part; 040import org.apache.commons.io.IOUtils; 041import org.apache.commons.lang.IllegalClassException; 042import org.apache.commons.lang3.StringUtils; 043import org.apache.excalibur.source.Source; 044import org.apache.excalibur.source.SourceResolver; 045 046import org.ametys.cms.content.indexing.solr.SolrFieldNames; 047import org.ametys.cms.search.query.DocumentTypeQuery; 048import org.ametys.cms.search.query.FilenameQuery; 049import org.ametys.cms.search.query.FullTextQuery; 050import org.ametys.cms.search.query.MatchAllQuery; 051import org.ametys.cms.search.query.MimeTypeGroupQuery; 052import org.ametys.cms.search.query.OrQuery; 053import org.ametys.cms.search.query.Query; 054import org.ametys.cms.search.query.Query.Operator; 055import org.ametys.cms.search.query.StringQuery; 056import org.ametys.cms.search.solr.SearcherFactory; 057import org.ametys.cms.tag.Tag; 058import org.ametys.cms.transformation.xslt.ResolveURIComponent; 059import org.ametys.core.right.RightManager.RightResult; 060import org.ametys.core.ui.Callable; 061import org.ametys.core.user.UserIdentity; 062import org.ametys.core.util.DateUtils; 063import org.ametys.core.util.FilenameUtils; 064import org.ametys.core.util.URIUtils; 065import org.ametys.plugins.explorer.ExplorerNode; 066import org.ametys.plugins.explorer.ModifiableExplorerNode; 067import org.ametys.plugins.explorer.cmis.CMISRootResourcesCollection; 068import org.ametys.plugins.explorer.resources.ModifiableResource; 069import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 070import org.ametys.plugins.explorer.resources.Resource; 071import org.ametys.plugins.explorer.resources.ResourceCollection; 072import org.ametys.plugins.explorer.resources.actions.AddOrUpdateResourceHelper; 073import org.ametys.plugins.explorer.resources.actions.AddOrUpdateResourceHelper.ResourceOperationMode; 074import org.ametys.plugins.explorer.resources.actions.AddOrUpdateResourceHelper.ResourceOperationResult; 075import org.ametys.plugins.explorer.resources.actions.ExplorerResourcesDAO; 076import org.ametys.plugins.explorer.resources.jcr.JCRResource; 077import org.ametys.plugins.explorer.resources.jcr.JCRResourcesCollection; 078import org.ametys.plugins.explorer.threads.jcr.JCRPost; 079import org.ametys.plugins.repository.AmetysObject; 080import org.ametys.plugins.repository.AmetysObjectIterable; 081import org.ametys.plugins.repository.AmetysRepositoryException; 082import org.ametys.plugins.repository.ModifiableAmetysObject; 083import org.ametys.plugins.repository.RemovableAmetysObject; 084import org.ametys.plugins.repository.jcr.JCRTraversableAmetysObject; 085import org.ametys.plugins.repository.lock.LockableAmetysObject; 086import org.ametys.plugins.repository.tag.TagAwareAmetysObject; 087import org.ametys.plugins.workspaces.WorkspacesHelper; 088import org.ametys.plugins.workspaces.documents.onlyoffice.OnlyOfficeManager; 089import org.ametys.plugins.workspaces.html.HTMLTransformer; 090import org.ametys.plugins.workspaces.indexing.solr.SolrWorkspacesConstants; 091import org.ametys.plugins.workspaces.project.ProjectManager; 092import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint; 093import org.ametys.plugins.workspaces.project.objects.Project; 094import org.ametys.plugins.workspaces.search.query.KeywordQuery; 095import org.ametys.plugins.workspaces.search.query.ProjectQuery; 096import org.ametys.plugins.workspaces.tags.ProjectTagProviderExtensionPoint; 097import org.ametys.plugins.workspaces.tags.ProjectTagsDAO; 098import org.ametys.runtime.authentication.AccessDeniedException; 099import org.ametys.runtime.plugin.component.PluginAware; 100import org.ametys.web.WebConstants; 101 102/** 103 * DAO for resources of a project 104 */ 105public class WorkspaceExplorerResourceDAO extends ExplorerResourcesDAO implements PluginAware 106{ 107 /** Avalon Role */ 108 @SuppressWarnings("hiding") 109 public static final String ROLE = WorkspaceExplorerResourceDAO.class.getName(); 110 111 /** 112 * Enumeration for resource type 113 */ 114 public static enum ResourceType 115 { 116 /** Folder */ 117 FOLDER, 118 /** File */ 119 FILE 120 } 121 122 /** resource operation helper */ 123 protected AddOrUpdateResourceHelper _addOrUpdateResourceHelper; 124 125 private ProjectManager _projectManager; 126 private SearcherFactory _searcherFactory; 127 private WorkspaceModuleExtensionPoint _moduleEP; 128 private WorkspacesHelper _workspaceHelper; 129 private OnlyOfficeManager _onlyOfficeManager; 130 private HTMLTransformer _htmlTransformer; 131 private SourceResolver _sourceResolver; 132 133 private String _pluginName; 134 135 private ProjectTagProviderExtensionPoint _tagProviderExtensionPoint; 136 private ProjectTagsDAO _projectTagsDAO; 137 138 @Override 139 public void service(ServiceManager manager) throws ServiceException 140 { 141 super.service(manager); 142 _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE); 143 _htmlTransformer = (HTMLTransformer) manager.lookup(HTMLTransformer.ROLE); 144 _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 145 _addOrUpdateResourceHelper = (AddOrUpdateResourceHelper) manager.lookup(AddOrUpdateResourceHelper.ROLE); 146 _searcherFactory = (SearcherFactory) manager.lookup(SearcherFactory.ROLE); 147 _moduleEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE); 148 _workspaceHelper = (WorkspacesHelper) manager.lookup(WorkspacesHelper.ROLE); 149 _onlyOfficeManager = (OnlyOfficeManager) manager.lookup(OnlyOfficeManager.ROLE); 150 _tagProviderExtensionPoint = (ProjectTagProviderExtensionPoint) manager.lookup(ProjectTagProviderExtensionPoint.ROLE); 151 _projectTagsDAO = (ProjectTagsDAO) manager.lookup(ProjectTagsDAO.ROLE); 152 } 153 154 @Override 155 public void setPluginInfo(String pluginName, String featureName, String id) 156 { 157 _pluginName = pluginName; 158 } 159 160 /** 161 * Add a folder 162 * @param parentId Identifier of the parent collection. Can be null to add folder to root folder. 163 * @param inputName The desired name 164 * @param description The folder description 165 * @return The created folder or an error if a folder with same name already exists. 166 * @throws IllegalAccessException If the user has no sufficient rights 167 */ 168 @Callable 169 public Map<String, Object> addFolder(String parentId, String inputName, String description) throws IllegalAccessException 170 { 171 return addFolder(parentId, inputName, description, false); 172 } 173 174 /** 175 * Add a folder 176 * @param parentId Identifier of the parent collection. Can be null to add folder to root folder. 177 * @param inputName The desired name 178 * @param description The folder description 179 * @param renameIfExists True to rename if existing 180 * @return The result map with id, parentId and name keys 181 */ 182 @Callable 183 public Map<String, Object> addFolder(String parentId, String inputName, String description, Boolean renameIfExists) 184 { 185 ResourceCollection document = _getRootIfNull(parentId); 186 if (document == null) 187 { 188 throw new IllegalArgumentException("Unable to add folder: parent folder not found"); 189 } 190 191 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COLLECTION_ADD, document) != RightResult.RIGHT_ALLOW) 192 { 193 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to add folder without convenient right [" + RIGHTS_COLLECTION_ADD + "]"); 194 } 195 196 if (!(document instanceof ModifiableResourceCollection)) 197 { 198 throw new IllegalClassException(ModifiableResourceCollection.class, document.getClass()); 199 } 200 201 List<String> errors = new LinkedList<>(); 202 ResourceCollection collection = addResourceCollection((ModifiableResourceCollection) document, inputName, renameIfExists, errors); 203 204 Map<String, Object> result = new HashMap<>(); 205 if (!errors.isEmpty()) 206 { 207 result.put("message", errors.get(0)); 208 result.put("error", true); 209 } 210 211 if (collection != null) 212 { 213 ((ModifiableResourceCollection) collection).setDescription(description); 214 ((ModifiableResourceCollection) collection).saveChanges(); 215 216 result.putAll(_extractFolderData(collection)); 217 } 218 219 return result; 220 } 221 222 /** 223 * Rename a folder 224 * @param id Identifier of the folder to edit 225 * @param name The new name 226 * @return The result map with id and name keys 227 */ 228 @Callable 229 public Map<String, Object> renameFolder(String id, String name) 230 { 231 Map<String, Object> result = new HashMap<>(); 232 233 JCRResourcesCollection folder = (JCRResourcesCollection) _resolver.resolveById(id); 234 235 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COLLECTION_EDIT, folder) != RightResult.RIGHT_ALLOW) 236 { 237 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to edit folder without convenient right [" + RIGHTS_COLLECTION_EDIT + "]"); 238 } 239 240 List<String> errors = new LinkedList<>(); 241 JCRResourcesCollection newFolder = null; 242 try 243 { 244 newFolder = (JCRResourcesCollection) renameObject(folder, name, errors); 245 if (!errors.isEmpty()) 246 { 247 result.put("success", false); 248 result.put("message", errors.get(0)); 249 } 250 else 251 { 252 newFolder.saveChanges(); 253 254 result.put("success", true); 255 result.putAll(_extractFolderData(newFolder)); 256 } 257 } 258 catch (RepositoryException e) 259 { 260 getLogger().error("Repository exception during folder edition.", e); 261 errors.add("repository"); 262 } 263 264 return result; 265 } 266 267 /** 268 * Edit a folder 269 * @param id Identifier of the folder to edit 270 * @param inputName The desired name 271 * @param description The folder description 272 * @return The result map with id and name keys 273 */ 274 @Callable 275 public Map<String, Object> editFolder(String id, String inputName, String description) 276 { 277 JCRResourcesCollection folder = (JCRResourcesCollection) _resolver.resolveById(id); 278 279 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COLLECTION_EDIT, folder) != RightResult.RIGHT_ALLOW) 280 { 281 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to edit folder without convenient right [" + RIGHTS_COLLECTION_EDIT + "]"); 282 } 283 284 List<String> errors = new LinkedList<>(); 285 JCRResourcesCollection newFolder = null; 286 try 287 { 288 newFolder = (JCRResourcesCollection) renameObject(folder, inputName, errors); 289 } 290 catch (RepositoryException e) 291 { 292 getLogger().error("Repository exception during folder edition.", e); 293 errors.add("repository"); 294 } 295 296 Map<String, Object> result = new HashMap<>(); 297 if (!errors.isEmpty()) 298 { 299 String error = errors.get(0); 300 301 // existing node is allowed, the description can still be changed. 302 if (!"already-exist".equals(error)) 303 { 304 result.put("message", error); 305 } 306 else 307 { 308 newFolder = folder; 309 } 310 } 311 312 if (newFolder != null) 313 { 314 newFolder.setDescription(description); 315 newFolder.saveChanges(); 316 317 result.putAll(_extractFolderData(newFolder)); 318 } 319 320 return result; 321 } 322 323 /** 324 * Delete a folder 325 * @param id Identifier of the folder to delete 326 * @return The result map with the parent id key 327 */ 328 @Callable 329 public Map<String, Object> deleteFolder(String id) 330 { 331 RemovableAmetysObject folder = (RemovableAmetysObject) _resolver.resolveById(id); 332 333 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_COLLECTION_DELETE, folder) != RightResult.RIGHT_ALLOW) 334 { 335 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to delete folder without convenient right [" + RIGHTS_COLLECTION_DELETE + "]"); 336 } 337 338 List<String> errors = new LinkedList<>(); 339 String parentId = deleteObject(folder, errors); 340 341 Map<String, Object> result = new HashMap<>(); 342 if (!errors.isEmpty()) 343 { 344 String error = errors.get(0); 345 result.put("message", error); 346 } 347 348 // include parent id except if it is the root 349 ResourceCollection rootCollection = _getRootFromRequest(); 350 boolean isRoot = rootCollection != null && rootCollection.getId().equals(parentId); 351 if (StringUtils.isNotEmpty(parentId) && !isRoot) 352 { 353 result.put("parentId", parentId); 354 } 355 356 return result; 357 } 358 359 /** 360 * Lock resources 361 * @param ids The id of resources to lock 362 * @return the result. 363 */ 364 @Callable 365 public Map<String, Object> lockResources(List<String> ids) 366 { 367 Map<String, Object> result = new HashMap<>(); 368 369 result.put("locked-resources", new ArrayList<String>()); 370 result.put("unlocked-resources", new ArrayList<Map<String, Object>>()); 371 372 for (String id : ids) 373 { 374 JCRResource resource = _resolver.resolveById(id); 375 376 if (!resource.isLocked()) 377 { 378 resource.lock(); 379 380 @SuppressWarnings("unchecked") 381 List<String> lockedResources = (List<String>) result.get("locked-resources"); 382 lockedResources.add(id); 383 } 384 else if (!resource.getLockOwner().equals(_currentUserProvider.getUser())) 385 { 386 UserIdentity lockOwner = resource.getLockOwner(); 387 388 getLogger().error("Unable to lock resource of id '" + id + "': the resource is already locked by user " + UserIdentity.userIdentityToString(lockOwner)); 389 390 Map<String, Object> info = new HashMap<>(); 391 info.put("id", id); 392 info.put("name", resource.getName()); 393 info.put("lockOwner", _userHelper.user2json(lockOwner)); 394 395 @SuppressWarnings("unchecked") 396 List<Map<String, Object>> unlockedResources = (List<Map<String, Object>>) result.get("unlocked-resources"); 397 unlockedResources.add(info); 398 } 399 } 400 401 return result; 402 } 403 404 /** 405 * Unlock resources 406 * @param ids The id of resources to lock 407 * @return the result. 408 */ 409 @Callable 410 public Map<String, Object> unlockResources(List<String> ids) 411 { 412 UserIdentity currentUser = _currentUserProvider.getUser(); 413 boolean canUnlockAll = _rightManager.hasRight(currentUser, RIGHTS_RESOURCE_UNLOCK_ALL, "/cms") == RightResult.RIGHT_ALLOW; 414 415 Map<String, Object> result = new HashMap<>(); 416 417 result.put("unlocked-resources", new ArrayList<String>()); 418 result.put("still-locked-resources", new ArrayList<Map<String, Object>>()); 419 420 for (String id : ids) 421 { 422 JCRResource resource = _resolver.resolveById(id); 423 424 if (resource.isLocked()) 425 { 426 if (canUnlockAll || resource.getLockOwner().equals(currentUser)) 427 { 428 resource.unlock(); 429 430 @SuppressWarnings("unchecked") 431 List<String> unlockedResources = (List<String>) result.get("unlocked-resources"); 432 unlockedResources.add(id); 433 } 434 else 435 { 436 UserIdentity lockOwner = resource.getLockOwner(); 437 438 getLogger().error("Unable to unlock resource of id '" + id + "': the resource is locked by user " + UserIdentity.userIdentityToString(lockOwner)); 439 440 Map<String, Object> info = new HashMap<>(); 441 info.put("id", id); 442 info.put("name", resource.getName()); 443 info.put("lockOwner", _userHelper.user2json(lockOwner)); 444 445 @SuppressWarnings("unchecked") 446 List<Map<String, Object>> stilllockedResources = (List<Map<String, Object>>) result.get("still-locked-resources"); 447 stilllockedResources.add(info); 448 } 449 } 450 else 451 { 452 @SuppressWarnings("unchecked") 453 List<String> unlockedResources = (List<String>) result.get("unlocked-resources"); 454 unlockedResources.add(id); 455 } 456 } 457 458 return result; 459 } 460 461 /** 462 * Determines if a resource with given name already exists 463 * @param parentId the id of parent collection. Can be null. 464 * @param name the name of resource 465 * @return true if a resource with same name exists 466 */ 467 @Callable 468 @Override 469 public boolean resourceExists(String parentId, String name) 470 { 471 ResourceCollection folder = _getRootIfNull(parentId); 472 return folder != null && resourceExists(folder, name); 473 } 474 475 /** 476 * Rename a folder 477 * @param id Identifier of the folder to edit 478 * @param name The new name 479 * @return The result map with id and name keys 480 */ 481 @Callable 482 public Map<String, Object> renameFile(String id, String name) 483 { 484 Map<String, Object> result = new HashMap<>(); 485 486 JCRResource file = (JCRResource) _resolver.resolveById(id); 487 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_RESOURCE_RENAME, file) != RightResult.RIGHT_ALLOW) 488 { 489 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to rename file without convenient right [" + RIGHTS_RESOURCE_RENAME + "]"); 490 } 491 492 if (StringUtils.isNotEmpty(name) && !StringUtils.equals(file.getName(), name)) 493 { 494 List<String> errors = new LinkedList<>(); 495 JCRResource renamedFile = null; 496 try 497 { 498 renamedFile = renameResource(file, name, errors); 499 if (!errors.isEmpty()) 500 { 501 result.put("success", false); 502 result.put("message", errors.get(0)); 503 } 504 else 505 { 506 renamedFile.saveChanges(); 507 508 result.put("success", true); 509 result.putAll(_extractFileData(renamedFile)); 510 } 511 } 512 catch (RepositoryException e) 513 { 514 getLogger().error("Repository exception during file edition.", e); 515 errors.add("repository"); 516 } 517 518 } 519 520 return result; 521 } 522 523 524 /** 525 * Edit a file 526 * @param id Identifier of the file to edit 527 * @param inputName The desired name 528 * @param description The file description 529 * @param tags The file tags 530 * @return The result map with id and name keys 531 */ 532 @Callable 533 public Map<String, Object> editFile(String id, String inputName, String description, Collection<String> tags) 534 { 535 Map<String, Object> result = new HashMap<>(); 536 JCRResource file = (JCRResource) _resolver.resolveById(id); 537 538 // Check lock on resource 539 if (!checkLock(file)) 540 { 541 getLogger().warn("User '{}' is trying to edit file '{}' but it is locked by another user", _currentUserProvider.getUser(), file.getName()); 542 result.put("message", "locked"); 543 return result; 544 } 545 546 List<String> errors = new LinkedList<>(); 547 548 // Rename 549 JCRResource renamedFile = null; 550 if (StringUtils.isNotEmpty(inputName) && !StringUtils.equals(file.getName(), inputName)) 551 { 552 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_RESOURCE_RENAME, file) != RightResult.RIGHT_ALLOW) 553 { 554 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to rename file without convenient right [" + RIGHTS_RESOURCE_RENAME + "]"); 555 } 556 557 try 558 { 559 renamedFile = renameResource(file, inputName, errors); 560 561 if (errors.isEmpty()) 562 { 563 file = renamedFile; 564 } 565 } 566 catch (RepositoryException e) 567 { 568 getLogger().error("Repository exception during folder edition.", e); 569 errors.add("repository-rename"); 570 } 571 } 572 573 if (!errors.isEmpty()) 574 { 575 String error = errors.get(0); 576 result.put("message", error); 577 return result; 578 } 579 580 // edit description + tags 581 List<String> fileTags = _sanitizeFileTags(tags); 582 583 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_RESOURCE_EDIT_DC, file) != RightResult.RIGHT_ALLOW) 584 { 585 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to edit dc for file without convenient right [" + RIGHTS_RESOURCE_EDIT_DC + "]"); 586 } 587 588 Map<String, Object> editValues = new HashMap<>(); 589 editValues.put("dc_description", StringUtils.defaultIfEmpty(description, null)); 590 591 try 592 { 593 file.setKeywords(fileTags.toArray(new String[fileTags.size()])); 594 setDCMetadata(file, editValues); 595 596 // Add tags to the project 597 _projectManager.addTags(fileTags); 598 599 file.saveChanges(); 600 } 601 catch (AmetysRepositoryException e) 602 { 603 getLogger().error("Repository exception during folder edition.", e); 604 errors.add("repository-edit"); 605 } 606 607 if (!errors.isEmpty()) 608 { 609 String error = errors.get(0); 610 result.put("message", error); 611 } 612 else 613 { 614 result.putAll(_extractFileData(file)); 615 } 616 617 return result; 618 } 619 620 /** 621 * Set tags to resource 622 * @param resourceId the id of resources 623 * @param tags the file tags to set 624 * @return the file tags 625 */ 626 @Callable 627 public Map<String, Object> setTags(String resourceId, List<Object> tags) 628 { 629 List<String> createdTags = new ArrayList<>(); 630 List<Map<String, Object>> createdTagsJson = new ArrayList<>(); 631 632 JCRResource file = (JCRResource) _resolver.resolveById(resourceId); 633 if (_rightManager.hasRight(_currentUserProvider.getUser(), RIGHTS_RESOURCE_EDIT_DC, file) != RightResult.RIGHT_ALLOW) 634 { 635 throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to tag file without convenient right [" + RIGHTS_RESOURCE_EDIT_DC + "]"); 636 } 637 638 Set<String> oldTags = file.getTags(); 639 for (Object tag : tags) 640 { 641 // Tag doesn't exist so create the tag 642 if (tag instanceof Map) 643 { 644 @SuppressWarnings("unchecked") 645 String tagText = (String) ((Map<String, Object>) tag).get("text"); 646 List<Map<String, Object>> newTags = _projectTagsDAO.addTags(new String[] {tagText}); 647 String newTag = (String) newTags.get(0).get("name"); 648 file.tag(newTag); 649 createdTags.add(newTag); 650 createdTagsJson.addAll(newTags); 651 } 652 else 653 { 654 file.tag((String) tag); 655 } 656 } 657 658 // Untag unused tags 659 for (String oldTag : oldTags) 660 { 661 if (!tags.contains(oldTag) && !createdTags.contains(oldTag)) 662 { 663 file.untag(oldTag); 664 } 665 } 666 667 file.saveChanges(); 668 669 Map<String, Object> results = new HashMap<>(); 670 results.put("fileTags", _tags2json(file)); 671 results.put("newTags", createdTagsJson); 672 673 return results; 674 } 675 676 /** 677 * Copy file resources 678 * @param ids The list of identifiers for the resources to copy 679 * @param targetId The id of target to copy into. Can be null to copy into root folder. 680 * @return The result map with a message key in case of an error or with the list of uncopied/copied resources 681 * @throws RepositoryException If there is a repository error 682 */ 683 @Callable 684 public Map<String, Object> copyFiles(List<String> ids, String targetId) throws RepositoryException 685 { 686 ResourceCollection folder = _getRootIfNull(targetId); 687 if (folder == null) 688 { 689 throw new IllegalArgumentException("Unable to copy files: parent folder not found"); 690 } 691 692 if (!(folder instanceof ModifiableResourceCollection)) 693 { 694 throw new IllegalClassException(ModifiableResourceCollection.class, folder.getClass()); 695 } 696 697 return copyResource(ids, (ModifiableResourceCollection) folder); 698 } 699 700 /** 701 * Move documents (files or folders) 702 * @param ids The list of identifiers for the objects to move 703 * @param targetId The id of target to move into. Can be null to move documents to root folder. 704 * @return The result map with a message key in case of an error or with the list of unmoved/moved objects 705 * @throws RepositoryException If there is a repository error 706 */ 707 @Callable 708 public Map<String, Object> moveDocuments(List<String> ids, String targetId) throws RepositoryException 709 { 710 ResourceCollection folder = _getRootIfNull(targetId); 711 if (folder == null) 712 { 713 throw new IllegalArgumentException("Unable to move documents: parent folder not found"); 714 } 715 716 if (!(folder instanceof JCRTraversableAmetysObject)) 717 { 718 throw new IllegalClassException(JCRTraversableAmetysObject.class, folder.getClass()); 719 } 720 721 return moveObject(ids, (JCRTraversableAmetysObject) folder); 722 } 723 724 /** 725 * Search for files in the document module 726 * @param query The search query 727 * @param lang The search language 728 * @return A result map containing a <em>resources</em> entry which is a list of the files data 729 * @throws Exception if an exception occurs 730 */ 731 @Callable 732 public Map<String, Object> searchFiles(String query, String lang) throws Exception 733 { 734 // Handle result map 735 Map<String, Object> result = new HashMap<>(); 736 737 String escapedQuery = query.replace("\"", "\\\""); 738 739 Query solrQuery; 740 if (StringUtils.isEmpty(query)) 741 { 742 solrQuery = new MatchAllQuery(); 743 } 744 else 745 { 746 List<Query> queries = new ArrayList<>(); 747 queries.add(new FilenameQuery(query)); 748 queries.add(new StringQuery(SolrFieldNames.TITLE, Operator.LIKE, "*" + query + "*", null)); 749 queries.add(new FullTextQuery(escapedQuery, lang)); 750 queries.add(new KeywordQuery(escapedQuery.split(" "))); 751 solrQuery = new OrQuery(queries); 752 } 753 754 AmetysObjectIterable<Resource> results = _searcherFactory.create() 755 .withQuery(solrQuery) 756 .addFilterQuery(new DocumentTypeQuery(SolrWorkspacesConstants.TYPE_PROJECT_RESOURCE)) 757 .addFilterQuery(new ProjectQuery(_getProjectFromRequest().getId())) 758 .search(); 759 760 List<Map<String, Object>> resourceData = results.stream() 761 .map(this::_extractFileData) 762 .collect(Collectors.toList()); 763 764 result.put("resources", resourceData); 765 766 return result; 767 } 768 769 /** 770 * Search for files by their type 771 * @param type The file type 772 * @return A result map containing a <em>resources</em> entry which is a list of the files data 773 * @throws Exception if an exception occurs 774 */ 775 @Callable 776 public Map<String, Object> searchFilesByType(String type) throws Exception 777 { 778 Map<String, Object> result = new HashMap<>(); 779 780 AmetysObjectIterable<Resource> results = _searcherFactory.create() 781 .withQuery(new MimeTypeGroupQuery(type)) 782 .addFilterQuery(new DocumentTypeQuery(SolrWorkspacesConstants.TYPE_PROJECT_RESOURCE)) 783 .addFilterQuery(new ProjectQuery(_getProjectFromRequest().getId())) 784 .search(); 785 786 List<Map<String, Object>> resourceData = results.stream() 787 .map(this::_extractFileData) 788 .collect(Collectors.toList()); 789 790 result.put("resources", resourceData); 791 792 return result; 793 } 794 795 /** 796 * Get the root folder 797 * @return the root folder as JSON object 798 */ 799 @Callable 800 public Map<String, Object> getRootFolder() 801 { 802 return getFolder(null); 803 } 804 805 /** 806 * Get folder 807 * @param folderId the folder id 808 * @return the folder as JSON object 809 */ 810 @Callable 811 public Map<String, Object> getFolder(String folderId) 812 { 813 ResourceCollection collection = _getRootIfNull(folderId); 814 return _extractFolderData(collection); 815 } 816 817 /** 818 * Get file 819 * @param resourceId the resource id 820 * @return the file as JSON object 821 */ 822 @Callable 823 public Map<String, Object> getFile(String resourceId) 824 { 825 Resource resource = _resolver.resolveById(resourceId); 826 return _extractFileData(resource); 827 } 828 /** 829 * Get the child folders 830 * @param parentId the parent id. Can be null to get root folders 831 * @return the sub folders 832 */ 833 @Callable 834 public List<Map<String, Object>> getFolders(String parentId) 835 { 836 return getChildDocumentsData(parentId, false, true); 837 } 838 839 /** 840 * Get the child files 841 * @param parentId the parent id. Can be null to get root files 842 * @return the child files 843 */ 844 @Callable 845 public List<Map<String, Object>> getFiles(String parentId) 846 { 847 return getChildDocumentsData(parentId, true, false); 848 } 849 850 /** 851 * Get the child folders and files 852 * @param folderId the parent id. Can be null to get root folders and files 853 * @return the child folders and files 854 */ 855 @Callable 856 public Map<String, Object> getFoldersAndFiles(String folderId) 857 { 858 ResourceCollection collection = _getRootIfNull(folderId); 859 860 Map<String, Object> data = _extractFolderData(collection); 861 862 data.put("files", getChildDocumentsData(folderId, true, false)); 863 data.put("children", getChildDocumentsData(folderId, false, true)); 864 865 return data; 866 } 867 868 869 /** 870 * Retrieves the children of a document and extracts its data. 871 * @param parentId Identifier of the parent collection. Can be null to get children of root folder. 872 * @param excludeFolders Folders will be excluded if true 873 * @param excludeFiles Files will be excluded if true 874 * @return The map of information 875 */ 876 @Callable 877 public List<Map<String, Object>> getChildDocumentsData(String parentId, boolean excludeFolders, boolean excludeFiles) 878 { 879 ResourceCollection document = _getRootIfNull(parentId); 880 if (document == null) 881 { 882 throw new IllegalArgumentException("Unable to get child documents: parent folder not found"); 883 } 884 return getChildDocumentsData(document, excludeFolders, excludeFiles); 885 } 886 887 /** 888 * Retrieves the children of a document and extracts its data. 889 * @param document the document 890 * @param excludeFolders Folders will be excluded if true 891 * @param excludeFiles Files will be excluded if true 892 * @return The map of information 893 */ 894 public List<Map<String, Object>> getChildDocumentsData(ResourceCollection document, boolean excludeFolders, boolean excludeFiles) 895 { 896 ResourceCollection parent = _getRootIfNull(document); 897 if (parent == null) 898 { 899 throw new IllegalArgumentException("Unable to get child documents: parent folder not found"); 900 } 901 902 return parent.getChildren().stream() 903 .map(child -> _extractDocumentData(child, excludeFolders, excludeFiles)) 904 .filter(Objects::nonNull) 905 .collect(Collectors.toList()); 906 } 907 908 /** 909 * Retrieves the set of standard data for a document (folder or resource) 910 * @param id the document id or null to get root document 911 * @param excludeFilesInFolderHierarchy Should child files be taken into account when extracting data of a folder 912 * @return The map of data 913 */ 914 // TODO To remove not used 915 @Deprecated 916 @Callable 917 public Map<String, Object> getDocumentData(String id, boolean excludeFilesInFolderHierarchy) 918 { 919 AmetysObject document = id == null ? _getRootFromRequest() : _resolver.resolveById(id); 920 if (document == null) 921 { 922 throw new IllegalArgumentException("No project found in request to get root document data"); 923 } 924 925 Map<String, Object> data = _extractDocumentData(document, false, false); 926 return data != null ? data : new HashMap<>(); 927 } 928 929 /** 930 * Retrieves the set of standard data for a list of documents 931 * @param ids The list of document identifiers 932 * @param excludeFilesInFolderHierarchy Should child files be taken into account when extracting data of a folder 933 * @return The map of data 934 */ 935 // TODO To remove not used 936 @Callable 937 public List<Map<String, Object>> getDocumentsData(List<String> ids, boolean excludeFilesInFolderHierarchy) 938 { 939 return ids.stream() 940 .map(id -> getDocumentData(id, excludeFilesInFolderHierarchy)) 941 .collect(Collectors.toList()); 942 } 943 944 @Override 945 protected Map<String, Object> _comment2json(JCRPost comment, boolean isEdition) 946 { 947 Map<String, Object> comment2json = super._comment2json(comment, isEdition); 948 949 String lang = _getCurrentLanguage(); 950 String authorImgUrl = _workspaceHelper.getAvatar(comment.getAuthor(), lang, 30); 951 952 @SuppressWarnings("unchecked") 953 Map<String, Object> author = (Map<String, Object>) comment2json.get("author"); 954 author.put("imgUrl", authorImgUrl); 955 956 return comment2json; 957 } 958 959 @Override 960 protected Map<String, Object> _version2json(JCRResource resource, VersionInformation versionInformation) throws RepositoryException 961 { 962 Map<String, Object> version2json = super._version2json(resource, versionInformation); 963 964 String lang = _getCurrentLanguage(); 965 966 @SuppressWarnings("unchecked") 967 Map<String, Object> author = (Map<String, Object>) version2json.get("author"); 968 String authorImgUrl = _workspaceHelper.getAvatar(resource.getLastContributor(), lang, 30); 969 author.put("imgUrl", authorImgUrl); 970 971 return version2json; 972 973 } 974 975 /** 976 * Generates an uri to open a document through webdav 977 * @param documentId The document identifier 978 * @return The generated uri 979 */ 980 @Callable 981 public String generateWebdavUri(String documentId) 982 { 983 return ResolveURIComponent.resolve("webdav-project-resource", documentId, false, true); 984 } 985 986 private ResourceCollection _getRootIfNull(ResourceCollection document) 987 { 988 return document != null ? document : _getRootFromRequest(); 989 } 990 991 private ResourceCollection _getRootIfNull(String documentId) 992 { 993 return StringUtils.isNotEmpty(documentId) 994 ? (ResourceCollection) _resolver.resolveById(documentId) 995 : _getRootFromRequest(); 996 } 997 998 private ResourceCollection _getRootFromRequest() 999 { 1000 Project project = _getProjectFromRequest(); 1001 1002 if (project != null) 1003 { 1004 return _getRootFromProject(project); 1005 } 1006 else 1007 { 1008 return null; 1009 } 1010 } 1011 1012 private ResourceCollection _getRootFromObject(AmetysObject ametysObject) 1013 { 1014 Project project = _getProjectFomObject(ametysObject); 1015 if (project != null) 1016 { 1017 return _getRootFromProject(project); 1018 } 1019 return null; 1020 } 1021 1022 private ResourceCollection _getRootFromProject(Project project) 1023 { 1024 if (project != null) 1025 { 1026 DocumentWorkspaceModule module = _moduleEP.getModule(DocumentWorkspaceModule.DOCUMENT_MODULE_ID); 1027 return module.getModuleRoot(project, false); 1028 } 1029 else 1030 { 1031 return null; 1032 } 1033 } 1034 1035 private Project _getProjectFromRequest() 1036 { 1037 Request request = ContextHelper.getRequest(_context); 1038 1039 String projectName = (String) request.getAttribute("projectName"); 1040 if (projectName != null) 1041 { 1042 return _projectManager.getProject(projectName); 1043 } 1044 else 1045 { 1046 return null; 1047 } 1048 } 1049 1050 private Project _getProjectFomObject(AmetysObject ametysObject) 1051 { 1052 AmetysObject parent = ametysObject; 1053 1054 while (parent != null && !(parent instanceof Project)) 1055 { 1056 parent = parent.getParent(); 1057 } 1058 1059 if (parent == null) 1060 { 1061 return null; 1062 } 1063 return (Project) parent; 1064 } 1065 1066 private String _getCurrentLanguage() 1067 { 1068 Request request = ContextHelper.getRequest(_context); 1069 return (String) request.getAttribute(WebConstants.REQUEST_ATTR_SITEMAP_NAME); 1070 } 1071 1072 /** 1073 * Internal method to extract the valuable data of a document 1074 * @param document The document (file or folder) 1075 * @param excludeFolders Folders will be excluded if true 1076 * @param excludeFiles Files will be excluded if true 1077 * @return the valuable document data 1078 */ 1079 protected Map<String, Object> _extractDocumentData(AmetysObject document, boolean excludeFolders, boolean excludeFiles) 1080 { 1081 if (!excludeFiles && document instanceof Resource) 1082 { 1083 Resource file = (Resource) document; 1084 if (_canView(file)) 1085 { 1086 return _extractFileData(file); 1087 } 1088 } 1089 else if (!excludeFolders && document instanceof ResourceCollection) 1090 { 1091 ResourceCollection folder = (ResourceCollection) document; 1092 if (_canView(folder)) 1093 { 1094 return _extractFolderData(folder); 1095 } 1096 } 1097 1098 return null; 1099 } 1100 1101 /** 1102 * Internal method to extract the valuable data of a folder 1103 * @param folder The folder 1104 * @return the valuable folder data 1105 */ 1106 protected Map<String, Object> _extractFolderData(ResourceCollection folder) 1107 { 1108 Map<String, Object> data = new HashMap<>(); 1109 1110 data.put("id", folder.getId()); 1111 data.put("name", folder.getName()); 1112 data.put("path", _getFolderPath(folder)); 1113 data.put("type", ResourceType.FOLDER.name().toLowerCase()); 1114 data.put("description", StringUtils.defaultString(folder.getDescription())); 1115 1116 AmetysObject parent = folder.getParent(); 1117 if (parent != null && parent instanceof ResourceCollection) 1118 { 1119 data.put("location", ((ResourceCollection) parent).getName()); 1120 data.put("parentId", parent.getId()); 1121 } 1122 1123 boolean hasChildren = _hasChildren(folder, true); 1124 if (hasChildren) 1125 { 1126 data.put("children", Collections.EMPTY_LIST); 1127 } 1128 1129 data.put("modifiable", folder instanceof ModifiableAmetysObject); 1130 data.put("canCreateChild", folder instanceof ModifiableExplorerNode); 1131 data.put("rights", _extractFolderRightData(folder)); 1132 1133 data.put("notification", false); // TODO unread notification (not yet supported) 1134 1135 return data; 1136 } 1137 1138 private List<String> _getFolderPath(ResourceCollection folder) 1139 { 1140 List<String> paths = new ArrayList<>(); 1141 1142 ResourceCollection rootDocuments = _getRootFromObject(folder); 1143 1144 if (!rootDocuments.equals(folder)) 1145 { 1146 List<ExplorerNode> parents = new ArrayList<>(); 1147 1148 AmetysObject parent = folder.getParent(); 1149 while (parent instanceof ExplorerNode && !parent.equals(rootDocuments)) 1150 { 1151 parents.add((ExplorerNode) parent); 1152 parent = parent.getParent(); 1153 } 1154 1155 parents.add(rootDocuments); 1156 1157 Collections.reverse(parents); 1158 1159 parents.stream().forEach(p -> 1160 { 1161 paths.add(p.getId()); 1162 }); 1163 } 1164 1165 return paths; 1166 } 1167 1168 /** 1169 * Internal method to detect if a document has child 1170 * @param folder The folder 1171 * @param ignoreFiles Should child files be taken into account to compute the 'hasChildDocuments' data. 1172 * @return the valuable folder data 1173 */ 1174 protected boolean _hasChildren(ResourceCollection folder, boolean ignoreFiles) 1175 { 1176 try (AmetysObjectIterable<AmetysObject> children = folder.getChildren()) 1177 { 1178 for (AmetysObject child : children) 1179 { 1180 if (child instanceof ResourceCollection && _canView((ResourceCollection) child) 1181 || !ignoreFiles && child instanceof Resource && _canView((Resource) child)) 1182 { 1183 return true; 1184 } 1185 } 1186 1187 return false; 1188 } 1189 } 1190 1191 /** 1192 * Internal method to extract the data concerning the right of the current user for a folder 1193 * @param folder The folder 1194 * @return The map of right data. Keys are the rights id, and values indicates whether the current user has the right or not. 1195 */ 1196 protected Map<String, Object> _extractFolderRightData(ResourceCollection folder) 1197 { 1198 Map<String, Object> rightsData = new HashMap<>(); 1199 UserIdentity user = _currentUserProvider.getUser(); 1200 1201 // Add 1202 rightsData.put("add-file", _rightManager.hasRight(user, RIGHTS_RESOURCE_ADD, folder) == RightResult.RIGHT_ALLOW); 1203 rightsData.put("add-folder", _rightManager.hasRight(user, RIGHTS_COLLECTION_ADD, folder) == RightResult.RIGHT_ALLOW); 1204 rightsData.put("add-cmis-folder", _rightManager.hasRight(user, "Plugin_Explorer_CMIS_Add", folder) == RightResult.RIGHT_ALLOW); 1205 1206 // Rename - Edit 1207 rightsData.put("edit", _rightManager.hasRight(user, RIGHTS_COLLECTION_EDIT, folder) == RightResult.RIGHT_ALLOW); 1208 1209 // Delete 1210 rightsData.put("delete", _rightManager.hasRight(user, RIGHTS_COLLECTION_DELETE, folder) == RightResult.RIGHT_ALLOW); 1211 // FIXME Delete own? 1212 1213 return rightsData; 1214 } 1215 1216 /** 1217 * Add a file 1218 * @param part The uploaded part corresponding to the file 1219 * @param parentId Identifier of the parent collection 1220 * @param unarchive True if the file is an archive that should be unarchived (only available for ZIP file) 1221 * @param allowRename True if the file can be renamed if it already exists 1222 * @param allowUpdate True if the file can be updated if it already exists (and allowRename is false) 1223 * @return The result map with id, parentId and name keys 1224 */ 1225 @Callable 1226 public Map<String, Object> addFile(Part part, String parentId, boolean unarchive, boolean allowRename, boolean allowUpdate) 1227 { 1228 ModifiableResourceCollection modifiableFolder = getModifiableResourceCollection(parentId); 1229 _addOrUpdateResourceHelper.checkAddResourceRight(modifiableFolder); 1230 1231 ResourceOperationMode mode = getOperationMode(unarchive, allowRename, allowUpdate); 1232 1233 ResourceOperationResult operationResult = _addOrUpdateResourceHelper.performResourceOperation(part, modifiableFolder, mode); 1234 1235 // Handle result map 1236 return generateActionResult(modifiableFolder, operationResult); 1237 } 1238 /** 1239 * Add a file 1240 * @param inputStream The uploaded input stream 1241 * @param fileName desired file name 1242 * @param parentId Identifier of the parent collection 1243 * @param unarchive True if the file is an archive that should be unarchived (only available for ZIP file) 1244 * @param allowRename True if the file can be renamed if it already exists 1245 * @param allowUpdate True if the file can be updated if it already exists (and allowRename is false) 1246 * @return The result map with id, parentId and name keys 1247 */ 1248 public Map<String, Object> addFile(InputStream inputStream, String fileName, String parentId, boolean unarchive, boolean allowRename, boolean allowUpdate) 1249 { 1250 ModifiableResourceCollection modifiableFolder = getModifiableResourceCollection(parentId); 1251 ResourceOperationMode mode = getOperationMode(unarchive, allowRename, allowUpdate); 1252 1253 ResourceOperationResult operationResult = _addOrUpdateResourceHelper.performResourceOperation(inputStream, fileName, modifiableFolder, mode); 1254 1255 // Handle result map 1256 return generateActionResult(modifiableFolder, operationResult); 1257 } 1258 1259 /** 1260 * get a {@link ModifiableResourceCollection} for an ID, or the root folder; 1261 * @param ametysId id of the resource. Can be null to get root folder. 1262 * @return ModifiableResourceCollection 1263 * @throws IllegalClassException if id links to a node which is not a {@link ModifiableResourceCollection} 1264 */ 1265 private ModifiableResourceCollection getModifiableResourceCollection(String ametysId) 1266 { 1267 ResourceCollection folder = _getRootIfNull(ametysId); 1268 1269 if (folder == null) 1270 { 1271 throw new IllegalArgumentException("Root folder not found"); 1272 } 1273 1274 if (!(folder instanceof ModifiableResourceCollection)) 1275 { 1276 throw new IllegalClassException(ModifiableResourceCollection.class, folder.getClass()); 1277 } 1278 1279 return (ModifiableResourceCollection) folder; 1280 } 1281 /** 1282 * returns the {@link ResourceOperationMode} according to parameters 1283 * @param unarchive unarchive 1284 * @param allowRename allowRename 1285 * @param allowUpdate allowUpdate 1286 * @return ADD, ADD_UNZIP, ADD_RENAME, ADD_UPDATE 1287 */ 1288 private ResourceOperationMode getOperationMode(boolean unarchive, boolean allowRename, boolean allowUpdate) 1289 { 1290 ResourceOperationMode mode = ResourceOperationMode.ADD; 1291 if (unarchive) 1292 { 1293 mode = ResourceOperationMode.ADD_UNZIP; 1294 } 1295 else if (allowRename) 1296 { 1297 mode = ResourceOperationMode.ADD_RENAME; 1298 } 1299 else if (allowUpdate) 1300 { 1301 mode = ResourceOperationMode.UPDATE; 1302 } 1303 return mode; 1304 } 1305 private Map<String, Object> generateActionResult(ResourceCollection folder, ResourceOperationResult operationResult) 1306 { 1307 Map<String, Object> result = new HashMap<>(); 1308 1309 if (operationResult.isSuccess()) 1310 { 1311 List<Map<String, Object>> resourceData = operationResult.getResources() 1312 .stream() 1313 .filter(r -> r.getParent().equals(folder)) // limit to direct children 1314 .map(this::_extractFileData) 1315 .collect(Collectors.toList()); 1316 1317 result.put("resources", resourceData); 1318 result.put("unzip", operationResult.isUnzip()); 1319 } 1320 else 1321 { 1322 result.put("message", operationResult.getErrorMessage()); 1323 } 1324 1325 return result; 1326 } 1327 1328 /** 1329 * Internal method to extract the valuable data of a file 1330 * @param file The file 1331 * @return the valuable file data 1332 */ 1333 protected Map<String, Object> _extractFileData(Resource file) 1334 { 1335 Map<String, Object> data = new HashMap<>(); 1336 1337 data.put("id", file.getId()); 1338 data.put("name", file.getName()); 1339 data.put("path", _getFilePath(file)); 1340 1341 // Encode path without extension 1342 String resourcePath = file.getResourcePath(); 1343 int i = resourcePath.lastIndexOf("."); 1344 resourcePath = i != -1 ? resourcePath.substring(0, i) : resourcePath; 1345 // Encode twice 1346 String encodedPath = FilenameUtils.encodePath(resourcePath); 1347 data.put("encodedPath", URIUtils.encodeURI(encodedPath, Map.of())); 1348 1349 data.put("type", ResourceType.FILE.name().toLowerCase()); 1350 data.put("fileType", _workspaceHelper.getFileType(file).name().toLowerCase()); 1351 data.put("fileExtension", StringUtils.substringAfterLast(file.getName(), ".")); 1352 1353 AmetysObject parent = file.getParent(); 1354 if (parent != null && parent instanceof ResourceCollection) 1355 { 1356 data.put("location", ((ResourceCollection) parent).getName()); 1357 data.put("parentId", parent.getId()); 1358 data.put("parentPath", ((ResourceCollection) parent).getExplorerPath()); 1359 } 1360 1361 data.put("modifiable", file instanceof ModifiableResource); 1362 data.put("canCreateChild", file instanceof ModifiableExplorerNode); 1363 1364 data.put("description", file.getDCDescription()); 1365 data.put("tags", _tags2json(file)); 1366 data.put("mimetype", file.getMimeType()); 1367 data.put("length", String.valueOf(file.getLength())); 1368 1369 boolean image = _workspaceHelper.isImage(file); 1370 if (image) 1371 { 1372 data.put("image", true); 1373 } 1374 1375 data.put("hasOnlyOfficePreview", _onlyOfficeManager.canBePreviewed(file.getId())); 1376 1377 UserIdentity creatorIdentity = file.getCreator(); 1378 data.put("creator", _userHelper.user2json(creatorIdentity)); 1379 data.put("creationDate", DateUtils.dateToString(file.getCreationDate())); 1380 1381 UserIdentity contribIdentity = file.getLastContributor(); 1382 data.put("author", _userHelper.user2json(contribIdentity)); 1383 data.put("lastModified", DateUtils.dateToString(file.getLastModified())); 1384 1385 data.put("rights", _extractFileRightData(file)); 1386 1387 data.putAll(_extractFileLockData(file)); 1388 1389 return data; 1390 } 1391 1392 private List<Map<String, Object>> _tags2json(Resource file) 1393 { 1394 return ((TagAwareAmetysObject) file).getTags() 1395 .stream() 1396 .filter(tag -> _tagProviderExtensionPoint.hasTag(tag, Map.of())) 1397 .map(tag -> _tagProviderExtensionPoint.getTag(tag, Map.of())) 1398 .map(this::_tag2json) 1399 .collect(Collectors.toList()); 1400 } 1401 1402 private Map<String, Object> _tag2json(Tag tag) 1403 { 1404 Map<String, Object> tagMap = new HashMap<>(); 1405 tagMap.put("text", tag.getTitle()); 1406 tagMap.put("name", tag.getName()); 1407 tagMap.put("color", null); // FIXME tag color is not supported yet 1408 1409 return tagMap; 1410 } 1411 1412 private List<String> _getFilePath(Resource file) 1413 { 1414 return _getFolderPath(file.getParent()); 1415 } 1416 1417 /** 1418 * Internal method to extract the data concerning the right of the current user for file 1419 * @param file The file 1420 * @return The map of right data. Keys are the rights id, and values indicates whether the current user has the right or not. 1421 */ 1422 protected Map<String, Object> _extractFileRightData(Resource file) 1423 { 1424 Map<String, Object> rightsData = new HashMap<>(); 1425 UserIdentity user = _currentUserProvider.getUser(); 1426 ResourceCollection folder = file.getParent(); 1427 1428 // Rename - Edit 1429 rightsData.put("rename", _rightManager.hasRight(user, RIGHTS_RESOURCE_RENAME, folder) == RightResult.RIGHT_ALLOW); 1430 rightsData.put("edit", _rightManager.hasRight(user, RIGHTS_RESOURCE_EDIT_DC, folder) == RightResult.RIGHT_ALLOW); 1431 1432 // Delete 1433 rightsData.put("delete", _rightManager.hasRight(user, RIGHTS_RESOURCE_DELETE, folder) == RightResult.RIGHT_ALLOW); 1434 1435 // TODO Delete own - no ability to detect document creator currently 1436 // rightsData.put("delete-own", ...); 1437 1438 // Unlock 1439 rightsData.put("unlock", _rightManager.hasRight(user, RIGHTS_RESOURCE_UNLOCK_ALL, folder) == RightResult.RIGHT_ALLOW); 1440 1441 // Comments 1442 rightsData.put("comment", _rightManager.hasRight(user, RIGHTS_RESOURCE_COMMENT, folder) == RightResult.RIGHT_ALLOW); 1443 rightsData.put("moderate-comments", _rightManager.hasRight(user, RIGHTS_RESOURCE_MODERATE_COMMENT, folder) == RightResult.RIGHT_ALLOW); 1444 1445 return rightsData; 1446 } 1447 1448 /** 1449 * Internal method to extract the data relative to the lock state of a file 1450 * @param file The file 1451 * @return The image specific data 1452 */ 1453 protected Map<String, Object> _extractFileLockData(Resource file) 1454 { 1455 Map<String, Object> lockData = new HashMap<>(); 1456 1457 if (file instanceof LockableAmetysObject) 1458 { 1459 boolean isLocked = ((LockableAmetysObject) file).isLocked(); 1460 lockData.put("locked", isLocked); 1461 1462 if (isLocked) 1463 { 1464 UserIdentity lockOwner = ((LockableAmetysObject) file).getLockOwner(); 1465 1466 lockData.put("isLockOwner", lockOwner.equals(_currentUserProvider.getUser())); 1467 lockData.put("lockOwner", _userHelper.user2json(lockOwner)); 1468 } 1469 } 1470 1471 return lockData; 1472 } 1473 1474 private List<String> _sanitizeFileTags(Collection<String> tags) throws AmetysRepositoryException 1475 { 1476 // Enforce lowercase and remove possible duplicate tags 1477 return Optional.ofNullable(tags).orElseGet(ArrayList::new).stream() 1478 .map(String::trim) 1479 .map(String::toLowerCase) 1480 .distinct() 1481 .collect(Collectors.toList()); 1482 } 1483 1484 @Override 1485 protected void _setComment(JCRPost comment, String content) 1486 { 1487 try 1488 { 1489 _htmlTransformer.transform(content, comment.getContent()); 1490 } 1491 catch (IOException e) 1492 { 1493 throw new AmetysRepositoryException("Failed to transform comment into rich text", e); 1494 } 1495 } 1496 1497 @Override 1498 protected String _getComment(JCRPost post) throws AmetysRepositoryException 1499 { 1500 Source contentSource = null; 1501 try 1502 { 1503 Map<String, Object> parameters = new HashMap<>(); 1504 parameters.put("source", post.getContent().getInputStream()); 1505 contentSource = _sourceResolver.resolveURI("cocoon://_plugins/" + _pluginName + "/convert/html2html", null, parameters); 1506 return IOUtils.toString(contentSource.getInputStream(), "UTF-8"); 1507 } 1508 catch (IOException e) 1509 { 1510 throw new AmetysRepositoryException("Failed to transform rich text into string", e); 1511 } 1512 finally 1513 { 1514 _sourceResolver.release(contentSource); 1515 } 1516 } 1517 1518 @Override 1519 protected String _getCommentForEditing(JCRPost post) throws AmetysRepositoryException 1520 { 1521 try 1522 { 1523 StringBuilder sb = new StringBuilder(); 1524 _htmlTransformer.transformForEditing(post.getContent(), sb); 1525 return sb.toString(); 1526 } 1527 catch (IOException e) 1528 { 1529 throw new AmetysRepositoryException("Failed to transform rich text into string", e); 1530 } 1531 } 1532 1533 /** 1534 * Indicates if the current user can view the folder 1535 * @param folder The folder to test 1536 * @return true if the folder can be viewed 1537 */ 1538 protected boolean _canView(ResourceCollection folder) 1539 { 1540 return _rightManager.currentUserHasReadAccess(folder); 1541 } 1542 1543 /** 1544 * Indicates if the current user can view the file 1545 * @param file The file to test 1546 * @return true if the file can be viewed 1547 */ 1548 protected boolean _canView(Resource file) 1549 { 1550 return _rightManager.currentUserHasReadAccess(file.getParent()); 1551 } 1552 1553 @Override 1554 @Callable 1555 public Map<String, String> getCMISProperties(String id) 1556 { 1557 // override to allow calls from workspaces 1558 return super.getCMISProperties(id); 1559 } 1560 1561 @Override 1562 @Callable 1563 public Map<String, Object> addCMISCollection(String parentId, String originalName, String url, String login, String password, String repoId, boolean renameIfExists) 1564 { 1565 String rootId = parentId == null ? _getRootFromRequest().getId() : parentId; 1566 if (rootId == null) 1567 { 1568 throw new IllegalArgumentException("Unable to add CMIS collection: parent folder not found."); 1569 } 1570 return super.addCMISCollection(rootId, originalName, url, login, password, repoId, renameIfExists); 1571 } 1572 1573 /** 1574 * Edits a CMIS folder (see {@link CMISRootResourcesCollection}) 1575 * 1576 * @param id the id of CMIS folder 1577 * @param name The name of the CMIS folder 1578 * @param url The url of CMIS repository 1579 * @param login The user's login to access CMIS repository 1580 * @param password The user's password to access CMIS repository 1581 * @param repoId The id of CMIS repository 1582 * @return the result map with id of edited node 1583 * @throws RepositoryException If an error occurred 1584 */ 1585 @Callable 1586 public Map<String, Object> editCMISCollection(String id, String name, String url, String login, String password, String repoId) throws RepositoryException 1587 { 1588 List<String> errors = new LinkedList<>(); 1589 try 1590 { 1591 renameObject(id, name); 1592 } 1593 catch (RepositoryException e) 1594 { 1595 getLogger().error("Repository exception during CMIS folder edition.", e); 1596 errors.add("repository"); 1597 } 1598 1599 // override to allow calls from workspaces 1600 Map<String, Object> result = super.editCMISCollection(id, url, login, password, repoId); 1601 if (errors.size() > 0) 1602 { 1603 result.put("errors", errors); 1604 } 1605 return result; 1606 } 1607 1608 @Override 1609 @Callable 1610 public boolean isCMISCollection(String id) 1611 { 1612 // override to allow calls from workspaces 1613 return super.isCMISCollection(id); 1614 } 1615 1616 /** 1617 * Count the total of documents in the project 1618 * @param project The project 1619 * @return The total of documents, or null if the module is not activated 1620 */ 1621 public Long getDocumentsCount(Project project) 1622 { 1623 Function<Project, ResourceCollection> getModuleRoot = proj -> _moduleEP.getModule(DocumentWorkspaceModule.DOCUMENT_MODULE_ID).getModuleRoot(proj, false); 1624 return Optional.ofNullable(project) 1625 .map(getModuleRoot) 1626 .map(root -> _getChildDocumentsCount(root)) 1627 .orElse(null); 1628 } 1629 1630 private Long _getChildDocumentsCount(ResourceCollection collection) 1631 { 1632 return collection.getChildren().stream() 1633 // Count the number of documents : a document counts as 1, a folder count has the sum of its children. Anything else is ignored (0) 1634 .map(ao -> ao instanceof Resource ? 1L : ao instanceof ResourceCollection ? _getChildDocumentsCount((ResourceCollection) ao) : 0L) 1635 .reduce(0L, Long::sum); 1636 } 1637}