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