001/* 002 * Copyright 2016 Anyware Services 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.ametys.plugins.workspaces.project; 017 018import java.time.ZonedDateTime; 019import java.util.ArrayList; 020import java.util.Arrays; 021import java.util.Collection; 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.Iterator; 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; 032import java.util.stream.Stream; 033import java.util.stream.StreamSupport; 034 035import javax.jcr.Node; 036import javax.jcr.PathNotFoundException; 037import javax.jcr.Property; 038import javax.jcr.RepositoryException; 039import javax.jcr.Session; 040import javax.jcr.Value; 041 042import org.apache.avalon.framework.component.Component; 043import org.apache.avalon.framework.context.Context; 044import org.apache.avalon.framework.context.ContextException; 045import org.apache.avalon.framework.context.Contextualizable; 046import org.apache.avalon.framework.logger.AbstractLogEnabled; 047import org.apache.avalon.framework.service.ServiceException; 048import org.apache.avalon.framework.service.ServiceManager; 049import org.apache.avalon.framework.service.Serviceable; 050import org.apache.commons.collections.CollectionUtils; 051import org.apache.commons.lang.ArrayUtils; 052import org.apache.commons.lang3.StringUtils; 053 054import org.ametys.cms.FilterNameHelper; 055import org.ametys.core.observation.Event; 056import org.ametys.core.observation.ObservationManager; 057import org.ametys.core.ui.Callable; 058import org.ametys.core.user.CurrentUserProvider; 059import org.ametys.core.user.UserIdentity; 060import org.ametys.core.util.I18nUtils; 061import org.ametys.core.util.LambdaUtils; 062import org.ametys.plugins.explorer.ExplorerNode; 063import org.ametys.plugins.repository.AmetysObject; 064import org.ametys.plugins.repository.AmetysObjectIterable; 065import org.ametys.plugins.repository.AmetysObjectResolver; 066import org.ametys.plugins.repository.AmetysRepositoryException; 067import org.ametys.plugins.repository.ModifiableAmetysObject; 068import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 069import org.ametys.plugins.repository.RemovableAmetysObject; 070import org.ametys.plugins.repository.RepositoryConstants; 071import org.ametys.plugins.repository.TraversableAmetysObject; 072import org.ametys.plugins.repository.UnknownAmetysObjectException; 073import org.ametys.plugins.repository.jcr.JCRAmetysObject; 074import org.ametys.plugins.repository.jcr.JCRTraversableAmetysObject; 075import org.ametys.plugins.repository.jcr.NodeTypeHelper; 076import org.ametys.plugins.repository.query.expression.Expression; 077import org.ametys.plugins.repository.query.expression.Expression.Operator; 078import org.ametys.plugins.workspaces.ObservationConstants; 079import org.ametys.plugins.workspaces.members.ProjectMemberManager; 080import org.ametys.plugins.workspaces.project.modules.WorkspaceModule; 081import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint; 082import org.ametys.plugins.workspaces.project.objects.Project; 083import org.ametys.plugins.workspaces.project.objects.Project.InscriptionStatus; 084import org.ametys.plugins.workspaces.project.objects.ProjectCategory; 085import org.ametys.plugins.workspaces.project.rights.ProjectRightHelper; 086import org.ametys.runtime.i18n.I18nizableText; 087import org.ametys.runtime.plugin.component.PluginAware; 088import org.ametys.web.repository.page.Page; 089import org.ametys.web.repository.page.PageQueryHelper; 090import org.ametys.web.repository.site.Site; 091import org.ametys.web.repository.site.SiteDAO; 092import org.ametys.web.repository.site.SiteManager; 093import org.ametys.web.repository.sitemap.Sitemap; 094import org.ametys.web.site.SiteConfigurationExtensionPoint; 095import org.ametys.web.tags.TagExpression; 096 097import com.google.common.collect.ImmutableMap; 098import com.google.common.collect.Iterables; 099 100/** 101 * Helper component for managing project workspaces 102 */ 103public class ProjectManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable, PluginAware 104{ 105 /** Avalon Role */ 106 public static final String ROLE = ProjectManager.class.getName(); 107 108 /** Workspaces plugin node name */ 109 private static final String __WORKSPACES_PLUGIN_NODE_NAME = "workspaces"; 110 111 /** Workspaces plugin node name */ 112 private static final String __WORKSPACES_PLUGIN_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":unstructured"; 113 114 /** The name of the projects root node */ 115 private static final String __PROJECTS_ROOT_NODE_NAME = "projects"; 116 117 /** The type of the projects root node */ 118 private static final String __PROJECTS_ROOT_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":unstructured"; 119 120 /** Constants for tags metadata */ 121 private static final String __PROJECTS_TAGS_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":tags"; 122 123 /** Constants for places metadata */ 124 private static final String __PROJECTS_PLACES_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":places"; 125 126 /** Ametys object resolver */ 127 protected AmetysObjectResolver _resolver; 128 129 /** The i18n utils. */ 130 protected I18nUtils _i18nUtils; 131 132 /** Site manager */ 133 protected SiteManager _siteManager; 134 135 /** Site DAO */ 136 protected SiteDAO _siteDao; 137 138 /** Site configuration EP */ 139 protected SiteConfigurationExtensionPoint _siteConfiguration; 140 141 /** Module Managers EP */ 142 protected WorkspaceModuleExtensionPoint _moduleManagerEP; 143 144 /** Avalon context */ 145 protected Context _context; 146 147 private ObservationManager _observationManager; 148 149 private CurrentUserProvider _currentUserProvider; 150 151 private ProjectMemberManager _projectMembers; 152 153 private ProjectMemberManager _projectMemberManager; 154 155 private String _pluginName; 156 157 private ProjectRightHelper _projectRightHelper; 158 159 @Override 160 public void contextualize(Context context) throws ContextException 161 { 162 _context = context; 163 } 164 165 @Override 166 public void service(ServiceManager manager) throws ServiceException 167 { 168 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 169 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 170 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 171 _siteDao = (SiteDAO) manager.lookup(SiteDAO.ROLE); 172 _siteConfiguration = (SiteConfigurationExtensionPoint) manager.lookup(SiteConfigurationExtensionPoint.ROLE); 173 _projectMembers = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE); 174 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 175 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 176 _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE); 177 _moduleManagerEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE); 178 _projectRightHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE); 179 } 180 181 @Override 182 public void setPluginInfo(String pluginName, String featureName, String id) 183 { 184 _pluginName = pluginName; 185 } 186 187 /** 188 * Retrieves all projects 189 * @return the projects 190 */ 191 public AmetysObjectIterable<Project> getProjects() 192 { 193 String jcrQuery = "//element(*, ametys:project)"; 194 return _resolver.query(jcrQuery); 195 } 196 197 /** 198 * Retrieves a project by its name 199 * @param name The project name 200 * @return the project or <code>null</code> if not found 201 */ 202 public Project getProject(String name) 203 { 204 String jcrQuery = "//element(" + name + ", ametys:project)"; 205 AmetysObjectIterable<Project> sites = _resolver.query(jcrQuery); 206 Iterator<Project> it = sites.iterator(); 207 208 if (it.hasNext()) 209 { 210 return it.next(); 211 } 212 213 return null; 214 } 215 216 /** 217 * Get the user's projects 218 * @param user the user 219 * @return the user's projects 220 */ 221 public List<Project> getUserProjects(UserIdentity user) 222 { 223 List<Project> userProjects = new ArrayList<>(); 224 225 AmetysObjectIterable<Project> projects = getProjects(); 226 227 for (Project project : projects) 228 { 229 if (_projectMembers.isProjectMember(project, user)) 230 { 231 userProjects.add(project); 232 } 233 } 234 235 return userProjects; 236 } 237 238 239 /** 240 * Returns true if the given project exists. 241 * @param projectName the project name. 242 * @return true if the given project exists. 243 */ 244 public boolean hasProject(String projectName) 245 { 246 return getProject(projectName) != null; 247 } 248 249 /** 250 * Retrieves the mapping of all the projects name with their title on which the current user has access 251 * @return the map (projectName, projectTitle) for all projects 252 */ 253 @Callable 254 public List<Map<String, String>> getUserProjectsData() 255 { 256 return getUserProjects(_currentUserProvider.getUser()) 257 .stream() 258 .map(p -> ImmutableMap.of("title", p.getTitle(), "name", p.getName())) 259 .collect(Collectors.toList()); 260 } 261 262 /** 263 * Retrieves the mapping of all the projects name with their title (regarless user rights) 264 * @return the map (projectName, projectTitle) for all projects 265 */ 266 @Callable 267 public List<Map<String, String>> getProjectsData() 268 { 269 return getProjects() 270 .stream() 271 .map(p -> ImmutableMap.of("title", p.getTitle(), "name", p.getName())) 272 .collect(Collectors.toList()); 273 } 274 275 /** 276 * Retrieves the project names 277 * @return the project names 278 */ 279 @Callable 280 public Collection<String> getProjectNames() 281 { 282 return getProjects() 283 .stream() 284 .map(Project::getName) 285 .collect(Collectors.toList()); 286 } 287 288 /** 289 * Retrieves the project paths of all projects. 290 * @return A list of project paths 291 */ 292 @Callable 293 public Collection<String> getProjectPaths() 294 { 295 return getProjects() 296 .stream() 297 .map(Project::getProjectsTreePath) 298 .collect(Collectors.toList()); 299 } 300 301 /** 302 * Return the root for projects 303 * The root node will be created if necessary 304 * @return The root for projects 305 */ 306 public ModifiableTraversableAmetysObject getProjectsRoot() 307 { 308 try 309 { 310 ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins"); 311 ModifiableTraversableAmetysObject workspacesPluginNode = _getOrCreateObject(pluginsNode, __WORKSPACES_PLUGIN_NODE_NAME, __WORKSPACES_PLUGIN_NODE_TYPE); 312 return _getOrCreateObject(workspacesPluginNode, __PROJECTS_ROOT_NODE_NAME, __PROJECTS_ROOT_NODE_TYPE); 313 } 314 catch (AmetysRepositoryException e) 315 { 316 throw new AmetysRepositoryException("Error getting the projects root node.", e); 317 } 318 } 319 320 /** 321 * Get the tree of a project which contains information about the projects 322 * and categories in the tree. 323 * @param treeRootId The desired root node of the tree 324 * @param maxDepth The maximal depth of the tree. Set to a value of zero or 325 * less to get the whole tree. 326 * @param contextOptions An optional map of context options. Valid context 327 * options are: "user", which holds an user identity, the project 328 * tree will be filtered given that user rights. 329 * @return The project tree which is a list of map. Each map is a node of 330 * the tree structure. 331 */ 332 public List<Map<String, Object>> getProjectTree(String treeRootId, int maxDepth, Map<String, Object> contextOptions) 333 { 334 return getProjectTree(treeRootId, maxDepth, true, false, contextOptions); 335 } 336 337 /** 338 * Get the tree of a project which contains information about the projects 339 * and categories in the tree. 340 * @param treeRootId The desired root node of the tree 341 * @param maxDepth The maximal depth of the tree. Set to a value of zero or 342 * less to get the whole tree. 343 * @param includeProjects False to only return the categories tree. 344 * @param memberOnly Only return projects for which the current user is a member 345 * @param contextOptions An optional map of context options. Valid context 346 * options are: "user", which holds an user identity, the project 347 * tree will be filtered given that user rights. 348 * @return The project tree which is a list of map. Each map is a node of 349 * the tree structure. 350 */ 351 public List<Map<String, Object>> getProjectTree(String treeRootId, int maxDepth, boolean includeProjects, boolean memberOnly, Map<String, Object> contextOptions) 352 { 353 List<Map<String, Object>> projectTree = new ArrayList<>(); 354 355 // FIXME WORKSPACES RIGHTS user context options is currently ignored 356 357 TraversableAmetysObject node = null; 358 if (StringUtils.isNotEmpty(treeRootId)) 359 { 360 node = _resolver.resolveById(treeRootId); 361 } 362 else 363 { 364 node = getProjectsRoot(); 365 } 366 367 // traverse the project tree and populate the result list. 368 for (AmetysObject child: node.getChildren()) 369 { 370 _projectTreeAccumulator(projectTree, child, 1, maxDepth, includeProjects, memberOnly); 371 } 372 373 return projectTree; 374 } 375 376 private void _projectTreeAccumulator(List<Map<String, Object>> projectTree, AmetysObject node, int depth, int maxDepth, boolean includeProjects, boolean memberOnly) 377 { 378 Map<String, Object> nodeInfo = null; 379 380 if (node instanceof Project) 381 { 382 if (includeProjects && (!memberOnly || _projectMemberManager.isProjectMember((Project) node, _currentUserProvider.getUser()))) 383 { 384 nodeInfo = getProjectProperties((Project) node); 385 } 386 } 387 else if (node instanceof ProjectCategory) 388 { 389 ProjectCategory projectCategory = (ProjectCategory) node; 390 nodeInfo = getCategoryProperties(projectCategory); 391 392 if (maxDepth <= 0 || depth < maxDepth) 393 { 394 // recurse through the children of the category. 395 List<Map<String, Object>> children = new ArrayList<>(); 396 nodeInfo.put("children", children); 397 398 for (AmetysObject child: projectCategory.getChildren()) 399 { 400 _projectTreeAccumulator(children, child, depth + 1, maxDepth, includeProjects, memberOnly); 401 } 402 } 403 } 404 else 405 { 406 // logging unexpected ametys object 407 String warningMsg = String.format("Unexpected ametys object type while traversing the project tree.\nAmetys object identifier is '%s'.", node.getId()); 408 getLogger().warn(warningMsg); 409 } 410 411 if (nodeInfo != null) 412 { 413 projectTree.add(nodeInfo); 414 } 415 } 416 417 /** 418 * Retrieves the standard information of a project 419 * @param projectId Identifier of the project 420 * @return The map of information 421 */ 422 @Callable 423 public Map<String, Object> getProjectProperties(String projectId) 424 { 425 return getProjectProperties((Project) _resolver.resolveById(projectId)); 426 } 427 428 /** 429 * Retrieves the standard information of a project 430 * @param project The project 431 * @return The map of information 432 */ 433 public Map<String, Object> getProjectProperties(Project project) 434 { 435 Map<String, Object> info = new HashMap<>(); 436 437 info.put("id", project.getId()); 438 info.put("name", project.getName()); 439 info.put("type", "project"); 440 info.put("path", project.getProjectsTreePath()); 441 442 AmetysObject parent = project.getParent(); 443 if (parent instanceof ProjectCategory) 444 { 445 info.put("parentId", parent.getId()); 446 } 447 448 info.put("title", project.getTitle()); 449 info.put("description", project.getDescription()); 450 info.put("mailingList", project.getMailingList()); 451 info.put("inscriptionStatus", project.getInscriptionStatus().toString()); 452 info.put("defaultProfile", project.getDefaultProfile()); 453 454 info.put("creationDate", project.getCreationDate()); 455 456 // check if the project workspace configuration is valid 457 Collection<Site> sites = project.getSites(); 458 boolean valid = sites.size() > 0; 459 if (valid) 460 { 461 Iterator<Site> siteIterator = sites.iterator(); 462 while (valid && siteIterator.hasNext()) 463 { 464 Site site = siteIterator.next(); 465 valid = _siteConfiguration.isValid(site.getName()); 466 } 467 } 468 469 info.put("valid", valid); 470 471 // sites is a list of map entry with id ,name, title and url property 472 // { id: site id, name: site name, title: site title, url: site url } 473 info.put("sites", sites.stream().map(site -> 474 { 475 Map<String, String> siteProps = new HashMap<>(); 476 siteProps.put("id", site.getId()); 477 siteProps.put("name", site.getName()); 478 siteProps.put("title", site.getTitle()); 479 siteProps.put("url", site.getUrl()); 480 return siteProps; 481 }).collect(Collectors.toList())); 482 483 return info; 484 } 485 486 /** 487 * Get the availables project URLs. 488 * @param project The project 489 * @return The availables project URLs, can be empty. 490 */ 491 public Set<String> getProjectUrls(Project project) 492 { 493 return _getProjectNonEmptyElements(project, Site::getUrl); 494 } 495 496 /** 497 * Get the availables project names. 498 * @param project The project 499 * @return The availables project names, can be empty. 500 */ 501 public Set<String> getProjectNames(Project project) 502 { 503 return _getProjectNonEmptyElements(project, Site::getName); 504 } 505 506 private Set<String> _getProjectNonEmptyElements(Project project, Function<? super Site, ? extends String> function) 507 { 508 return project.getSites() // Get the sites of the project 509 .stream() // Build it as a stream 510 .map(function) // Get the element of each site 511 .filter(StringUtils::isNotEmpty) // Filter empty strings 512 .collect(Collectors.toSet()); // Get only the first value 513 } 514 515 /** 516 * Get the tree of a project. 517 * @param treeRootId The desired root node of the tree 518 * @param maxDepth The maximal depth of the tree. Set to a value of zero or 519 * less to get the whole tree. 520 * @return The project tree which is a list of projects and categories. 521 * Each category is a map with the category node and a list of 522 * children. 523 */ 524 public List<Object> getProjectTreeNodes(String treeRootId, int maxDepth) 525 { 526 return getProjectTreeNodes(treeRootId, maxDepth, true, false); 527 } 528 529 /** 530 * Get the tree of a project. 531 * @param treeRootId The desired root node of the tree 532 * @param maxDepth The maximal depth of the tree. Set to a value of zero or 533 * less to get the whole tree. 534 * @param includeProjects False to only return the categories tree. 535 * @param memberOnly Only return projects for which the current user is a member 536 * @return The project tree which is a list of projects and categories. 537 * Each category is a map with the category node and a list of 538 * children. 539 */ 540 public List<Object> getProjectTreeNodes(String treeRootId, int maxDepth, boolean includeProjects, boolean memberOnly) 541 { 542 return getProjectTreeNodes(treeRootId, maxDepth, 0, includeProjects, memberOnly, null); 543 } 544 545 /** 546 * Get the tree of a project. 547 * @param treeRootId The desired root node of the tree 548 * @param maxDepth The maximal depth of the tree. Set to a value of zero or 549 * less to get the whole tree. 550 * @param maxResult Limit the number of projects returned 551 * @param includeProjects False to only return the categories tree. 552 * @param memberOnly Only return projects for which the current user is a member 553 * @param filterCategories The list of categories to filter. Can be null to ignore 554 * @return The project tree which is a list of projects and categories. 555 * Each category is a map with the category node and a list of 556 * children. 557 */ 558 public List<Object> getProjectTreeNodes(String treeRootId, int maxDepth, int maxResult, boolean includeProjects, boolean memberOnly, List<String> filterCategories) 559 { 560 List<Object> projectTree = new ArrayList<>(); 561 562 TraversableAmetysObject node = null; 563 if (StringUtils.isNotEmpty(treeRootId)) 564 { 565 node = _resolver.resolveById(treeRootId); 566 } 567 else 568 { 569 node = getProjectsRoot(); 570 } 571 572 // traverse the project tree and populate the result list. 573 for (AmetysObject child: node.getChildren()) 574 { 575 _projectTreeNodeAccumulator(projectTree, child, 1, maxDepth, includeProjects, memberOnly); 576 } 577 578 if (filterCategories != null || maxResult != 0) 579 { 580 Map<String, Object> filters = new HashMap<>(); 581 filters.put("categories", filterCategories); 582 filters.put("max", maxResult <= 0 ? null : new Integer(maxResult)); 583 projectTree = _filterProjectTree(projectTree, filters, StringUtils.isNotEmpty(treeRootId) ? treeRootId : "category-root", 1, maxDepth); 584 } 585 586 return Optional.ofNullable(projectTree).orElse(new ArrayList<>()); 587 } 588 589 private void _projectTreeNodeAccumulator(List<Object> projectTree, AmetysObject node, int depth, int maxDepth, boolean includeProjects, boolean memberOnly) 590 { 591 if (node instanceof Project) 592 { 593 Project project = (Project) node; 594 if (includeProjects && (_projectMemberManager.isProjectMember(project, _currentUserProvider.getUser())) || (!memberOnly && !project.getInscriptionStatus().equals(InscriptionStatus.PRIVATE))) 595 { 596 projectTree.add(node); 597 } 598 } 599 else if (node instanceof ProjectCategory) 600 { 601 ProjectCategory projectCategory = (ProjectCategory) node; 602 Map<String, Object> categoryData = new HashMap<>(); 603 categoryData.put("category", projectCategory); 604 605 if (maxDepth <= 0 || depth < maxDepth) 606 { 607 // recurse through the children of the category. 608 List<Object> children = new ArrayList<>(); 609 for (AmetysObject child: projectCategory.getChildren()) 610 { 611 _projectTreeNodeAccumulator(children, child, depth + 1, maxDepth, includeProjects, memberOnly); 612 } 613 categoryData.put("children", children); 614 } 615 616 projectTree.add(categoryData); 617 } 618 else 619 { 620 // logging unexpected ametys object 621 String warningMsg = String.format("Unexpected ametys object type while traversing the project tree.\nAmetys object identifier is '%s'.", node.getId()); 622 getLogger().warn(warningMsg); 623 } 624 } 625 626 @SuppressWarnings("unchecked") 627 private List<Object> _filterProjectTree(List<Object> projectTree, Map<String, Object> filters, String categoryId, int depth, int maxDepth) 628 { 629 List<String> filterCategories = (List<String>) filters.get("categories"); 630 boolean isCategoryInFilters = filterCategories == null ? true : filterCategories.contains(categoryId); 631 632 // filtered project tree stays null unless a sub-category is in the filterCategories list 633 List<Object> filteredProjectTree = isCategoryInFilters ? new ArrayList<>() : null; 634 635 for (Object treeNode : projectTree) 636 { 637 if (isCategoryInFilters && treeNode instanceof Project && filteredProjectTree != null && (filters.get("max") == null || ((Integer) filters.get("max")) > 0)) 638 { 639 filteredProjectTree.add(treeNode); 640 if (filters.get("max") != null) 641 { 642 filters.put("max", ((Integer) filters.get("max")) - 1); 643 } 644 } 645 else if (treeNode instanceof Map) 646 { 647 Map<String, Object> categoryData = (Map<String, Object>) treeNode; 648 filteredProjectTree = _filterProjectTreeCategory(categoryData, filteredProjectTree, filters, isCategoryInFilters, depth, maxDepth); 649 } 650 } 651 652 return filteredProjectTree; 653 } 654 655 @SuppressWarnings("unchecked") 656 private List<Object> _filterProjectTreeCategory(Map<String, Object> categoryData, List<Object> filteredProjectTree, Map<String, Object> filters, boolean isCategoryInFilters, int depth, int maxDepth) 657 { 658 List<Object> resultProjectTree = filteredProjectTree; 659 660 boolean addCategoryToTree = isCategoryInFilters; 661 662 if (maxDepth > 0 && depth >= maxDepth && !addCategoryToTree) 663 { 664 // resolve and parse the category children to find at least one children not filtered 665 List<String> filterCategories = (List<String>) filters.get("categories"); 666 String categoryId = ((ProjectCategory) categoryData.get("category")).getId(); 667 addCategoryToTree = isCategoryInFilters(categoryId, filterCategories, false); 668 } 669 670 if (categoryData.containsKey("children")) 671 { 672 // will return null if no child passes the filters, otherwise will return the list of filtered children 673 List<Object> categoryChildren = _filterProjectTree((List<Object>) categoryData.get("children"), filters, ((ProjectCategory) categoryData.get("category")).getId(), depth + 1, maxDepth); 674 675 if (categoryChildren != null) 676 { 677 addCategoryToTree = true; 678 } 679 categoryData.put("children", categoryChildren); 680 } 681 682 if (addCategoryToTree) 683 { 684 if (resultProjectTree == null) 685 { 686 resultProjectTree = new ArrayList<>(); 687 } 688 resultProjectTree.add(categoryData); 689 } 690 return resultProjectTree; 691 } 692 693 /** 694 * Test if a category child is contained in the list of filters 695 * @param categoryId The category id 696 * @param filterCategories The list of category id that one of the children must match 697 * @param includeProjects True to test filters on all category children, false to only test on sub-categories 698 * @return True if the child is in the filters 699 */ 700 public boolean isCategoryInFilters(String categoryId, List<String> filterCategories, boolean includeProjects) 701 { 702 if (filterCategories.contains(categoryId)) 703 { 704 return true; 705 } 706 707 AmetysObject object = _resolver.resolveById(categoryId); 708 709 if (object instanceof ProjectCategory) 710 { 711 712 for (AmetysObject categoryChild : ((ProjectCategory) object).getChildren()) 713 { 714 if (includeProjects || categoryChild instanceof ProjectCategory) 715 { 716 if (isCategoryInFilters(categoryChild.getId(), filterCategories, includeProjects)) 717 { 718 return true; 719 } 720 } 721 } 722 } 723 return false; 724 } 725 726 /** 727 * Create a project from the projects root node. 728 * @param name The project name 729 * @param title The project title 730 * @param description The project description 731 * @param mailingList Project mailing list 732 * @param inscriptionStatus The inscription status of the project 733 * @param defaultProfileId The default profile for new members 734 * @return A map containing the id of the new project or an error key. 735 */ 736 @Callable 737 public Map<String, Object> createProject(String name, String title, String description, String mailingList, String inscriptionStatus, String defaultProfileId) 738 { 739 return createProject((String) null, name, title, description, mailingList, inscriptionStatus, defaultProfileId); 740 } 741 742 /** 743 * Create a project 744 * @param parentId Identifier of the parent of the project to create 745 * @param name The project name 746 * @param title The project title 747 * @param description The project description 748 * @param mailingList Project mailing list 749 * @param inscriptionStatus The inscription status of the project 750 * @param defaultProfileId The default profile for new members 751 * @return A map containing the id of the new project or an error key. 752 */ 753 @Callable 754 public Map<String, Object> createProject(String parentId, String name, String title, String description, String mailingList, String inscriptionStatus, String defaultProfileId) 755 { 756 Map<String, Object> result = new HashMap<>(); 757 List<String> errors = new ArrayList<>(); 758 759 Map<String, Object> additionalValues = new HashMap<>(); 760 additionalValues.put("description", description); 761 additionalValues.put("mailingList", mailingList); 762 additionalValues.put("inscriptionStatus", inscriptionStatus); 763 additionalValues.put("defaultProfileId", defaultProfileId); 764 765 String projectId = createProject(parentId, name, title, additionalValues, null, errors); 766 767 if (CollectionUtils.isEmpty(errors)) 768 { 769 result.put("id", projectId); 770 } 771 else 772 { 773 result.put("error", errors.get(0)); 774 } 775 776 return result; 777 } 778 779 /** 780 * Create a project 781 * @param parentId Identifier of the parent of the project to create 782 * @param name The project name 783 * @param title The project title 784 * @param additionalValues A list of optional additional values. Accepted values are : description, mailingList, inscriptionStatus, defaultProfile, tags, categoryTags and keywords 785 * @param modulesIds The list of modules to activate. Can be null to activate all modules 786 * @param errors A list that will be populated with the encountered errors. If null, errors will not be tracked. 787 * @return The id of the new project 788 */ 789 public String createProject(String parentId, String name, String title, Map<String, Object> additionalValues, Set<String> modulesIds, List<String> errors) 790 { 791 ModifiableTraversableAmetysObject parent = null; 792 if (StringUtils.isNotEmpty(parentId)) 793 { 794 parent = _resolver.resolveById(parentId); 795 } 796 else 797 { 798 parent = getProjectsRoot(); 799 } 800 801 Project project = createProject(parent, name, title, additionalValues, modulesIds, errors); 802 803 return project != null ? project.getId() : null; 804 } 805 806 /** 807 * Create a project 808 * @param parent the parent of the project to create 809 * @param name The project name 810 * @param title The project title 811 * @param additionalValues A list of optional additional values. Accepted values are : description, mailingList, inscriptionStatus, defaultProfile, tags, categoryTags and keywords 812 * @param modulesIds The list of modules to activate. Can be null to activate all modules 813 * @param errors A list that will be populated with the encountered errors. If null, errors will not be tracked. 814 * @return The id of the new project 815 */ 816 public Project createProject(ModifiableTraversableAmetysObject parent, String name, String title, Map<String, Object> additionalValues, Set<String> modulesIds, List<String> errors) 817 { 818 if (StringUtils.isEmpty(title)) 819 { 820 throw new IllegalArgumentException(String.format("Cannot create the project for parent id '%s'. Title metadata is mandatory", parent.getId())); 821 } 822 823 // Project name should be unique 824 if (hasProject(name)) 825 { 826 if (getLogger().isWarnEnabled()) 827 { 828 getLogger().warn(String.format("A project with the name '%s' already exists", name)); 829 } 830 831 if (errors != null) 832 { 833 errors.add("project-exists"); 834 } 835 836 return null; 837 } 838 839 Project project = parent.createChild(name, Project.NODE_TYPE); 840 project.setTitle(title); 841 String description = (String) additionalValues.getOrDefault("description", null); 842 if (StringUtils.isNotEmpty(description)) 843 { 844 project.setDescription(description); 845 } 846 String mailingList = (String) additionalValues.getOrDefault("emailList", null); 847 if (StringUtils.isNotEmpty(mailingList)) 848 { 849 project.setMailingList(mailingList); 850 } 851 String inscriptionStatus = (String) additionalValues.getOrDefault("inscriptionStatus", null); 852 if (StringUtils.isNotEmpty(inscriptionStatus)) 853 { 854 project.setInscriptionStatus(inscriptionStatus); 855 } 856 String defaultProfile = (String) additionalValues.getOrDefault("defaultProfile", null); 857 if (StringUtils.isNotEmpty(defaultProfile)) 858 { 859 project.setDefaultProfile(defaultProfile); 860 } 861 862 project.setCreationDate(ZonedDateTime.now()); 863 864 // Create the project workspace = a site + a set of pages 865 _createProjectWorkspace(project, errors); 866 867 activateModules(project, modulesIds); 868 869 if (CollectionUtils.isEmpty(errors)) 870 { 871 project.saveChanges(); 872 873 // Notify observers 874 Map<String, Object> eventParams = new HashMap<>(); 875 eventParams.put(ObservationConstants.ARGS_PROJECT, project); 876 _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_ADDED, _currentUserProvider.getUser(), eventParams)); 877 878 } 879 else 880 { 881 deleteProject(project); 882 } 883 884 return project; 885 } 886 887 /** 888 * Edit a project 889 * @param id The project identifier 890 * @param title The title to set 891 * @param description The description to set 892 * @param mailingList Project mailing list 893 * @param inscriptionStatus The inscription status of the project 894 * @param defaultProfile The default profile for new members 895 */ 896 @Callable 897 public void editProject(String id, String title, String description, String mailingList, String inscriptionStatus, String defaultProfile) 898 { 899 Project project = _resolver.resolveById(id); 900 editProject(project, title, description, mailingList, inscriptionStatus, defaultProfile); 901 } 902 903 /** 904 * Edit a project 905 * @param project The project 906 * @param title The title to set 907 * @param description The description to set 908 * @param mailingList Project mailing list 909 * @param inscriptionStatus The inscription status of the project 910 * @param defaultProfile The default profile for new members 911 */ 912 public void editProject(Project project, String title, String description, String mailingList, String inscriptionStatus, String defaultProfile) 913 { 914 project.setTitle(title); 915 916 if (StringUtils.isNotEmpty(description)) 917 { 918 project.setDescription(description); 919 } 920 else 921 { 922 project.removeDescription(); 923 } 924 925 if (StringUtils.isNotEmpty(mailingList)) 926 { 927 project.setMailingList(mailingList); 928 } 929 else 930 { 931 project.removeMailingList(); 932 } 933 934 project.setInscriptionStatus(inscriptionStatus); 935 project.setDefaultProfile(defaultProfile); 936 937 project.saveChanges(); 938 939 // Notify observers 940 Map<String, Object> eventParams = new HashMap<>(); 941 eventParams.put(ObservationConstants.ARGS_PROJECT, project); 942 _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_UPDATED, _currentUserProvider.getUser(), eventParams)); 943 } 944 945 /** 946 * Delete a list of project. 947 * @param ids The ids of projects to delete 948 * @return The ids of the deleted projects, unknowns projects and the deleted sites 949 */ 950 @Callable 951 public Map<String, Object> deleteProjectsByIds(List<String> ids) 952 { 953 Map<String, Object> result = new HashMap<>(); 954 List<Map<String, Object>> deleted = new ArrayList<>(); 955 List<String> unknowns = new ArrayList<>(); 956 957 for (String id : ids) 958 { 959 try 960 { 961 Project project = _resolver.resolveById(id); 962 963 Map<String, Object> projectInfo = new HashMap<>(); 964 projectInfo.put("id", id); 965 projectInfo.put("title", project.getTitle()); 966 projectInfo.put("sites", deleteProject(project)); 967 968 deleted.add(projectInfo); 969 } 970 catch (UnknownAmetysObjectException e) 971 { 972 getLogger().warn(String.format("Unable to delete the definition of id '%s', because it does not exist.", id), e); 973 unknowns.add(id); 974 } 975 } 976 977 result.put("deleted", deleted); 978 result.put("unknowns", unknowns); 979 980 return result; 981 } 982 983 /** 984 * Delete a project. 985 * @param projects The list of projects to delete 986 * @return list of deleted sites (each list entry contains a data map with 987 * the id and the name of the delete site). 988 */ 989 public List<Map<String, String>> deleteProject(List<Project> projects) 990 { 991 List<Map<String, String>> deletedSitesInfo = new ArrayList<>(); 992 993 for (Project project : projects) 994 { 995 deletedSitesInfo.addAll(deleteProject(project)); 996 } 997 998 return deletedSitesInfo; 999 } 1000 1001 /** 1002 * Delete a project and its sites 1003 * @param project The project to delete 1004 * @return list of deleted sites (each list entry contains a data map with 1005 * the id and the name of the delete site). 1006 */ 1007 public List<Map<String, String>> deleteProject(Project project) 1008 { 1009 ModifiableAmetysObject parent = project.getParent(); 1010 1011 Collection<Site> sites = project.getSites(); 1012 1013 // list of map entry with id, name and title property 1014 // { id: site id, name: site name } 1015 List<Map<String, String>> deletedSitesInfo = new ArrayList<>(); 1016 1017 sites.forEach(site -> 1018 { 1019 try 1020 { 1021 Map<String, String> siteProps = new HashMap<>(); 1022 siteProps.put("id", site.getId()); 1023 siteProps.put("name", site.getName()); 1024 1025 _siteDao.deleteSite(site.getId()); 1026 deletedSitesInfo.add(siteProps); 1027 } 1028 catch (RepositoryException e) 1029 { 1030 String errorMsg = String.format("Error while trying to delete the site '%s' for the project '%s'.", site.getName(), project.getName()); 1031 getLogger().error(errorMsg, e); 1032 } 1033 }); 1034 1035 String projectId = project.getId(); 1036 project.remove(); 1037 parent.saveChanges(); 1038 1039 // Notify observers 1040 Map<String, Object> eventParams = new HashMap<>(); 1041 eventParams.put(ObservationConstants.ARGS_PROJECT, project); 1042 eventParams.put(ObservationConstants.ARGS_PROJECT_ID, projectId); 1043 _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_DELETED, _currentUserProvider.getUser(), eventParams)); 1044 1045 return deletedSitesInfo; 1046 } 1047 1048 /** 1049 * Retrieves the standard information of a project category 1050 * @param categoryId Identifier of the project category 1051 * @return The map of information 1052 */ 1053 @Callable 1054 public Map<String, Object> getCategoryProperties(String categoryId) 1055 { 1056 return getCategoryProperties((ProjectCategory) _resolver.resolveById(categoryId)); 1057 } 1058 1059 /** 1060 * Retrieves the standard information of a project category 1061 * @param projectCategory The project category 1062 * @return The map of information 1063 */ 1064 public Map<String, Object> getCategoryProperties(ProjectCategory projectCategory) 1065 { 1066 Map<String, Object> info = new HashMap<>(); 1067 1068 info.put("id", projectCategory.getId()); 1069 info.put("name", projectCategory.getName()); 1070 info.put("type", "category"); 1071 info.put("path", projectCategory.getProjectsTreePath()); 1072 1073 info.put("title", projectCategory.getTitle()); 1074 info.put("description", projectCategory.getDescription()); 1075 1076 return info; 1077 } 1078 1079 /** 1080 * Create a project category from the projects root node. 1081 * @param title The category title 1082 * @param description The category description 1083 * @return The id of the new category 1084 */ 1085 @Callable 1086 public String createCategory(String title, String description) 1087 { 1088 return createCategory((String) null, title, description); 1089 } 1090 1091 /** 1092 * Create a project category 1093 * @param parentId Identifier of the parent of the category to create 1094 * @param title The category title 1095 * @param description The category description 1096 * @return The id of the new category 1097 */ 1098 @Callable 1099 public String createCategory(String parentId, String title, String description) 1100 { 1101 ModifiableTraversableAmetysObject parent = null; 1102 if (StringUtils.isNotEmpty(parentId)) 1103 { 1104 parent = _resolver.resolveById(parentId); 1105 } 1106 else 1107 { 1108 parent = getProjectsRoot(); 1109 } 1110 1111 ProjectCategory category = createCategory(parent, title, description); 1112 1113 return category.getId(); 1114 } 1115 1116 /** 1117 * Create a project category 1118 * @param parent the parent of the category to create 1119 * @param title The category title 1120 * @param description The category description 1121 * @return The id of the new category 1122 */ 1123 public ProjectCategory createCategory(ModifiableTraversableAmetysObject parent, String title, String description) 1124 { 1125 if (StringUtils.isEmpty(title)) 1126 { 1127 throw new IllegalArgumentException(String.format("Cannot create the project category for parent id '%s'. Title metadata is mandatory", parent.getId())); 1128 } 1129 1130 // Find unique name 1131 String originalName = FilterNameHelper.filterName(title); 1132 String name = originalName; 1133 int index = 2; 1134 while (parent.hasChild(name)) 1135 { 1136 name = originalName + "-" + (index++); 1137 } 1138 1139 ProjectCategory category = parent.createChild(name, ProjectCategory.NODE_TYPE); 1140 category.setTitle(title); 1141 if (StringUtils.isNotEmpty(description)) 1142 { 1143 category.setDescription(description); 1144 } 1145 1146 parent.saveChanges(); 1147 1148 return category; 1149 } 1150 1151 /** 1152 * Edit a project category 1153 * @param id The category identifier 1154 * @param title The title to set 1155 * @param description The description to set 1156 */ 1157 @Callable 1158 public void editCategory(String id, String title, String description) 1159 { 1160 ProjectCategory category = _resolver.resolveById(id); 1161 editCategory(category, title, description); 1162 } 1163 1164 /** 1165 * Edit a project category 1166 * @param category The project category 1167 * @param title The title to set 1168 * @param description The description to set 1169 */ 1170 public void editCategory(ProjectCategory category, String title, String description) 1171 { 1172 category.setTitle(title); 1173 if (StringUtils.isNotEmpty(description)) 1174 { 1175 category.setDescription(description); 1176 } 1177 1178 category.saveChanges(); 1179 } 1180 1181 /** 1182 * Delete a list of project categories. 1183 * @param ids The ids of categories to delete 1184 * @return The ids of the deleted categories, unknowns categories and the deleted sites 1185 */ 1186 @Callable 1187 public Map<String, Object> deleteCategoryByIds(List<String> ids) 1188 { 1189 Map<String, Object> result = new HashMap<>(); 1190 List<String> deleted = new ArrayList<>(); 1191 List<String> unknowns = new ArrayList<>(); 1192 1193 List<Map<String, String>> deletedSitesInfo = new ArrayList<>(); 1194 1195 for (String id : ids) 1196 { 1197 try 1198 { 1199 ProjectCategory projectCategory = _resolver.resolveById(id); 1200 deletedSitesInfo.addAll(deleteCategory(projectCategory)); 1201 deleted.add(id); 1202 } 1203 catch (UnknownAmetysObjectException e) 1204 { 1205 getLogger().warn(String.format("Unable to delete the definition of id '%s', because it does not exist.", id), e); 1206 unknowns.add(id); 1207 } 1208 } 1209 1210 result.put("deleted", deleted); 1211 result.put("unknowns", unknowns); 1212 result.put("deletedSites", deletedSitesInfo); 1213 1214 return result; 1215 } 1216 1217 /** 1218 * Delete a list of project categories. 1219 * @param categories The list of categories to delete 1220 * @return list of deleted sites (each list entry contains a data map with 1221 * the id and the name of the delete site). 1222 */ 1223 public List<Map<String, String>> deleteCategory(List<ProjectCategory> categories) 1224 { 1225 List<Map<String, String>> deletedSitesInfo = new ArrayList<>(); 1226 1227 for (ProjectCategory category : categories) 1228 { 1229 deletedSitesInfo.addAll(deleteCategory(category)); 1230 } 1231 1232 return deletedSitesInfo; 1233 } 1234 1235 /** 1236 * Delete a list of project category 1237 * @param category The category to delete 1238 * @return list of deleted sites (each list entry contains a data map with 1239 * the id and the name of the delete site). 1240 */ 1241 public List<Map<String, String>> deleteCategory(ProjectCategory category) 1242 { 1243 ModifiableAmetysObject parent = category.getParent(); 1244 1245 List<Map<String, String>> deletedSitesInfo = new ArrayList<>(); 1246 1247 _removeDescendants(category, deletedSitesInfo); 1248 category.remove(); 1249 1250 parent.saveChanges(); 1251 1252 return deletedSitesInfo; 1253 } 1254 1255 /** 1256 * Remove descendant ametys objects. 1257 * Descendants should be a project or a category 1258 * @param object The project or category for which descendants must be removed 1259 * @param deletedSitesInfo list of deleted sites to be populated (each list entry contains a data map with 1260 * the id and the name of the delete site). 1261 */ 1262 private void _removeDescendants(AmetysObject object, List<Map<String, String>> deletedSitesInfo) 1263 { 1264 if (object instanceof TraversableAmetysObject) 1265 { 1266 for (AmetysObject child : ((TraversableAmetysObject) object).getChildren()) 1267 { 1268 // remove descendants of child 1269 _removeDescendants(child, deletedSitesInfo); 1270 1271 // then remove child itself 1272 if (child instanceof Project) 1273 { 1274 deletedSitesInfo.addAll(deleteProject((Project) child)); 1275 } 1276 else if (child instanceof RemovableAmetysObject) 1277 { 1278 ((RemovableAmetysObject) child).remove(); 1279 } 1280 } 1281 } 1282 } 1283 1284 /** 1285 * Move a node of the project tree into another node. 1286 * Node can be a project or a category 1287 * @param nodeId The identifier of the node to move 1288 * @param targetId Identifier of the category which is the target of the move operation. Can be null if the target is the projects root node. 1289 * @return A map containing a possible error. 1290 * @throws RepositoryException if a repository error occurs. 1291 */ 1292 @Callable 1293 public Map<String, Object> moveProjectTreeNode(String nodeId, String targetId) throws RepositoryException 1294 { 1295 JCRAmetysObject objectNode = (JCRAmetysObject) _resolver.resolveById(nodeId); 1296 1297 JCRTraversableAmetysObject targetNode = null; 1298 if (StringUtils.isNotEmpty(targetId)) 1299 { 1300 targetNode = _resolver.resolveById(targetId); 1301 } 1302 1303 return moveProjectTreeNodeObject(objectNode, targetNode); 1304 } 1305 1306 /** 1307 * Move a node of the project tree into another node. 1308 * Node can be a project or a category 1309 * @param objectNode The node to move (must be a project or a category) 1310 * @param targetNode The category which is the target of the move operation. Can be null if the target is the projects root node. 1311 * @return A map containing a possible error. 1312 * @throws RepositoryException if a repository error occurs. 1313 */ 1314 public Map<String, Object> moveProjectTreeNodeObject(JCRAmetysObject objectNode, JCRTraversableAmetysObject targetNode) throws RepositoryException 1315 { 1316 Map<String, Object> result = new HashMap<>(); 1317 1318 if (!(objectNode instanceof Project || objectNode instanceof ProjectCategory) || !(targetNode == null || targetNode instanceof ProjectCategory)) 1319 { 1320 if (getLogger().isWarnEnabled()) 1321 { 1322 String warnMsg = String.format("The object '%s' cannot be moved to '%s'. Object and/or target of the move operation are of wrong type", 1323 objectNode.getName(), targetNode == null ? "projects root" : targetNode.getName()); 1324 getLogger().warn(warnMsg); 1325 } 1326 1327 result.put("error", "invalid"); 1328 } 1329 1330 JCRTraversableAmetysObject actualTargetNode = targetNode; 1331 if (actualTargetNode == null) 1332 { 1333 actualTargetNode = (JCRTraversableAmetysObject) getProjectsRoot(); 1334 } 1335 1336 if (actualTargetNode.hasChild(objectNode.getName())) 1337 { 1338 if (getLogger().isWarnEnabled()) 1339 { 1340 String warnMsg = String.format("The object '%s' cannot be moved. An object with the same name already exists in the target collection.", objectNode.getName()); 1341 getLogger().warn(warnMsg); 1342 } 1343 1344 result.put("error", "already-exists"); 1345 } 1346 else 1347 { 1348 Session session = objectNode.getNode().getSession(); 1349 session.move(objectNode.getNode().getPath(), actualTargetNode.getNode().getPath() + "/" + objectNode.getNode().getName()); 1350 session.save(); 1351 } 1352 1353 return result; 1354 } 1355 1356 /** 1357 * Utility method to get or create an ametys object 1358 * @param <A> A sub class of AmetysObject 1359 * @param parent The parent object 1360 * @param name The ametys object name 1361 * @param type The ametys object type 1362 * @return ametys object 1363 * @throws AmetysRepositoryException if an repository error occurs 1364 */ 1365 private <A extends AmetysObject> A _getOrCreateObject(ModifiableTraversableAmetysObject parent, String name, String type) throws AmetysRepositoryException 1366 { 1367 A object; 1368 1369 if (parent.hasChild(name)) 1370 { 1371 object = parent.getChild(name); 1372 } 1373 else 1374 { 1375 object = parent.createChild(name, type); 1376 parent.saveChanges(); 1377 } 1378 1379 return object; 1380 } 1381 1382 /** 1383 * Get the project of an ametys object inside a project. 1384 * It can be an explorer node, or any type of resource in a module. 1385 * @param id The identifier of the ametys object 1386 * @return the project or null if not found 1387 */ 1388 public Project getParentProject(String id) 1389 { 1390 return getParentProject(_resolver.<AmetysObject>resolveById(id)); 1391 } 1392 1393 /** 1394 * Get the project of an ametys object inside a project. 1395 * It can be an explorer node, or any type of resource in a module. 1396 * @param object The ametys object 1397 * @return the project or null if not found 1398 */ 1399 public Project getParentProject(AmetysObject object) 1400 { 1401 AmetysObject ametysObject = object; 1402 // Go back to the local explorer root. 1403 do 1404 { 1405 ametysObject = ametysObject.getParent(); 1406 } 1407 while (ametysObject instanceof ExplorerNode); 1408 1409 if (!(ametysObject instanceof Project)) 1410 { 1411 getLogger().warn(String.format("No project found for ametys object with id '%s'", ametysObject.getId())); 1412 return null; 1413 } 1414 1415 return (Project) ametysObject; 1416 } 1417 1418 /** 1419 * Get the list of project names for a given site 1420 * @param siteName The site name 1421 * @return the list of project names 1422 */ 1423 @Callable 1424 public List<String> getProjectsForSite(String siteName) 1425 { 1426 List<String> projectNames = new ArrayList<>(); 1427 1428 if (_siteManager.hasSite(siteName)) 1429 { 1430 Site site = _siteManager.getSite(siteName); 1431 getProjectsForSite(site) 1432 .stream() 1433 .map(Project::getName) 1434 .forEach(projectNames::add); 1435 } 1436 1437 return projectNames; 1438 } 1439 1440 /** 1441 * Get the list of project for a given site 1442 * @param site The site 1443 * @return the list of project 1444 */ 1445 public List<Project> getProjectsForSite(Site site) 1446 { 1447 try 1448 { 1449 // Stream over the weak reference properties pointing to this 1450 // node to find referencing projects 1451 Iterator<Property> propertyIterator = site.getNode().getWeakReferences(); 1452 Iterable<Property> propertyIterable = () -> propertyIterator; 1453 1454 return StreamSupport.stream(propertyIterable.spliterator(), false) 1455 .map(p -> 1456 { 1457 try 1458 { 1459 // Parent should be a composite with name "ametys:sites" 1460 Node parent = p.getParent(); 1461 if (NodeTypeHelper.isNodeType(parent, "ametys:compositeMetadata") && (RepositoryConstants.NAMESPACE_PREFIX + ":" + Project.DATA_SITES).equals(parent.getName())) 1462 { 1463 // Parent should be the project 1464 parent = parent.getParent(); 1465 if (NodeTypeHelper.isNodeType(parent, "ametys:project")) 1466 { 1467 Project project = _resolver.resolve(parent, false); 1468 return project; 1469 } 1470 } 1471 } 1472 catch (Exception e) 1473 { 1474 if (getLogger().isWarnEnabled()) 1475 { 1476 // this weak reference is not from a project 1477 String propertyPath = null; 1478 try 1479 { 1480 propertyPath = p.getPath(); 1481 } 1482 catch (Exception e2) 1483 { 1484 // ignore 1485 } 1486 1487 String warnMsg = String.format("Site '%s' is pointed by a weak reference '%s' which is not representing a relation with project. This reference is ignored.", site.getName(), propertyPath); 1488 getLogger().warn(warnMsg); 1489 } 1490 } 1491 1492 return null; 1493 }) 1494 .filter(Objects::nonNull) 1495 .collect(Collectors.toList()); 1496 } 1497 catch (RepositoryException e) 1498 { 1499 getLogger().error(String.format("Unable to find projects for site '%s'", site.getName()), e); 1500 } 1501 1502 return new ArrayList<>(); 1503 } 1504 1505 /** 1506 * Create the project workspace for a given project. 1507 * @param project The project for which the workspace must be created 1508 * @param errors A list of possible errors to populate. Can be null if the caller is not interested in error tracking. 1509 * @return The site created for this workspace 1510 */ 1511 protected Site _createProjectWorkspace(Project project, List<String> errors) 1512 { 1513 String initialSiteName = project.getName(); 1514 Site site = null; 1515 1516 Map<String, Object> result = _siteDao.createSite(null, initialSiteName, ProjectWorkspaceSiteType.TYPE_ID, true); 1517 1518 String siteId = (String) result.get("id"); 1519 String siteName = (String) result.get("name"); 1520 if (StringUtils.isNotEmpty(siteId)) 1521 { 1522 // Creation success 1523 site = _siteManager.getSite(siteName); 1524 1525 I18nizableText i18nSiteTitle = new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_DEFAULT_PROJECT_WORKSPACE_TITLE", Arrays.asList(project.getTitle())); 1526 site.setTitle(_i18nUtils.translate(i18nSiteTitle)); 1527 1528 // Add site to project 1529 project.setSites(Arrays.asList(site.getName())); 1530 1531 site.saveChanges(); 1532 } 1533 1534 return site; 1535 } 1536 1537 /** 1538 * Get the project's tags 1539 * @return The project's tags 1540 */ 1541 @Callable 1542 public List<String> getTags() 1543 { 1544 AmetysObject projectsRootNode = getProjectsRoot(); 1545 if (projectsRootNode instanceof JCRAmetysObject) 1546 { 1547 Node node = ((JCRAmetysObject) projectsRootNode).getNode(); 1548 1549 try 1550 { 1551 return Arrays.stream(node.getProperty(__PROJECTS_TAGS_PROPERTY).getValues()) 1552 .map(LambdaUtils.wrap(Value::getString)) 1553 .collect(Collectors.toList()); 1554 } 1555 catch (PathNotFoundException e) 1556 { 1557 // property is not set, empty list will be returned. 1558 } 1559 catch (RepositoryException e) 1560 { 1561 throw new AmetysRepositoryException(e); 1562 } 1563 } 1564 1565 return new ArrayList<>(); 1566 } 1567 1568 /** 1569 * Set the tags 1570 * @param tags The tags to set 1571 */ 1572 @Callable 1573 public synchronized void setTags(List<String> tags) 1574 { 1575 AmetysObject projectsRootNode = getProjectsRoot(); 1576 if (projectsRootNode instanceof JCRAmetysObject) 1577 { 1578 JCRAmetysObject jcrProjectsRootNode = (JCRAmetysObject) projectsRootNode; 1579 1580 if (CollectionUtils.isNotEmpty(tags)) 1581 { 1582 String[] tagsArray = tags.stream() 1583 .map(String::trim) 1584 .map(String::toLowerCase) 1585 .filter(StringUtils::isNotEmpty) 1586 .distinct() 1587 .toArray(String[]::new); 1588 1589 try 1590 { 1591 jcrProjectsRootNode.getNode().setProperty(__PROJECTS_TAGS_PROPERTY, tagsArray); 1592 jcrProjectsRootNode.saveChanges(); 1593 } 1594 catch (RepositoryException e) 1595 { 1596 throw new AmetysRepositoryException(e); 1597 } 1598 } 1599 else 1600 { 1601 Node node = jcrProjectsRootNode.getNode(); 1602 try 1603 { 1604 if (node.hasProperty(__PROJECTS_TAGS_PROPERTY)) 1605 { 1606 node.getProperty(__PROJECTS_TAGS_PROPERTY).remove(); 1607 jcrProjectsRootNode.saveChanges(); 1608 } 1609 } 1610 catch (RepositoryException e) 1611 { 1612 throw new AmetysRepositoryException(e); 1613 } 1614 } 1615 } 1616 } 1617 1618 /** 1619 * Add project's tags 1620 * @param newTags The new tags to add 1621 */ 1622 public synchronized void addTags(Collection<String> newTags) 1623 { 1624 if (CollectionUtils.isNotEmpty(newTags)) 1625 { 1626 AmetysObject projectsRootNode = getProjectsRoot(); 1627 if (projectsRootNode instanceof JCRAmetysObject) 1628 { 1629 // Concat existing tags with new lowercased tags 1630 String[] tags = Stream.concat(getTags().stream(), newTags.stream().map(String::trim).map(String::toLowerCase).filter(StringUtils::isNotEmpty)) 1631 .distinct() 1632 .toArray(String[]::new); 1633 1634 try 1635 { 1636 ((JCRAmetysObject) projectsRootNode).getNode().setProperty(__PROJECTS_TAGS_PROPERTY, tags); 1637 } 1638 catch (RepositoryException e) 1639 { 1640 throw new AmetysRepositoryException(e); 1641 } 1642 } 1643 } 1644 } 1645 1646 /** 1647 * Get the project's places 1648 * @return The project's places 1649 */ 1650 @Callable 1651 public List<String> getPlaces() 1652 { 1653 AmetysObject projectsRootNode = getProjectsRoot(); 1654 if (projectsRootNode instanceof JCRAmetysObject) 1655 { 1656 Node node = ((JCRAmetysObject) projectsRootNode).getNode(); 1657 1658 try 1659 { 1660 return Arrays.stream(node.getProperty(__PROJECTS_PLACES_PROPERTY).getValues()) 1661 .map(LambdaUtils.wrap(Value::getString)) 1662 .collect(Collectors.toList()); 1663 } 1664 catch (PathNotFoundException e) 1665 { 1666 // property is not set, empty list will be returned. 1667 } 1668 catch (RepositoryException e) 1669 { 1670 throw new AmetysRepositoryException(e); 1671 } 1672 } 1673 1674 return new ArrayList<>(); 1675 } 1676 1677 /** 1678 * Add project's places 1679 * @param newPlaces The new places to add 1680 */ 1681 public synchronized void addPlaces(Collection<String> newPlaces) 1682 { 1683 if (CollectionUtils.isNotEmpty(newPlaces)) 1684 { 1685 AmetysObject projectsRootNode = getProjectsRoot(); 1686 if (projectsRootNode instanceof JCRAmetysObject) 1687 { 1688 Set<String> lowercasedPlaces = new HashSet<>(); 1689 1690 // Concat existing places with new places 1691 String[] places = Stream.concat(getPlaces().stream(), newPlaces.stream().map(String::trim).filter(StringUtils::isNotEmpty)) 1692 // duplicates are filtered out 1693 .filter(p -> lowercasedPlaces.add(p.toLowerCase())) 1694 .toArray(String[]::new); 1695 1696 try 1697 { 1698 ((JCRAmetysObject) projectsRootNode).getNode().setProperty(__PROJECTS_PLACES_PROPERTY, places); 1699 } 1700 catch (RepositoryException e) 1701 { 1702 throw new AmetysRepositoryException(e); 1703 } 1704 } 1705 } 1706 } 1707 1708 /** 1709 * Set the places 1710 * @param places The places to set 1711 */ 1712 @Callable 1713 public synchronized void setPlaces(List<String> places) 1714 { 1715 AmetysObject projectsRootNode = getProjectsRoot(); 1716 if (projectsRootNode instanceof JCRAmetysObject) 1717 { 1718 JCRAmetysObject jcrProjectsRootNode = (JCRAmetysObject) projectsRootNode; 1719 1720 if (CollectionUtils.isNotEmpty(places)) 1721 { 1722 Set<String> lowercasedPlaces = new HashSet<>(); 1723 1724 String[] placesArray = places.stream() 1725 .map(String::trim) 1726 .filter(StringUtils::isNotEmpty) 1727 // duplicates are filtered out 1728 .filter(p -> lowercasedPlaces.add(p.toLowerCase())) 1729 .toArray(String[]::new); 1730 1731 try 1732 { 1733 jcrProjectsRootNode.getNode().setProperty(__PROJECTS_PLACES_PROPERTY, placesArray); 1734 jcrProjectsRootNode.saveChanges(); 1735 } 1736 catch (RepositoryException e) 1737 { 1738 throw new AmetysRepositoryException(e); 1739 } 1740 } 1741 else 1742 { 1743 Node node = jcrProjectsRootNode.getNode(); 1744 try 1745 { 1746 if (node.hasProperty(__PROJECTS_PLACES_PROPERTY)) 1747 { 1748 node.getProperty(__PROJECTS_PLACES_PROPERTY).remove(); 1749 jcrProjectsRootNode.saveChanges(); 1750 } 1751 } 1752 catch (RepositoryException e) 1753 { 1754 throw new AmetysRepositoryException(e); 1755 } 1756 } 1757 } 1758 } 1759 1760 /** 1761 * Get the list of activated modules for a project 1762 * @param project The project 1763 * @return The list of activated modules 1764 */ 1765 public List<WorkspaceModule> getModules(Project project) 1766 { 1767 return _moduleManagerEP.getModules().stream() 1768 .filter(module -> isModuleActivated(project, module.getId())) 1769 .collect(Collectors.toList()); 1770 } 1771 1772 /** 1773 * Retrieves the page of the module for all available languages 1774 * @param project The project 1775 * @param moduleId The project module id 1776 * @param language the sitemap language or <code>null</code> for all sitemap languages. 1777 * @return the page or null if not found 1778 */ 1779 public AmetysObjectIterable<Page> getModulePages(Project project, String moduleId, String language) 1780 { 1781 if (_moduleManagerEP.hasExtension(moduleId)) 1782 { 1783 WorkspaceModule moduleManager = _moduleManagerEP.getExtension(moduleId); 1784 return moduleManager.getModulePages(project, language); 1785 } 1786 return null; 1787 } 1788 1789 /** 1790 * Get a page in the site of a given project with a specific tag 1791 * @param project The project 1792 * @param tagName The name of the tag 1793 * @param language the sitemap language or <code>null</code> for all sitemap languages. 1794 * @return The module's pages 1795 */ 1796 public AmetysObjectIterable<Page> getProjectPages(Project project, String tagName, String language) 1797 { 1798 String siteName = Iterables.getFirst(getProjectNames(project), null); 1799 if (StringUtils.isEmpty(siteName)) 1800 { 1801 return null; 1802 } 1803 1804 Expression expression = new TagExpression(Operator.EQ, tagName); 1805 String query = PageQueryHelper.getPageXPathQuery(siteName, language, null, expression, null); 1806 1807 return _resolver.query(query); 1808 } 1809 1810 1811 /** 1812 * Get the dashboard page in the site of a given project 1813 * @param project The project 1814 * @param language the sitemap language or <code>null</code> for all sitemap languages. 1815 * @return The module's dashboard pages 1816 */ 1817 public AmetysObjectIterable<Page> getProjectDashboardPage(Project project, String language) 1818 { 1819 String siteName = Iterables.getFirst(getProjectNames(project), null); 1820 if (StringUtils.isEmpty(siteName)) 1821 { 1822 return null; 1823 } 1824 1825 String query = "//element(" + siteName + ", ametys:site)/ametys-internal:sitemaps/" 1826 + (language == null ? "*" : language) 1827 + "//element(index, ametys:page)"; 1828 1829 return _resolver.query(query); 1830 } 1831 1832 /** 1833 * Activate the list of module of the project 1834 * @param project The project 1835 * @param moduleIds The list of modules. Can be null to activate all modules 1836 */ 1837 public void activateModules(Project project, Set<String> moduleIds) 1838 { 1839 Set<String> modules = moduleIds == null ? _moduleManagerEP.getExtensionsIds() : moduleIds; 1840 1841 for (String moduleId : modules) 1842 { 1843 WorkspaceModule module = _moduleManagerEP.getModule(moduleId); 1844 if (module != null && !isModuleActivated(project, moduleId)) 1845 { 1846 module.activateModule(project); 1847 project.addModule(moduleId); 1848 } 1849 } 1850 1851 project.saveChanges(); 1852 } 1853 1854 /** 1855 * Initialize the sitemap with the active module of the project 1856 * @param project The project 1857 * @param sitemap The sitemap 1858 */ 1859 public void initializeModulesSitemap(Project project, Sitemap sitemap) 1860 { 1861 Set<String> modules = _moduleManagerEP.getExtensionsIds(); 1862 1863 for (String moduleId : modules) 1864 { 1865 if (_moduleManagerEP.hasExtension(moduleId)) 1866 { 1867 WorkspaceModule module = _moduleManagerEP.getExtension(moduleId); 1868 1869 if (isModuleActivated(project, moduleId)) 1870 { 1871 module.initializeSitemap(sitemap); 1872 } 1873 } 1874 } 1875 } 1876 1877 /** 1878 * Determines if a module is activated 1879 * @param project The project 1880 * @param moduleId The id of module 1881 * @return true if the module the currently activated 1882 */ 1883 public boolean isModuleActivated(Project project, String moduleId) 1884 { 1885 return ArrayUtils.contains(project.getModules(), moduleId); 1886 } 1887 1888 /** 1889 * Remove the explorer root node of the project module, remove all events 1890 * related to that module and set it to deactivated 1891 * @param project The project 1892 * @param moduleIds The id of module to activate 1893 */ 1894 public void deactivateModules(Project project, Set<String> moduleIds) 1895 { 1896 for (String moduleId : moduleIds) 1897 { 1898 WorkspaceModule module = _moduleManagerEP.getModule(moduleId); 1899 if (module != null && isModuleActivated(project, moduleId)) 1900 { 1901 module.deactivateModule(project); 1902 project.removeModule(moduleId); 1903 } 1904 } 1905 1906 project.saveChanges(); 1907 } 1908 1909 1910 /** 1911 * Get the list of profiles configured for the workspaces' projects 1912 * @return The list of profiles as JSON 1913 */ 1914 @Callable 1915 public Map<String, Object> getProjectProfiles() 1916 { 1917 Map<String, Object> result = new HashMap<>(); 1918 List<Map<String, Object>> profiles = _projectRightHelper.getProfileList().stream().map(p -> p.toJSON()).collect(Collectors.toList()); 1919 result.put("profiles", profiles); 1920 return result; 1921 } 1922}