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