001/* 002 * Copyright 2020 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.Collections; 023import java.util.HashMap; 024import java.util.HashSet; 025import java.util.Iterator; 026import java.util.LinkedHashSet; 027import java.util.List; 028import java.util.Map; 029import java.util.Objects; 030import java.util.Optional; 031import java.util.Set; 032import java.util.function.Predicate; 033import java.util.stream.Collectors; 034import java.util.stream.Stream; 035import java.util.stream.StreamSupport; 036 037import javax.jcr.Node; 038import javax.jcr.PathNotFoundException; 039import javax.jcr.Property; 040import javax.jcr.RepositoryException; 041import javax.jcr.Session; 042import javax.jcr.Value; 043 044import org.apache.avalon.framework.activity.Initializable; 045import org.apache.avalon.framework.component.Component; 046import org.apache.avalon.framework.context.Context; 047import org.apache.avalon.framework.context.ContextException; 048import org.apache.avalon.framework.context.Contextualizable; 049import org.apache.avalon.framework.logger.AbstractLogEnabled; 050import org.apache.avalon.framework.service.ServiceException; 051import org.apache.avalon.framework.service.ServiceManager; 052import org.apache.avalon.framework.service.Serviceable; 053import org.apache.cocoon.components.ContextHelper; 054import org.apache.cocoon.environment.Request; 055import org.apache.commons.collections.CollectionUtils; 056import org.apache.commons.lang.ArrayUtils; 057import org.apache.commons.lang3.StringUtils; 058import org.apache.commons.lang3.tuple.Pair; 059 060import org.ametys.cms.repository.ContentDAO.TagMode; 061import org.ametys.cms.tag.Tag; 062import org.ametys.core.cache.AbstractCacheManager; 063import org.ametys.core.cache.AbstractCacheManager.CacheType; 064import org.ametys.core.cache.Cache; 065import org.ametys.core.cache.CacheException; 066import org.ametys.core.group.GroupDirectoryContextHelper; 067import org.ametys.core.group.GroupIdentity; 068import org.ametys.core.observation.Event; 069import org.ametys.core.observation.ObservationManager; 070import org.ametys.core.observation.Observer; 071import org.ametys.core.right.RightManager; 072import org.ametys.core.right.RightManager.RightResult; 073import org.ametys.core.ui.Callable; 074import org.ametys.core.user.CurrentUserProvider; 075import org.ametys.core.user.UserIdentity; 076import org.ametys.core.user.population.PopulationContextHelper; 077import org.ametys.core.util.I18nUtils; 078import org.ametys.core.util.LambdaUtils; 079import org.ametys.plugins.core.impl.cache.AbstractCacheKey; 080import org.ametys.plugins.core.search.UserAndGroupSearchManager; 081import org.ametys.plugins.core.user.UserHelper; 082import org.ametys.plugins.explorer.ExplorerNode; 083import org.ametys.plugins.explorer.resources.ModifiableResourceCollection; 084import org.ametys.plugins.repository.AmetysObject; 085import org.ametys.plugins.repository.AmetysObjectIterable; 086import org.ametys.plugins.repository.AmetysObjectResolver; 087import org.ametys.plugins.repository.AmetysRepositoryException; 088import org.ametys.plugins.repository.CollectionIterable; 089import org.ametys.plugins.repository.ModifiableAmetysObject; 090import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 091import org.ametys.plugins.repository.RepositoryConstants; 092import org.ametys.plugins.repository.UnknownAmetysObjectException; 093import org.ametys.plugins.repository.jcr.JCRAmetysObject; 094import org.ametys.plugins.repository.jcr.NodeTypeHelper; 095import org.ametys.plugins.repository.provider.AbstractRepository; 096import org.ametys.plugins.repository.provider.JackrabbitRepository; 097import org.ametys.plugins.repository.provider.WorkspaceSelector; 098import org.ametys.plugins.repository.query.expression.Expression; 099import org.ametys.plugins.repository.query.expression.Expression.Operator; 100import org.ametys.plugins.repository.query.expression.StringExpression; 101import org.ametys.plugins.workspaces.ObservationConstants; 102import org.ametys.plugins.workspaces.categories.CategoryProviderExtensionPoint; 103import org.ametys.plugins.workspaces.members.JCRProjectMember.MemberType; 104import org.ametys.plugins.workspaces.members.ProjectMemberManager; 105import org.ametys.plugins.workspaces.members.ProjectMemberManager.ProjectMember; 106import org.ametys.plugins.workspaces.project.modules.WorkspaceModule; 107import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint; 108import org.ametys.plugins.workspaces.project.objects.Project; 109import org.ametys.plugins.workspaces.project.objects.Project.InscriptionStatus; 110import org.ametys.plugins.workspaces.project.rights.ProjectRightHelper; 111import org.ametys.plugins.workspaces.tags.ProjectTagProviderExtensionPoint; 112import org.ametys.plugins.workspaces.util.StatisticColumn; 113import org.ametys.plugins.workspaces.util.StatisticsColumnType; 114import org.ametys.runtime.config.Config; 115import org.ametys.runtime.i18n.I18nizableText; 116import org.ametys.runtime.plugin.component.PluginAware; 117import org.ametys.web.repository.page.ModifiablePage; 118import org.ametys.web.repository.page.Page; 119import org.ametys.web.repository.page.PageQueryHelper; 120import org.ametys.web.repository.page.SitemapElement; 121import org.ametys.web.repository.site.Site; 122import org.ametys.web.repository.site.SiteDAO; 123import org.ametys.web.repository.site.SiteManager; 124import org.ametys.web.repository.sitemap.Sitemap; 125import org.ametys.web.site.SiteConfigurationManager; 126 127/** 128 * Helper component for managing project workspaces 129 */ 130public class ProjectManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable, PluginAware, Initializable, Observer 131{ 132 /** Avalon Role */ 133 public static final String ROLE = ProjectManager.class.getName(); 134 135 /** Constant for the {@link Cache} id (the {@link Cache} is in {@link CacheType#REQUEST REQUEST} attribute) for the {@link Project}s objects 136 * in cache by {@link RequestProjectCacheKey} (composition of project name and workspace name). */ 137 public static final String REQUEST_PROJECTBYID_CACHE = ProjectManager.class.getName() + "$ProjectById"; 138 139 /** Constant for the {@link Cache} id for the {@link Project} ids (as {@link String}s) in cache by project name (for whole application). */ 140 public static final String MEMORY_PROJECTIDBYNAMECACHE = ProjectManager.class.getName() + "$UUID"; 141 142 /** Constant for the {@link Cache} id (the {@link Cache} is in {@link CacheType#REQUEST REQUEST} attribute) for the {@link Page}s objects 143 * in cache by {@link RequestModuleCacheKey} (composition of project name and module name). */ 144 public static final String REQUEST_PAGESBYPROJECTANDMODULE_CACHE = ProjectManager.class.getName() + "$PagesByModule"; 145 146 /** Constant for the {@link Cache} id for the {@link Project} ids (as {@link String}s) in cache by project name (for whole application). */ 147 public static final String MEMORY_PAGESBYIDCACHE = ProjectManager.class.getName() + "$PageUUID"; 148 149 /** Constant for the {@link Cache} id for the {@link Project} ids (as {@link String}s) in cache by site name (for whole application). */ 150 public static final String MEMORY_SITEASSOCIATION_CACHE = ProjectManager.class.getName() + "$SiteAssociation"; 151 152 /** Workspaces plugin node name */ 153 private static final String __WORKSPACES_PLUGIN_NODE_NAME = "workspaces"; 154 155 /** Workspaces plugin node name */ 156 private static final String __WORKSPACES_PLUGIN_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":unstructured"; 157 158 /** The name of the projects root node */ 159 private static final String __PROJECTS_ROOT_NODE_NAME = "projects"; 160 161 /** The type of the projects root node */ 162 private static final String __PROJECTS_ROOT_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":unstructured"; 163 164 /** Constants for tags metadata */ 165 private static final String __PROJECTS_TAGS_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":tags"; 166 167 /** Constants for places metadata */ 168 private static final String __PROJECTS_PLACES_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":places"; 169 170 private static final String __PAGE_MODULES_VALUE = "workspaces-modules"; 171 172 private static final String __IS_CACHE_FILLED = "###iscachefilled###"; 173 174 /** Ametys object resolver */ 175 protected AmetysObjectResolver _resolver; 176 177 /** The i18n utils. */ 178 protected I18nUtils _i18nUtils; 179 180 /** Site manager */ 181 protected SiteManager _siteManager; 182 183 /** Site DAO */ 184 protected SiteDAO _siteDao; 185 186 /** Site configuration manager */ 187 protected SiteConfigurationManager _siteConfigurationManager; 188 189 /** Module Managers EP */ 190 protected WorkspaceModuleExtensionPoint _moduleManagerEP; 191 192 /** Helper for user population */ 193 protected PopulationContextHelper _populationContextHelper; 194 195 /** Helper for group directory's context */ 196 protected GroupDirectoryContextHelper _groupDirectoryContextHelper; 197 198 /** The project members' manager */ 199 protected ProjectMemberManager _projectMemberManager; 200 201 /** Avalon context */ 202 protected Context _context; 203 204 private ObservationManager _observationManager; 205 206 private CurrentUserProvider _currentUserProvider; 207 208 private ProjectMemberManager _projectMembers; 209 210 private String _pluginName; 211 212 private ProjectRightHelper _projectRightHelper; 213 214 private ProjectTagProviderExtensionPoint _projectTagProviderEP; 215 216 private CategoryProviderExtensionPoint _categoryProviderEP; 217 218 private UserHelper _userHelper; 219 220 private AbstractCacheManager _cacheManager; 221 222 private UserAndGroupSearchManager _userAndGroupSearchManager; 223 224 private JackrabbitRepository _repository; 225 226 private WorkspaceSelector _workspaceSelector; 227 228 private RightManager _rightManager; 229 230 @Override 231 public void contextualize(Context context) throws ContextException 232 { 233 _context = context; 234 } 235 236 @Override 237 public void service(ServiceManager manager) throws ServiceException 238 { 239 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 240 _repository = (JackrabbitRepository) manager.lookup(AbstractRepository.ROLE); 241 _workspaceSelector = (WorkspaceSelector) manager.lookup(WorkspaceSelector.ROLE); 242 _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE); 243 _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE); 244 _siteDao = (SiteDAO) manager.lookup(SiteDAO.ROLE); 245 _siteConfigurationManager = (SiteConfigurationManager) manager.lookup(SiteConfigurationManager.ROLE); 246 _projectMembers = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE); 247 _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE); 248 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 249 _moduleManagerEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE); 250 _projectRightHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE); 251 _projectTagProviderEP = (ProjectTagProviderExtensionPoint) manager.lookup(ProjectTagProviderExtensionPoint.ROLE); 252 _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE); 253 _categoryProviderEP = (CategoryProviderExtensionPoint) manager.lookup(CategoryProviderExtensionPoint.ROLE); 254 _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE); 255 _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE); 256 _groupDirectoryContextHelper = (GroupDirectoryContextHelper) manager.lookup(GroupDirectoryContextHelper.ROLE); 257 _projectMemberManager = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE); 258 _userAndGroupSearchManager = (UserAndGroupSearchManager) manager.lookup(UserAndGroupSearchManager.ROLE); 259 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 260 } 261 262 public void initialize() throws Exception 263 { 264 _createCaches(); 265 _observationManager.registerObserver(this); 266 } 267 268 @Override 269 public void setPluginInfo(String pluginName, String featureName, String id) 270 { 271 _pluginName = pluginName; 272 } 273 274 /** 275 * Retrieves all projects 276 * @return the projects 277 */ 278 public AmetysObjectIterable<Project> getProjects() 279 { 280 return getProjects(true); 281 } 282 283 /** 284 * Retrieves all projects 285 * @param onlyWorking true to retrieve only working projects with non null sites 286 * @return the projects 287 */ 288 public AmetysObjectIterable<Project> getProjects(boolean onlyWorking) 289 { 290 // As cache is computed from default JCR workspace, we need to filter on sites that exist into the current JCR workspace 291 Set<Project> projects = _getUUIDCache().values().stream() 292 .filter(_resolver::hasAmetysObjectForId) 293 .map(_resolver::<Project>resolveById) 294 // If needed, check if the site is not null, as it can happen if site was deleted from admin side 295 .filter(project -> !onlyWorking || Objects.nonNull(project.getSite())) 296 .collect(Collectors.toSet()); 297 298 return new CollectionIterable<>(projects); 299 } 300 301 /** 302 * Retrieves projects filtered by categories 303 * @param filteredCategories the filtered categories. Can be empty to no filter by categories. 304 * @return the projects 305 */ 306 public List<Project> getProjects(List<String> filteredCategories) 307 { 308 Predicate<Project> matchCategories = p -> filteredCategories.isEmpty() || !Collections.disjoint(p.getCategories(), filteredCategories); 309 310 return getProjects() 311 .stream() 312 .filter(matchCategories) 313 .collect(Collectors.toList()); 314 } 315 316 /** 317 * Retrieves projects filtered by categories and/or keywords 318 * @param filteredCategories the filtered categories. Can be empty to no filter by categories. 319 * @param filteredKeywords the filtered keywords. Can be empty to no filter by keywords. 320 * @param matchesAny true to get projects matching categories OR keywords 321 * @return the projects 322 */ 323 public List<Project> getProjects(List<String> filteredCategories, List<String> filteredKeywords, boolean matchesAny) 324 { 325 Predicate<Project> fullMatch = null; 326 327 if (matchesAny) 328 { 329 // filter with project matching one of categories OR one of keywords 330 Predicate<Project> matchCategories = p -> !Collections.disjoint(p.getCategories(), filteredCategories); 331 Predicate<Project> matchKeywords = p -> !Collections.disjoint(Arrays.asList(p.getKeywords()), filteredKeywords); 332 Predicate<Project> emptyFilters = p -> filteredCategories.isEmpty() && filteredKeywords.isEmpty(); 333 334 fullMatch = matchCategories.or(matchKeywords).or(emptyFilters); 335 } 336 else 337 { 338 // filter with project matching one of categories AND one of keywords 339 Predicate<Project> matchCategories = p -> filteredCategories.isEmpty() || !Collections.disjoint(p.getCategories(), filteredCategories); 340 Predicate<Project> matchKeywords = p -> filteredKeywords.isEmpty() || !Collections.disjoint(Arrays.asList(p.getKeywords()), filteredKeywords); 341 342 fullMatch = matchCategories.and(matchKeywords); 343 } 344 345 return getProjects() 346 .stream() 347 .filter(fullMatch) 348 .collect(Collectors.toList()); 349 } 350 351 /** 352 * Retrieves all projects for client side 353 * @return the projects 354 */ 355 @Callable(right = "Runtime_Rights_Admin_Access", context = "/admin") 356 public List<Map<String, Object>> getProjectsForClientSide() 357 { 358 return getProjects(false) 359 .stream() 360 .map(p -> getProjectProperties(p)) 361 .collect(Collectors.toList()); 362 } 363 364 365 /** 366 * Retrieves a project by its name 367 * @param projectName The project name 368 * @return the project or <code>null</code> if not found 369 */ 370 public Project getProject(String projectName) 371 { 372 if (StringUtils.isBlank(projectName)) 373 { 374 return null; 375 } 376 377 Request request = _getRequest(); 378 if (request == null) 379 { 380 // There is no request to store cache 381 return _computeProject(projectName); 382 } 383 384 Cache<RequestProjectCacheKey, Project> projectsCache = _getRequestProjectCache(); 385 386 // The site key in the cache is of the form {site + workspace}. 387 String currentWorkspace = _workspaceSelector.getWorkspace(); 388 RequestProjectCacheKey projectKey = RequestProjectCacheKey.of(projectName, currentWorkspace); 389 390 try 391 { 392 Project project = projectsCache.get(projectKey, __ -> _computeProject(projectName)); 393 return project; 394 } 395 catch (CacheException e) 396 { 397 if (e.getCause() instanceof UnknownAmetysObjectException) 398 { 399 throw new UnknownAmetysObjectException(e.getMessage()); 400 } 401 else 402 { 403 throw e; 404 } 405 } 406 } 407 408 /** 409 * Get the user's projects 410 * @param user the user 411 * @return the user's projects 412 */ 413 public Map<Project, MemberType> getUserProjects(UserIdentity user) 414 { 415 return getUserProjects(user, List.of()); 416 } 417 418 /** 419 * Get the user's projects filtered by categories 420 * @param user the user 421 * @param filteredCategories the filtered categories. Can be empty to no filter by categories 422 * @return the user's projects 423 */ 424 public Map<Project, MemberType> getUserProjects(UserIdentity user, List<String> filteredCategories) 425 { 426 Map<Project, MemberType> userProjects = new HashMap<>(); 427 428 List<Project> projects = getProjects(filteredCategories); 429 for (Project project : projects) 430 { 431 ProjectMember member = _projectMembers.getProjectMember(project, user); 432 if (member != null) 433 { 434 userProjects.put(project, member.getType()); 435 } 436 } 437 438 return userProjects; 439 } 440 441 /** 442 * Get the user's projects filtered by categories OR keywords 443 * @param user the user 444 * @param filteredCategories the filtered categories. Can be empty to no filter by categories 445 * @param filteredKeywords the filtered keywords. Can be empty to no filter by keywords 446 * @return the user's projects 447 */ 448 public Map<Project, MemberType> getUserProjects(UserIdentity user, List<String> filteredCategories, List<String> filteredKeywords) 449 { 450 Map<Project, MemberType> userProjects = new HashMap<>(); 451 452 List<Project> projects = getProjects(filteredCategories, filteredKeywords, true); 453 for (Project project : projects) 454 { 455 ProjectMember member = _projectMembers.getProjectMember(project, user); 456 if (member != null) 457 { 458 userProjects.put(project, member.getType()); 459 } 460 } 461 462 return userProjects; 463 } 464 465 /** 466 * Get the projects managed by the user 467 * @param user the user 468 * @return the projects for which the user is a manager 469 */ 470 public List<Project> getManagedProjects(UserIdentity user) 471 { 472 return getManagedProjects(user, List.of()); 473 } 474 475 /** 476 * Get the projects managed by the user 477 * @param user the user 478 * @param filteredCategories the filtered categories. Can be empty to no filter by categories. 479 * @return the projects for which the user is a manager 480 */ 481 public List<Project> getManagedProjects(UserIdentity user, List<String> filteredCategories) 482 { 483 return getProjects(filteredCategories) 484 .stream() 485 .filter(p -> ArrayUtils.contains(p.getManagers(), user)) 486 .collect(Collectors.toList()); 487 } 488 489 /** 490 * Returns true if the given project exists. 491 * @param projectName the project name. 492 * @return true if the given project exists. 493 */ 494 public boolean hasProject(String projectName) 495 { 496 Map<String, String> uuidCache = _getUUIDCache(); 497 if (uuidCache.containsKey(projectName)) 498 { 499 // As cache is computed from default JCR workspace, we need to check if the project exists into the current JCR workspace 500 return _resolver.hasAmetysObjectForId(uuidCache.get(projectName)); 501 } 502 return false; 503 } 504 505 /** 506 * Get all managers 507 * @return the managers 508 */ 509 public Set<UserIdentity> getManagers() 510 { 511 return getProjects() 512 .stream() 513 .map(Project::getManagers) 514 .flatMap(Arrays::stream) 515 .collect(Collectors.toSet()); 516 } 517 518 /** 519 * Determines if the current user is a manager of at least one project 520 * @return true if the user is a manager 521 */ 522 public boolean isManager() 523 { 524 return isManager(_currentUserProvider.getUser()); 525 } 526 527 /** 528 * Determines if the user is a manager of at least one project 529 * @param user the user 530 * @return true if the user is a manager 531 */ 532 public boolean isManager(UserIdentity user) 533 { 534 AmetysObjectIterable<Project> projects = getProjects(); 535 for (Project project : projects) 536 { 537 if (isManager(project, user)) 538 { 539 return true; 540 } 541 } 542 return false; 543 } 544 545 /** 546 * Determines if the user is a manager of the project 547 * @param projectName the project name 548 * @param user the user 549 * @return true if the user is a manager 550 */ 551 public boolean isManager(String projectName, UserIdentity user) 552 { 553 Project project = getProject(projectName); 554 if (project != null) 555 { 556 return isManager(project, user); 557 } 558 return false; 559 } 560 561 /** 562 * Determines if the user is a manager of the project 563 * @param project the project 564 * @param user the user 565 * @return true if the user is a manager 566 */ 567 public boolean isManager(Project project, UserIdentity user) 568 { 569 return ArrayUtils.contains(project.getManagers(), user); 570 } 571 572 /** 573 * Can the current user access backoffice on the site of the current project 574 * @param project The non null project to analyse 575 * @return true if the user can access to the backoffice 576 */ 577 public boolean canAccessBO(Project project) 578 { 579 Site site = project.getSite(); 580 if (site == null) 581 { 582 return false; 583 } 584 585 Request request = ContextHelper.getRequest(_context); 586 String currentSiteName = (String) request.getAttribute("siteName"); 587 try 588 { 589 request.setAttribute("siteName", site.getName()); // Setting temporarily this attribute to check user rights on any object on this site 590 return !_rightManager.getUserRights(_currentUserProvider.getUser(), "/cms").isEmpty(); 591 } 592 finally 593 { 594 request.setAttribute("siteName", currentSiteName); 595 } 596 } 597 598 /** 599 * Retrieves the mapping of all the projects name with their title on which the current user has access 600 * @return the map (projectName, projectTitle) for all projects 601 */ 602 @Callable 603 public List<Map<String, Object>> getUserProjectsData() 604 { 605 return getUserProjects(_currentUserProvider.getUser()) 606 .keySet() 607 .stream() 608 .map(p -> _project2json(p)) 609 .collect(Collectors.toList()); 610 } 611 612 /** 613 * Retrieves the users that have not been yet added to a project with a given criteria 614 * @param projectName the project name 615 * @param limit limit of request 616 * @param criteria the criteria of the search 617 * @param previousSearchData the previous search data to compute offset. Null if first search 618 * @return list of users 619 */ 620 @SuppressWarnings("unchecked") 621 @Callable 622 public Map<String, Object> searchUserByProject(String projectName, int limit, String criteria, Map<String, Object> previousSearchData) 623 { 624 625 Map<String, Object> results = new HashMap<>(); 626 Project project = this.getProject(projectName); 627 Site site = project.getSite(); 628 629 Set<String> projectMemberList = _projectMemberManager.getProjectMembers(project, false) 630 .stream() 631 .map(member -> 632 { 633 if (member.getType() == MemberType.USER) 634 { 635 return UserIdentity.userIdentityToString(member.getUser().getIdentity()); 636 } 637 else 638 { 639 GroupIdentity groupIdentityAsString = member.getGroup().getIdentity(); 640 return GroupIdentity.groupIdentityToString(groupIdentityAsString); 641 } 642 }) 643 .collect(Collectors.toSet()); 644 645 Set<String> contexts = new HashSet<>(Arrays.asList("/sites/" + site.getName(), "/sites-fo/" + site.getName())); 646 647 Map<String, Object> params = new HashMap<>(); 648 params.put("pattern", criteria); 649 650 Map<String, Object> result = new HashMap<>(); 651 Map<String, Object> searchData = previousSearchData; 652 List<Map<String, Object>> memberList = new ArrayList<>(); 653 654 do 655 { 656 result = _userAndGroupSearchManager. 657 searchUsersAndGroupByContext(contexts, limit - memberList.size(), searchData, params); 658 List<Map<String, Object>> filteredMembers = ((List<Map<String, Object>>) result.get("results")) 659 .stream() 660 .filter(member -> 661 { 662 return !projectMemberList.contains(member.get("login") + "#" + member.get("populationId")) && !projectMemberList.contains(member.get("id") + "#" + member.get("groupDirectory")); 663 }).collect(Collectors.toList()); 664 searchData = (Map<String, Object>) result.get("searchData"); 665 memberList.addAll(filteredMembers); 666 } 667 while (!result.containsKey("finished") && memberList.size() < limit); 668 669 results.put("searchData", searchData); 670 results.put("memberList", memberList); 671 return results; 672 } 673 674 /** 675 * Retrieves the mapping of all the projects name with their title (regarless user rights) 676 * @return the map (projectName, projectTitle) for all projects 677 */ 678 @Callable(right = "Runtime_Rights_Admin_Access", context = "/admin") 679 public List<Map<String, Object>> getProjectsData() 680 { 681 return getProjects() 682 .stream() 683 .map(p -> _project2json(p)) 684 .collect(Collectors.toList()); 685 } 686 687 /** 688 * Get the project's main properties as json object 689 * @param project the project 690 * @return the json representation of project 691 */ 692 protected Map<String, Object> _project2json(Project project) 693 { 694 Map<String, Object> json = new HashMap<>(); 695 696 json.put("id", project.getId()); 697 json.put("name", project.getName()); 698 json.put("title", project.getTitle()); 699 json.put("url", getProjectUrl(project, StringUtils.EMPTY)); 700 701 return json; 702 } 703 /** 704 * Retrieves the project names 705 * @return the project names 706 */ 707 @Callable(right = "Runtime_Rights_Admin_Access", context = "/admin") 708 public Collection<String> getProjectNames() 709 { 710 // As cache is computed from default JCR workspace, we need to filter on sites that exist into the current JCR workspace 711 return _getUUIDCache().entrySet().stream() 712 .filter(e -> _resolver.hasAmetysObjectForId(e.getValue())) 713 .map(Map.Entry::getKey) 714 .collect(Collectors.toList()); 715 } 716 717 /** 718 * Return the root for projects 719 * The root node will be created if necessary 720 * @return The root for projects 721 */ 722 public ModifiableTraversableAmetysObject getProjectsRoot() 723 { 724 try 725 { 726 ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins"); 727 ModifiableTraversableAmetysObject workspacesPluginNode = _getOrCreateObject(pluginsNode, __WORKSPACES_PLUGIN_NODE_NAME, __WORKSPACES_PLUGIN_NODE_TYPE); 728 return _getOrCreateObject(workspacesPluginNode, __PROJECTS_ROOT_NODE_NAME, __PROJECTS_ROOT_NODE_TYPE); 729 } 730 catch (AmetysRepositoryException e) 731 { 732 throw new AmetysRepositoryException("Error getting the projects root node.", e); 733 } 734 } 735 736 /** 737 * Retrieves the standard information of a project 738 * @param projectId Identifier of the project 739 * @return The map of information 740 */ 741 @Callable(right = "Runtime_Rights_Admin_Access", context = "/admin") 742 public Map<String, Object> getProjectProperties(String projectId) 743 { 744 return getProjectProperties((Project) _resolver.resolveById(projectId)); 745 } 746 747 /** 748 * Retrieves the standard information of a project 749 * @param project The project 750 * @return The map of information 751 */ 752 public Map<String, Object> getProjectProperties(Project project) 753 { 754 Map<String, Object> info = new HashMap<>(); 755 756 info.put("id", project.getId()); 757 info.put("name", project.getName()); 758 info.put("type", "project"); 759 760 info.put("title", project.getTitle()); 761 info.put("description", project.getDescription()); 762 info.put("inscriptionStatus", project.getInscriptionStatus().toString()); 763 info.put("defaultProfile", project.getDefaultProfile()); 764 765 info.put("creationDate", project.getCreationDate()); 766 767 // check if the project workspace configuration is valid 768 Site site = project.getSite(); 769 boolean valid = site != null && _siteConfigurationManager.isSiteConfigurationValid(site); 770 771 Set<String> categories = project.getCategories(); 772 info.put("categories", categories.stream() 773 .map(c -> _categoryProviderEP.getTag(c, new HashMap<>())) 774 .filter(Objects::nonNull) 775 .map(t -> _tag2json(t)) 776 .collect(Collectors.toList())); 777 778 Set<String> tags = project.getTags(); 779 info.put("tags", tags.stream() 780 .map(c -> _projectTagProviderEP.getTag(c, new HashMap<>())) 781 .filter(Objects::nonNull) 782 .map(t -> _tag2json(t)) 783 .collect(Collectors.toList())); 784 785 info.put("valid", valid); 786 787 UserIdentity[] managers = project.getManagers(); 788 info.put("managers", Arrays.stream(managers) 789 .map(u -> _userHelper.user2json(u)) 790 .collect(Collectors.toList())); 791 792 Map<String, String> siteProps = new HashMap<>(); 793 // site map with id ,name, title and url property 794 // { id: site id, name: site name, title: site title, url: site url } 795 if (site != null) 796 { 797 siteProps.put("id", site.getId()); 798 siteProps.put("name", site.getName()); 799 siteProps.put("title", site.getTitle()); 800 siteProps.put("url", site.getUrl()); 801 } 802 info.put("site", siteProps); 803 804 return info; 805 } 806 807 private Map<String, Object> _tag2json(Tag tag) 808 { 809 Map<String, Object> json = new HashMap<>(); 810 json.put("id", tag.getId()); 811 json.put("name", tag.getName()); 812 json.put("title", tag.getTitle()); 813 return json; 814 } 815 816 /** 817 * Get the project URL. 818 * @param project The project 819 * @param defaultValue The default value to use if there is no site 820 * @return The project URL if a site is configured, otherwise return the default value. 821 */ 822 public String getProjectUrl(Project project, String defaultValue) 823 { 824 Site site = project.getSite(); 825 if (site == null) 826 { 827 return defaultValue; 828 } 829 else 830 { 831 return site.getUrl(); 832 } 833 } 834 835 /** 836 * Create a project 837 * @param name The project name 838 * @param title The project title 839 * @param description The project description 840 * @param emailList Project mailing list 841 * @param inscriptionStatus The inscription status of the project 842 * @param defaultProfile The default profile for new members 843 * @return A map containing the id of the new project or an error key. 844 */ 845 @Callable 846 public Map<String, Object> createProject(String name, String title, String description, String emailList, String inscriptionStatus, String defaultProfile) 847 { 848 checkRightsForProjectCreation(InscriptionStatus.valueOf(inscriptionStatus.toUpperCase()), null); 849 850 Map<String, Object> result = new HashMap<>(); 851 List<String> errors = new ArrayList<>(); 852 853 Map<String, Object> additionalValues = new HashMap<>(); 854 additionalValues.put("description", description); 855 additionalValues.put("emailList", emailList); 856 additionalValues.put("inscriptionStatus", inscriptionStatus); 857 additionalValues.put("defaultProfile", defaultProfile); 858 859 Project project = createProject(name, title, additionalValues, null, errors); 860 861 if (CollectionUtils.isEmpty(errors)) 862 { 863 result.put("id", project.getId()); 864 } 865 else 866 { 867 result.put("error", errors.get(0)); 868 } 869 870 return result; 871 } 872 873 /** 874 * Create a project 875 * @param name The project name 876 * @param title The project title 877 * @param additionalValues A list of optional additional values. Accepted values are : description, mailingList, inscriptionStatus, defaultProfile, tags, categoryTags, keywords and language 878 * @param modulesIds The list of modules to activate. Can be null to activate all modules 879 * @param errors A list that will be populated with the encountered errors. If null, errors will not be tracked. 880 * @return The id of the new project 881 */ 882 public Project createProject(String name, String title, Map<String, Object> additionalValues, Set<String> modulesIds, List<String> errors) 883 { 884 if (StringUtils.isEmpty(title)) 885 { 886 throw new IllegalArgumentException(String.format("Cannot create project. Title is mandatory")); 887 } 888 889 ModifiableTraversableAmetysObject projectsRoot = getProjectsRoot(); 890 891 // Project name should be unique 892 if (hasProject(name)) 893 { 894 if (getLogger().isWarnEnabled()) 895 { 896 getLogger().warn(String.format("A project with the name '%s' already exists", name)); 897 } 898 899 if (errors != null) 900 { 901 errors.add("project-exists"); 902 } 903 904 return null; 905 } 906 907 Project project = projectsRoot.createChild(name, Project.NODE_TYPE); 908 project.setTitle(title); 909 String description = (String) additionalValues.getOrDefault("description", null); 910 if (StringUtils.isNotEmpty(description)) 911 { 912 project.setDescription(description); 913 } 914 String mailingList = (String) additionalValues.getOrDefault("emailList", null); 915 if (StringUtils.isNotEmpty(mailingList)) 916 { 917 project.setMailingList(mailingList); 918 } 919 920 String inscriptionStatus = (String) additionalValues.getOrDefault("inscriptionStatus", null); 921 if (StringUtils.isNotEmpty(inscriptionStatus)) 922 { 923 project.setInscriptionStatus(inscriptionStatus); 924 } 925 926 String defaultProfile = (String) additionalValues.getOrDefault("defaultProfile", null); 927 if (StringUtils.isNotEmpty(defaultProfile)) 928 { 929 project.setDefaultProfile(defaultProfile); 930 } 931 932 @SuppressWarnings("unchecked") 933 List<String> tags = (List<String>) additionalValues.getOrDefault("tags", null); 934 if (tags != null) 935 { 936 project.setTags(tags); 937 } 938 @SuppressWarnings("unchecked") 939 List<String> categoryTags = (List<String>) additionalValues.getOrDefault("categoryTags", null); 940 if (categoryTags != null) 941 { 942 project.setCategoryTags(categoryTags); 943 } 944 945 @SuppressWarnings("unchecked") 946 List<String> keywords = (List<String>) additionalValues.getOrDefault("keywords", null); 947 if (keywords != null) 948 { 949 project.setKeywords(keywords.toArray(new String[keywords.size()])); 950 } 951 952 project.setCreationDate(ZonedDateTime.now()); 953 954 // Create the project workspace = a site + a set of pages 955 _createProjectWorkspace(project, errors); 956 957 activateModules(project, modulesIds, additionalValues); 958 959 if (CollectionUtils.isEmpty(errors)) 960 { 961 project.saveChanges(); 962 963 // Notify observers 964 Map<String, Object> eventParams = new HashMap<>(); 965 eventParams.put(ObservationConstants.ARGS_PROJECT, project); 966 _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_ADDED, _currentUserProvider.getUser(), eventParams)); 967 968 } 969 else 970 { 971 deleteProject(project); 972 } 973 974 clearCaches(); 975 976 return project; 977 } 978 979 /** 980 * Edit a project 981 * @param id The project identifier 982 * @param title The title to set 983 * @param description The description to set 984 * @param mailingList Project mailing list 985 * @param inscriptionStatus The inscription status of the project 986 * @param defaultProfile The default profile for new members 987 */ 988 @Callable(right = ProjectConstants.RIGHT_PROJECT_EDIT, context = "/admin") 989 public void editProject(String id, String title, String description, String mailingList, String inscriptionStatus, String defaultProfile) 990 { 991 Project project = _resolver.resolveById(id); 992 editProject(project, title, description, mailingList, inscriptionStatus, defaultProfile); 993 } 994 995 /** 996 * Edit a project 997 * @param project The project 998 * @param title The title to set 999 * @param description The description to set 1000 * @param mailingList Project mailing list 1001 * @param inscriptionStatus The inscription status of the project 1002 * @param defaultProfile The default profile for new members 1003 */ 1004 public void editProject(Project project, String title, String description, String mailingList, String inscriptionStatus, String defaultProfile) 1005 { 1006 checkRightsForProjectEdition(project, InscriptionStatus.valueOf(inscriptionStatus.toUpperCase()), null); 1007 1008 project.setTitle(title); 1009 1010 if (StringUtils.isNotEmpty(description)) 1011 { 1012 project.setDescription(description); 1013 } 1014 else 1015 { 1016 project.removeDescription(); 1017 } 1018 1019 if (StringUtils.isNotEmpty(mailingList)) 1020 { 1021 project.setMailingList(mailingList); 1022 } 1023 else 1024 { 1025 project.removeMailingList(); 1026 } 1027 1028 project.setInscriptionStatus(inscriptionStatus); 1029 project.setDefaultProfile(defaultProfile); 1030 1031 project.saveChanges(); 1032 1033 // Notify observers 1034 Map<String, Object> eventParams = new HashMap<>(); 1035 eventParams.put(ObservationConstants.ARGS_PROJECT, project); 1036 _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_UPDATED, _currentUserProvider.getUser(), eventParams)); 1037 } 1038 1039 /** 1040 * Delete a list of project. 1041 * @param ids The ids of projects to delete 1042 * @return The ids of the deleted projects, unknowns projects and the deleted sites 1043 */ 1044 @Callable(right = ProjectConstants.RIGHT_PROJECT_DELETE, context = "/admin") 1045 public Map<String, Object> deleteProjectsByIds(List<String> ids) 1046 { 1047 Map<String, Object> result = new HashMap<>(); 1048 List<Map<String, Object>> deleted = new ArrayList<>(); 1049 List<String> unknowns = new ArrayList<>(); 1050 1051 for (String id : ids) 1052 { 1053 try 1054 { 1055 Project project = _resolver.resolveById(id); 1056 1057 Map<String, Object> projectInfo = new HashMap<>(); 1058 projectInfo.put("id", id); 1059 projectInfo.put("title", project.getTitle()); 1060 projectInfo.put("sites", deleteProject(project)); 1061 1062 deleted.add(projectInfo); 1063 } 1064 catch (UnknownAmetysObjectException e) 1065 { 1066 getLogger().warn(String.format("Unable to delete the definition of id '%s', because it does not exist.", id), e); 1067 unknowns.add(id); 1068 } 1069 } 1070 1071 result.put("deleted", deleted); 1072 result.put("unknowns", unknowns); 1073 1074 return result; 1075 } 1076 1077 /** 1078 * Delete a project. 1079 * @param projects The list of projects to delete 1080 * @return list of deleted sites (each list entry contains a data map with 1081 * the id and the name of the delete site). 1082 */ 1083 public List<Map<String, String>> deleteProject(List<Project> projects) 1084 { 1085 List<Map<String, String>> deletedSitesInfo = new ArrayList<>(); 1086 1087 for (Project project : projects) 1088 { 1089 deletedSitesInfo.addAll(deleteProject(project)); 1090 } 1091 1092 return deletedSitesInfo; 1093 } 1094 1095 /** 1096 * Delete a project and its sites 1097 * @param project The project to delete 1098 * @return list of deleted sites (each list entry contains a data map with 1099 * the id and the name of the delete site). 1100 */ 1101 public List<Map<String, String>> deleteProject(Project project) 1102 { 1103 ModifiableAmetysObject parent = project.getParent(); 1104 1105 1106 // list of map entry with id, name and title property 1107 // { id: site id, name: site name } 1108 List<Map<String, String>> deletedSitesInfo = new ArrayList<>(); 1109 1110 Site site = project.getSite(); 1111 if (site != null) 1112 { 1113 try 1114 { 1115 Map<String, String> siteProps = new HashMap<>(); 1116 siteProps.put("id", site.getId()); 1117 siteProps.put("name", site.getName()); 1118 1119 _siteDao.deleteSite(site.getId()); 1120 deletedSitesInfo.add(siteProps); 1121 } 1122 catch (RepositoryException e) 1123 { 1124 String errorMsg = String.format("Error while trying to delete the site '%s' for the project '%s'.", site.getName(), project.getName()); 1125 getLogger().error(errorMsg, e); 1126 } 1127 } 1128 1129 String projectId = project.getId(); 1130 Collection<ProjectMember> projectMembers = _projectMembers.getProjectMembers(project, true); 1131 project.remove(); 1132 parent.saveChanges(); 1133 1134 // Notify observers 1135 Map<String, Object> eventParams = new HashMap<>(); 1136 eventParams.put(ObservationConstants.ARGS_PROJECT_ID, projectId); 1137 eventParams.put(ObservationConstants.ARGS_PROJECT_NAME, project.getName()); 1138 eventParams.put(ObservationConstants.ARGS_PROJECT_MEMBERS, projectMembers); 1139 _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_DELETED, _currentUserProvider.getUser(), eventParams)); 1140 1141 clearCaches(); 1142 1143 return deletedSitesInfo; 1144 } 1145 1146 /** 1147 * Utility method to get or create an ametys object 1148 * @param <A> A sub class of AmetysObject 1149 * @param parent The parent object 1150 * @param name The ametys object name 1151 * @param type The ametys object type 1152 * @return ametys object 1153 * @throws AmetysRepositoryException if an repository error occurs 1154 */ 1155 private <A extends AmetysObject> A _getOrCreateObject(ModifiableTraversableAmetysObject parent, String name, String type) throws AmetysRepositoryException 1156 { 1157 A object; 1158 1159 if (parent.hasChild(name)) 1160 { 1161 object = parent.getChild(name); 1162 } 1163 else 1164 { 1165 object = parent.createChild(name, type); 1166 parent.saveChanges(); 1167 } 1168 1169 return object; 1170 } 1171 1172 /** 1173 * Get the project of an ametys object inside a project. 1174 * It can be an explorer node, or any type of resource in a module. 1175 * @param id The identifier of the ametys object 1176 * @return the project or null if not found 1177 */ 1178 public Project getParentProject(String id) 1179 { 1180 return getParentProject(_resolver.<AmetysObject>resolveById(id)); 1181 } 1182 1183 /** 1184 * Get the project of an ametys object inside a project. 1185 * It can be an explorer node, or any type of resource in a module. 1186 * @param object The ametys object 1187 * @return the project or null if not found 1188 */ 1189 public Project getParentProject(AmetysObject object) 1190 { 1191 AmetysObject ametysObject = object; 1192 // Go back to the local explorer root. 1193 do 1194 { 1195 ametysObject = ametysObject.getParent(); 1196 } 1197 while (ametysObject instanceof ExplorerNode); 1198 1199 if (!(ametysObject instanceof Project)) 1200 { 1201 getLogger().warn(String.format("No project found for ametys object with id '%s'", ametysObject.getId())); 1202 return null; 1203 } 1204 1205 return (Project) ametysObject; 1206 } 1207 1208 /** 1209 * Get the list of project names for a given site 1210 * @param siteName The site name 1211 * @return the list of project names 1212 */ 1213 @Callable(right = "Runtime_Rights_Admin_Access", context = "/admin") 1214 public List<String> getProjectsForSite(String siteName) 1215 { 1216 Cache<String, List<Pair<String, String>>> cache = _getMemorySiteAssociationCache(); 1217 if (cache.hasKey(siteName)) 1218 { 1219 return cache.get(siteName).stream() 1220 .map(p -> p.getRight()) 1221 .collect(Collectors.toList()); 1222 } 1223 else 1224 { 1225 List<String> projectNames = new ArrayList<>(); 1226 1227 if (_siteManager.hasSite(siteName)) 1228 { 1229 Site site = _siteManager.getSite(siteName); 1230 getProjectsForSite(site) 1231 .stream() 1232 .map(Project::getName) 1233 .forEach(projectNames::add); 1234 } 1235 1236 return projectNames; 1237 } 1238 } 1239 1240 /** 1241 * Get the list of project for a given site 1242 * @param site The site 1243 * @return the list of project 1244 */ 1245 public List<Project> getProjectsForSite(Site site) 1246 { 1247 Cache<String, List<Pair<String, String>>> cache = _getMemorySiteAssociationCache(); 1248 if (cache.hasKey(site.getName())) 1249 { 1250 cache.get(site.getName()).stream() 1251 .map(p -> _resolver.resolveById(p.getLeft())) 1252 .collect(Collectors.toList()); 1253 } 1254 1255 try 1256 { 1257 // Stream over the weak reference properties pointing to this 1258 // node to find referencing projects 1259 Iterator<Property> propertyIterator = site.getNode().getWeakReferences(); 1260 Iterable<Property> propertyIterable = () -> propertyIterator; 1261 1262 List<Project> projects = StreamSupport.stream(propertyIterable.spliterator(), false) 1263 .map(p -> 1264 { 1265 try 1266 { 1267 Node parent = p.getParent(); 1268 1269 // Check if the parent is a project" 1270 if (NodeTypeHelper.isNodeType(parent, "ametys:project")) 1271 { 1272 Project project = _resolver.resolve(parent, false); 1273 return project; 1274 } 1275 } 1276 catch (Exception e) 1277 { 1278 if (getLogger().isWarnEnabled()) 1279 { 1280 // this weak reference is not from a project 1281 String propertyPath = null; 1282 try 1283 { 1284 propertyPath = p.getPath(); 1285 } 1286 catch (Exception e2) 1287 { 1288 // ignore 1289 } 1290 1291 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); 1292 getLogger().warn(warnMsg); 1293 } 1294 } 1295 1296 return null; 1297 }) 1298 .filter(Objects::nonNull) 1299 .collect(Collectors.toList()); 1300 1301 List<Pair<String, String>> projectsPairs = projects.stream().map(p -> Pair.of(p.getId(), p.getName())).collect(Collectors.toList()); 1302 cache.put(site.getName(), projectsPairs); 1303 return projects; 1304 } 1305 catch (RepositoryException e) 1306 { 1307 getLogger().error(String.format("Unable to find projects for site '%s'", site.getName()), e); 1308 } 1309 1310 return new ArrayList<>(); 1311 } 1312 1313 /** 1314 * Create the project workspace for a given project. 1315 * @param project The project for which the workspace must be created 1316 * @param errors A list of possible errors to populate. Can be null if the caller is not interested in error tracking. 1317 * @return The site created for this workspace 1318 */ 1319 protected Site _createProjectWorkspace(Project project, List<String> errors) 1320 { 1321 String initialSiteName = project.getName(); 1322 Site site = null; 1323 1324 Site catalogSite = _siteManager.getSite(getCatalogSiteName()); 1325 String rootId = catalogSite != null ? catalogSite.getId() : null; 1326 1327 Map<String, Object> result = _siteDao.createSite(rootId, initialSiteName, ProjectWorkspaceSiteType.TYPE_ID, true); 1328 1329 String siteId = (String) result.get("id"); 1330 String siteName = (String) result.get("name"); 1331 if (StringUtils.isNotEmpty(siteId)) 1332 { 1333 // Creation success 1334 site = _siteManager.getSite(siteName); 1335 1336 setProjectSiteTitle(site, project.getTitle()); 1337 1338 // Add site to project 1339 project.setSite(site); 1340 1341 site.saveChanges(); 1342 } 1343 1344 return site; 1345 } 1346 1347 /** 1348 * Get the project's tags 1349 * @return The project's tags 1350 */ 1351 @Callable 1352 public List<String> getTags() 1353 { 1354 AmetysObject projectsRootNode = getProjectsRoot(); 1355 if (projectsRootNode instanceof JCRAmetysObject) 1356 { 1357 Node node = ((JCRAmetysObject) projectsRootNode).getNode(); 1358 1359 try 1360 { 1361 return Arrays.stream(node.getProperty(__PROJECTS_TAGS_PROPERTY).getValues()) 1362 .map(LambdaUtils.wrap(Value::getString)) 1363 .collect(Collectors.toList()); 1364 } 1365 catch (PathNotFoundException e) 1366 { 1367 // property is not set, empty list will be returned. 1368 } 1369 catch (RepositoryException e) 1370 { 1371 throw new AmetysRepositoryException(e); 1372 } 1373 } 1374 1375 return new ArrayList<>(); 1376 } 1377 1378 /** 1379 * Set the tags 1380 * @param tags The tags to set 1381 */ 1382 @Callable 1383 public synchronized void setTags(List<String> tags) 1384 { 1385 AmetysObject projectsRootNode = getProjectsRoot(); 1386 if (projectsRootNode instanceof JCRAmetysObject) 1387 { 1388 JCRAmetysObject jcrProjectsRootNode = (JCRAmetysObject) projectsRootNode; 1389 1390 if (CollectionUtils.isNotEmpty(tags)) 1391 { 1392 String[] tagsArray = tags.stream() 1393 .map(String::trim) 1394 .map(String::toLowerCase) 1395 .filter(StringUtils::isNotEmpty) 1396 .distinct() 1397 .toArray(String[]::new); 1398 1399 try 1400 { 1401 jcrProjectsRootNode.getNode().setProperty(__PROJECTS_TAGS_PROPERTY, tagsArray); 1402 jcrProjectsRootNode.saveChanges(); 1403 } 1404 catch (RepositoryException e) 1405 { 1406 throw new AmetysRepositoryException(e); 1407 } 1408 } 1409 else 1410 { 1411 Node node = jcrProjectsRootNode.getNode(); 1412 try 1413 { 1414 if (node.hasProperty(__PROJECTS_TAGS_PROPERTY)) 1415 { 1416 node.getProperty(__PROJECTS_TAGS_PROPERTY).remove(); 1417 jcrProjectsRootNode.saveChanges(); 1418 } 1419 } 1420 catch (RepositoryException e) 1421 { 1422 throw new AmetysRepositoryException(e); 1423 } 1424 } 1425 } 1426 } 1427 1428 /** 1429 * Add project's tags 1430 * @param newTags The new tags to add 1431 */ 1432 @Callable 1433 public synchronized void addTags(Collection<String> newTags) 1434 { 1435 if (CollectionUtils.isNotEmpty(newTags)) 1436 { 1437 AmetysObject projectsRootNode = getProjectsRoot(); 1438 if (projectsRootNode instanceof JCRAmetysObject) 1439 { 1440 // Concat existing tags with new lowercased tags 1441 String[] tags = Stream.concat(getTags().stream(), newTags.stream().map(String::trim).map(String::toLowerCase).filter(StringUtils::isNotEmpty)) 1442 .distinct() 1443 .toArray(String[]::new); 1444 1445 try 1446 { 1447 ((JCRAmetysObject) projectsRootNode).getNode().setProperty(__PROJECTS_TAGS_PROPERTY, tags); 1448 } 1449 catch (RepositoryException e) 1450 { 1451 throw new AmetysRepositoryException(e); 1452 } 1453 } 1454 } 1455 } 1456 1457 /** 1458 * Get the project's places 1459 * @return The project's places 1460 */ 1461 @Callable 1462 public List<String> getPlaces() 1463 { 1464 AmetysObject projectsRootNode = getProjectsRoot(); 1465 if (projectsRootNode instanceof JCRAmetysObject) 1466 { 1467 Node node = ((JCRAmetysObject) projectsRootNode).getNode(); 1468 1469 try 1470 { 1471 return Arrays.stream(node.getProperty(__PROJECTS_PLACES_PROPERTY).getValues()) 1472 .map(LambdaUtils.wrap(Value::getString)) 1473 .collect(Collectors.toList()); 1474 } 1475 catch (PathNotFoundException e) 1476 { 1477 // property is not set, empty list will be returned. 1478 } 1479 catch (RepositoryException e) 1480 { 1481 throw new AmetysRepositoryException(e); 1482 } 1483 } 1484 1485 return new ArrayList<>(); 1486 } 1487 1488 /** 1489 * Add project's places 1490 * @param newPlaces The new places to add 1491 */ 1492 public synchronized void addPlaces(Collection<String> newPlaces) 1493 { 1494 if (CollectionUtils.isNotEmpty(newPlaces)) 1495 { 1496 AmetysObject projectsRootNode = getProjectsRoot(); 1497 if (projectsRootNode instanceof JCRAmetysObject) 1498 { 1499 Set<String> lowercasedPlaces = new HashSet<>(); 1500 1501 // Concat existing places with new places 1502 String[] places = Stream.concat(getPlaces().stream(), newPlaces.stream().map(String::trim).filter(StringUtils::isNotEmpty)) 1503 // duplicates are filtered out 1504 .filter(p -> lowercasedPlaces.add(p.toLowerCase())) 1505 .toArray(String[]::new); 1506 1507 try 1508 { 1509 ((JCRAmetysObject) projectsRootNode).getNode().setProperty(__PROJECTS_PLACES_PROPERTY, places); 1510 } 1511 catch (RepositoryException e) 1512 { 1513 throw new AmetysRepositoryException(e); 1514 } 1515 } 1516 } 1517 } 1518 1519 /** 1520 * Set the places 1521 * @param places The places to set 1522 */ 1523 @Callable 1524 public synchronized void setPlaces(List<String> places) 1525 { 1526 AmetysObject projectsRootNode = getProjectsRoot(); 1527 if (projectsRootNode instanceof JCRAmetysObject) 1528 { 1529 JCRAmetysObject jcrProjectsRootNode = (JCRAmetysObject) projectsRootNode; 1530 1531 if (CollectionUtils.isNotEmpty(places)) 1532 { 1533 Set<String> lowercasedPlaces = new HashSet<>(); 1534 1535 String[] placesArray = places.stream() 1536 .map(String::trim) 1537 .filter(StringUtils::isNotEmpty) 1538 // duplicates are filtered out 1539 .filter(p -> lowercasedPlaces.add(p.toLowerCase())) 1540 .toArray(String[]::new); 1541 1542 try 1543 { 1544 jcrProjectsRootNode.getNode().setProperty(__PROJECTS_PLACES_PROPERTY, placesArray); 1545 jcrProjectsRootNode.saveChanges(); 1546 } 1547 catch (RepositoryException e) 1548 { 1549 throw new AmetysRepositoryException(e); 1550 } 1551 } 1552 else 1553 { 1554 Node node = jcrProjectsRootNode.getNode(); 1555 try 1556 { 1557 if (node.hasProperty(__PROJECTS_PLACES_PROPERTY)) 1558 { 1559 node.getProperty(__PROJECTS_PLACES_PROPERTY).remove(); 1560 jcrProjectsRootNode.saveChanges(); 1561 } 1562 } 1563 catch (RepositoryException e) 1564 { 1565 throw new AmetysRepositoryException(e); 1566 } 1567 } 1568 } 1569 } 1570 1571 /** 1572 * Get the list of activated modules for a project 1573 * @param project The project 1574 * @return The list of activated modules 1575 */ 1576 public List<WorkspaceModule> getModules(Project project) 1577 { 1578 return _moduleManagerEP.getModules().stream() 1579 .filter(module -> isModuleActivated(project, module.getId())) 1580 .collect(Collectors.toList()); 1581 } 1582 1583 /** 1584 * Retrieves the page of the module for all available languages 1585 * @param project The project 1586 * @param moduleId The project module id 1587 * @return the page or null if not found 1588 */ 1589 public Set<Page> getModulePages(Project project, String moduleId) 1590 { 1591 if (_moduleManagerEP.hasExtension(moduleId)) 1592 { 1593 WorkspaceModule module = _moduleManagerEP.getExtension(moduleId); 1594 return getModulePages(project, module); 1595 } 1596 return null; 1597 } 1598 1599 /** 1600 * Return the possible module roots associated to a page 1601 * @param page The given page 1602 * @return A non null set of the data of the linked modules 1603 */ 1604 public Set<ModifiableResourceCollection> pageToModuleRoot(Page page) 1605 { 1606 Set<ModifiableResourceCollection> data = new LinkedHashSet<>(); 1607 1608 Page rootPage = page; 1609 SitemapElement parent = page.getParent(); 1610 while (!(parent instanceof Sitemap) && !page.hasValue(__PAGE_MODULES_VALUE)) 1611 { 1612 rootPage = (Page) parent; 1613 parent = parent.getParent(); 1614 } 1615 1616 String[] modulesRootsIds = rootPage.getValue(__PAGE_MODULES_VALUE, new String[0]); 1617 if (modulesRootsIds.length > 0) 1618 { 1619 for (String moduleRootId : modulesRootsIds) 1620 { 1621 try 1622 { 1623 ModifiableResourceCollection moduleRoot = _resolver.resolveById(moduleRootId); 1624 data.add(moduleRoot); 1625 } 1626 catch (UnknownAmetysObjectException e) 1627 { 1628 // Ignore obsolete data 1629 } 1630 } 1631 } 1632 1633 return data; 1634 } 1635 1636 /** 1637 * Mark the given page as this module page. The modified page will not be saved. 1638 * @param page The page to change 1639 * @param moduleRoot The workspace module that use this page 1640 */ 1641 public void tagProjectPage(ModifiablePage page, ModifiableResourceCollection moduleRoot) 1642 { 1643 String[] currentModules = page.getValue(__PAGE_MODULES_VALUE, new String[0]); 1644 1645 Set<String> modules = new LinkedHashSet<>(Arrays.asList(currentModules)); 1646 modules.add(moduleRoot.getId()); 1647 1648 String[] newModules = new String[modules.size()]; 1649 modules.toArray(newModules); 1650 1651 page.setValue(__PAGE_MODULES_VALUE, newModules); 1652 } 1653 1654 /** 1655 * Remove the mark on the given page of this module. The modified page will not be saved. 1656 * @param page The page to change 1657 * @param moduleRoot The workspace module that use this page 1658 */ 1659 public void untagProjectPage(ModifiablePage page, ModifiableResourceCollection moduleRoot) 1660 { 1661 if (moduleRoot != null) 1662 { 1663 String[] currentModules = page.getValue(__PAGE_MODULES_VALUE, new String[0]); 1664 1665 Set<String> modules = new LinkedHashSet<>(Arrays.asList(currentModules)); 1666 modules.remove(moduleRoot.getId()); 1667 1668 String[] newModules = new String[modules.size()]; 1669 modules.toArray(newModules); 1670 1671 page.setValue(__PAGE_MODULES_VALUE, newModules); 1672 } 1673 } 1674 1675 /** 1676 * Get a page in the site of a given project with a specific tag 1677 * @param project The project 1678 * @param workspaceModule the module 1679 * @return The module's pages 1680 */ 1681 public Set<Page> getModulePages(Project project, WorkspaceModule workspaceModule) 1682 { 1683 Request request = _getRequest(); 1684 if (request == null) 1685 { 1686 // There is no request to store cache 1687 return _computePages(project, workspaceModule); 1688 } 1689 1690 Cache<RequestModuleCacheKey, Set<Page>> pagesCache = _getRequestPageCache(); 1691 1692 // The site key in the cache is of the form {site + workspace}. 1693 String currentWorkspace = _workspaceSelector.getWorkspace(); 1694 RequestModuleCacheKey pagesKey = RequestModuleCacheKey.of(project.getName(), workspaceModule.getId(), currentWorkspace); 1695 1696 try 1697 { 1698 return pagesCache.get(pagesKey, __ -> _computePages(project, workspaceModule)); 1699 } 1700 catch (CacheException e) 1701 { 1702 if (e.getCause() instanceof UnknownAmetysObjectException) 1703 { 1704 throw (UnknownAmetysObjectException) e.getCause(); 1705 } 1706 else 1707 { 1708 throw new RuntimeException("An error occurred while computing page of module " + workspaceModule.getModuleName() + " in project " + project.getName(), e); 1709 } 1710 } 1711 } 1712 1713 private Set<Page> _computePages(Project project, WorkspaceModule workspaceModule) 1714 { 1715 Set<String> pagesUUids = _getMemoryPageCache().get(ModuleCacheKey.of(project.getName(), workspaceModule.getId()), __ -> _computePagesIds(project, workspaceModule)); 1716 if (pagesUUids != null) 1717 { 1718 return pagesUUids.stream().map(uuid -> _resolver.<Page>resolveById(uuid)).collect(Collectors.toSet()); 1719 } 1720 else 1721 { 1722 // Project may be present in cache for 'default' workspace but does not exist in current JCR workspace 1723 throw new UnknownAmetysObjectException("There is no pages for '" + project.getName() + "', module '" + workspaceModule.getModuleName() + "'"); 1724 } 1725 } 1726 1727 private Set<String> _computePagesIds(Project project, WorkspaceModule workspaceModule) 1728 { 1729 Site site = project.getSite(); 1730 String siteName = site != null ? site.getName() : null; 1731 if (StringUtils.isEmpty(siteName)) 1732 { 1733 return null; 1734 } 1735 1736 ModifiableResourceCollection moduleRoot = workspaceModule.getModuleRoot(project, false); 1737 if (moduleRoot != null) 1738 { 1739 Expression expression = new StringExpression(__PAGE_MODULES_VALUE, Operator.EQ, moduleRoot.getId()); 1740 String query = PageQueryHelper.getPageXPathQuery(siteName, null, null, expression, null); 1741 1742 return StreamSupport.stream(_resolver.query(query).spliterator(), false) 1743 .map(page -> page.getId()) 1744 .collect(Collectors.toSet()); 1745 } 1746 else 1747 { 1748 return Set.of(); 1749 } 1750 } 1751 1752 /** 1753 * Activate the list of module of the project 1754 * @param project The project 1755 * @param moduleIds The list of modules. Can be null to activate all modules 1756 * @param additionalValues A list of optional additional values. Accepted values are : description, mailingList, inscriptionStatus, defaultProfile, tags, categoryTags, keywords and language 1757 */ 1758 public void activateModules(Project project, Set<String> moduleIds, Map<String, Object> additionalValues) 1759 { 1760 Set<String> modules = moduleIds == null ? _moduleManagerEP.getExtensionsIds() : moduleIds; 1761 1762 for (String moduleId : modules) 1763 { 1764 WorkspaceModule module = _moduleManagerEP.getModule(moduleId); 1765 if (module != null && !isModuleActivated(project, moduleId)) 1766 { 1767 module.activateModule(project, additionalValues); 1768 project.addModule(moduleId); 1769 } 1770 } 1771 1772 project.saveChanges(); 1773 } 1774 1775 /** 1776 * Initialize the sitemap with the active module of the project 1777 * @param project The project 1778 * @param sitemap The sitemap 1779 */ 1780 public void initializeModulesSitemap(Project project, Sitemap sitemap) 1781 { 1782 Set<String> modules = _moduleManagerEP.getExtensionsIds(); 1783 1784 for (String moduleId : modules) 1785 { 1786 if (_moduleManagerEP.hasExtension(moduleId)) 1787 { 1788 WorkspaceModule module = _moduleManagerEP.getExtension(moduleId); 1789 1790 if (isModuleActivated(project, moduleId)) 1791 { 1792 module.initializeSitemap(project, sitemap); 1793 } 1794 } 1795 } 1796 } 1797 1798 /** 1799 * Determines if a module is activated 1800 * @param project The project 1801 * @param moduleId The id of module 1802 * @return true if the module the currently activated 1803 */ 1804 public boolean isModuleActivated(Project project, String moduleId) 1805 { 1806 return ArrayUtils.contains(project.getModules(), moduleId); 1807 } 1808 1809 /** 1810 * Remove the explorer root node of the project module, remove all events 1811 * related to that module and set it to deactivated 1812 * @param project The project 1813 * @param moduleIds The id of module to activate 1814 */ 1815 public void deactivateModules(Project project, Set<String> moduleIds) 1816 { 1817 for (String moduleId : moduleIds) 1818 { 1819 WorkspaceModule module = _moduleManagerEP.getModule(moduleId); 1820 if (module != null && isModuleActivated(project, moduleId)) 1821 { 1822 module.deactivateModule(project); 1823 project.removeModule(moduleId); 1824 } 1825 } 1826 1827 project.saveChanges(); 1828 } 1829 1830 1831 /** 1832 * Get the list of profiles configured for the workspaces' projects 1833 * @return The list of profiles as JSON 1834 */ 1835 @Callable 1836 public Map<String, Object> getProjectProfiles() 1837 { 1838 Map<String, Object> result = new HashMap<>(); 1839 List<Map<String, Object>> profiles = _projectRightHelper.getProfiles().stream().map(p -> p.toJSON()).collect(Collectors.toList()); 1840 result.put("profiles", profiles); 1841 return result; 1842 } 1843 1844 /** 1845 * Get the tags from the projects 1846 * @param projectIds The ids of the projects 1847 * @return the tags of the projects 1848 */ 1849 @Callable 1850 public Set<String> getTags(List<String> projectIds) 1851 { 1852 Set<String> tags = new HashSet<>(); 1853 1854 for (String projectId : projectIds) 1855 { 1856 Project project = _resolver.resolveById(projectId); 1857 tags.addAll(project.getTags()); 1858 } 1859 1860 return tags; 1861 } 1862 1863 /** 1864 * Tag the projects 1865 * @param projectIds the project ids 1866 * @param tagNames the tag names 1867 * @param contextualParameters the contextuals parameters 1868 * @return results 1869 */ 1870 @Callable 1871 public Map<String, Object> tag(List<String> projectIds, List<String> tagNames, Map<String, Object> contextualParameters) 1872 { 1873 return tag(projectIds, tagNames, TagMode.REPLACE.toString(), contextualParameters); 1874 } 1875 1876 /** 1877 * Tag the projects 1878 * @param projectIds the project ids 1879 * @param tagNames the tag names 1880 * @param mode the mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags. 1881 * @param contextualParameters the contextual parameters 1882 * @return results 1883 */ 1884 @Callable 1885 public Map<String, Object> tag(List<String> projectIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters) 1886 { 1887 Map<String, Object> result = new HashMap<>(); 1888 1889 result.put("invalid-tags", new ArrayList<String>()); 1890 result.put("allright-projects", new ArrayList<Map<String, Object>>()); 1891 1892 for (String projectId : projectIds) 1893 { 1894 Project project = _resolver.resolveById(projectId); 1895 1896 Map<String, Object> project2json = new HashMap<>(); 1897 project2json.put("id", project.getId()); 1898 project2json.put("title", project.getTitle()); 1899 1900 TagMode tagMode = TagMode.valueOf(mode); 1901 1902 Set<String> oldTags = project.getTags(); 1903 if (TagMode.REPLACE.equals(tagMode)) 1904 { 1905 // First delete old tags 1906 for (String tagName : oldTags) 1907 { 1908 project.untag(tagName); 1909 } 1910 } 1911 1912 // Then set new tags 1913 for (String tagName : tagNames) 1914 { 1915 if (isTagValid(tagName)) 1916 { 1917 if (TagMode.REMOVE.equals(tagMode)) 1918 { 1919 project.untag(tagName); 1920 } 1921 else if (TagMode.REPLACE.equals(tagMode) || !oldTags.contains(tagName)) 1922 { 1923 project.tag(tagName); 1924 } 1925 } 1926 else 1927 { 1928 @SuppressWarnings("unchecked") 1929 List<String> invalidTags = (List<String>) result.get("invalid-tags"); 1930 invalidTags.add(tagName); 1931 } 1932 } 1933 1934 project.saveChanges(); 1935 1936 project2json.put("tags", project.getTags()); 1937 @SuppressWarnings("unchecked") 1938 List<Map<String, Object>> allRightProjects = (List<Map<String, Object>>) result.get("allright-projects"); 1939 allRightProjects.add(project2json); 1940 1941 if (!oldTags.equals(project.getTags())) 1942 { 1943 // Notify observers that the project has been tagged 1944 Map<String, Object> eventParams = new HashMap<>(); 1945 eventParams.put(ObservationConstants.ARGS_PROJECT, project); 1946 _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_UPDATED, _currentUserProvider.getUser(), eventParams)); 1947 } 1948 } 1949 1950 return result; 1951 } 1952 1953 /** 1954 * Test if a tag is valid 1955 * @param tagName The tag name 1956 * @return True if the tag is valid 1957 */ 1958 public boolean isTagValid (String tagName) 1959 { 1960 Map<String, Object> params = new HashMap<>(); 1961 Tag tag = _projectTagProviderEP.getTag(tagName, params); 1962 1963 return tag != null; 1964 } 1965 1966 /** 1967 * Get the site name holding the catalog of projects 1968 * @return the catalog's site name 1969 */ 1970 public String getCatalogSiteName() 1971 { 1972 String catalogSiteName = Config.getInstance().getValue("workspaces.catalog.site.name"); 1973 if (!_siteManager.hasSite(catalogSiteName)) 1974 { 1975 throw new IllegalArgumentException("Unknown site '" + catalogSiteName + "'. The global Ametys configuration is invalid for the parameter 'workspaces.catalog.site.name'"); 1976 } 1977 return catalogSiteName; 1978 } 1979 1980 /** 1981 * Get the site name holding the users directory 1982 * @return the users directory's site name 1983 */ 1984 public String getUsersDirectorySiteName() 1985 { 1986 String udSiteName = Config.getInstance().getValue("workspaces.member.userdirectory.site.name"); 1987 if (!_siteManager.hasSite(udSiteName)) 1988 { 1989 throw new IllegalArgumentException("Unknown site '" + udSiteName + "'. The global Ametys configuration is invalid for the parameter 'workspaces.member.userdirectory.site.name'"); 1990 } 1991 return udSiteName; 1992 } 1993 1994 @Override 1995 public boolean supports(Event event) 1996 { 1997 return event.getId().equals(ObservationConstants.EVENT_PROJECT_DELETED) 1998 || event.getId().equals(ObservationConstants.EVENT_PROJECT_UPDATED) 1999 || event.getId().equals(ObservationConstants.EVENT_PROJECT_ADDED) 2000 || event.getId().equals(org.ametys.web.ObservationConstants.EVENT_PAGE_ADDED) 2001 || event.getId().equals(org.ametys.web.ObservationConstants.EVENT_PAGE_DELETED); 2002 } 2003 2004 public int getPriority(Event event) 2005 { 2006 return 0; 2007 } 2008 2009 public void observe(Event event, Map<String, Object> transientVars) throws Exception 2010 { 2011 clearCaches(); 2012 } 2013 2014 /** 2015 * Prefix project title 2016 * @param site the site 2017 * @param title the title 2018 */ 2019 public void setProjectSiteTitle(Site site, String title) 2020 { 2021 I18nizableText i18nSiteTitle = new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_DEFAULT_PROJECT_WORKSPACE_TITLE", Arrays.asList(title)); 2022 site.setTitle(_i18nUtils.translate(i18nSiteTitle)); 2023 } 2024 2025 private Project _computeProject(String projectName) 2026 { 2027 if (hasProject(projectName)) 2028 { 2029 String uuid = _getUUIDCache().get(projectName); 2030 return _resolver.<Project>resolveById(uuid); 2031 } 2032 else 2033 { 2034 // Project may be present in cache for 'default' workspace but does not exist in current JCR workspace 2035 throw new UnknownAmetysObjectException("There is no site named '" + projectName + "'"); 2036 } 2037 } 2038 2039 /** 2040 * Check rights to create project 2041 * @param inscriptionStatus the inscription status 2042 * @param catalogPage the catalog page where projects are created 2043 */ 2044 public void checkRightsForProjectCreation(InscriptionStatus inscriptionStatus, SitemapElement catalogPage) 2045 { 2046 switch (inscriptionStatus) 2047 { 2048 case PRIVATE: 2049 boolean hasRightToCreatePrivateProjetOnPages = catalogPage != null ? _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PRIVATE, catalogPage) == RightResult.RIGHT_ALLOW : false; 2050 if (!hasRightToCreatePrivateProjetOnPages && _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PRIVATE, "/${WorkspaceName}") != RightResult.RIGHT_ALLOW) 2051 { 2052 throw new IllegalAccessError("Can't have rights to create private project"); 2053 } 2054 break; 2055 case MODERATED: 2056 boolean hasRightToCreateModeratedProjetOnPages = catalogPage != null ? _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_MODERATED, catalogPage) == RightResult.RIGHT_ALLOW : false; 2057 if (!hasRightToCreateModeratedProjetOnPages && _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_MODERATED, "/${WorkspaceName}") != RightResult.RIGHT_ALLOW) 2058 { 2059 throw new IllegalAccessError("Can't have rights to create public project with moderation"); 2060 } 2061 break; 2062 case OPEN: 2063 boolean hasRightToCreateOpenProjetOnPages = catalogPage != null ? _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_OPENED, catalogPage) == RightResult.RIGHT_ALLOW : false; 2064 if (!hasRightToCreateOpenProjetOnPages && _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_OPENED, "/${WorkspaceName}") != RightResult.RIGHT_ALLOW) 2065 { 2066 throw new IllegalAccessError("Can't have rights to create public project"); 2067 } 2068 break; 2069 default: 2070 throw new IllegalArgumentException("Inscription status '" + inscriptionStatus.toString() + "' is unknown"); 2071 } 2072 } 2073 2074 /** 2075 * Check rights to edit project 2076 * @param project the project 2077 * @param inscriptionStatus the inscription status 2078 * @param catalogPage the catalog page where projects are created 2079 */ 2080 public void checkRightsForProjectEdition(Project project, InscriptionStatus inscriptionStatus, SitemapElement catalogPage) 2081 { 2082 InscriptionStatus oldInscriptionStatus = project.getInscriptionStatus(); 2083 if (oldInscriptionStatus != inscriptionStatus) 2084 { 2085 boolean canCreatePrivateProjet = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PRIVATE, "/${WorkspaceName}") == RightResult.RIGHT_ALLOW 2086 || (catalogPage != null ? _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PRIVATE, catalogPage) == RightResult.RIGHT_ALLOW : false); 2087 boolean canCreatePublicProjetWithModeration = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_MODERATED, "/${WorkspaceName}") == RightResult.RIGHT_ALLOW 2088 || (catalogPage != null ? _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_MODERATED, catalogPage) == RightResult.RIGHT_ALLOW : false); 2089 boolean canCreatePublicProjet = _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_OPENED, "/${WorkspaceName}") == RightResult.RIGHT_ALLOW 2090 || (catalogPage != null ? _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_CREATE_PUBLIC_OPENED, catalogPage) == RightResult.RIGHT_ALLOW : false); 2091 2092 switch (oldInscriptionStatus) 2093 { 2094 case PRIVATE: 2095 if (!canCreatePrivateProjet) 2096 { 2097 throw new IllegalAccessError("Can't have rights to change the inscription status of private project"); 2098 } 2099 break; 2100 case MODERATED: 2101 if (!canCreatePublicProjetWithModeration) 2102 { 2103 throw new IllegalAccessError("Can't have rights to change the inscription status of public project with moderation"); 2104 } 2105 break; 2106 case OPEN: 2107 if (!canCreatePublicProjet) 2108 { 2109 throw new IllegalAccessError("Can't have rights to change the inscription status of public project"); 2110 } 2111 break; 2112 default: 2113 throw new IllegalArgumentException("Inscription status '" + oldInscriptionStatus.toString() + "' is unknown"); 2114 } 2115 2116 switch (inscriptionStatus) 2117 { 2118 case PRIVATE: 2119 if (!canCreatePrivateProjet) 2120 { 2121 throw new IllegalAccessError("Can't have rights to change the project to private project"); 2122 } 2123 break; 2124 case MODERATED: 2125 if (!canCreatePublicProjetWithModeration) 2126 { 2127 throw new IllegalAccessError("Can't have rights to change the project to public project with moderation"); 2128 } 2129 break; 2130 case OPEN: 2131 if (!canCreatePublicProjet) 2132 { 2133 throw new IllegalAccessError("Can't have rights to change the project to public project"); 2134 } 2135 break; 2136 default: 2137 throw new IllegalArgumentException("Inscription status '" + inscriptionStatus.toString() + "' is unknown"); 2138 } 2139 } 2140 } 2141 2142 /** 2143 * Clear the site cache 2144 */ 2145 public void clearCaches () 2146 { 2147 _getMemorySiteAssociationCache().invalidateAll(); 2148 _getMemoryProjectCache().invalidateAll(); 2149 _getMemoryPageCache().invalidateAll(); 2150 _getRequestProjectCache().invalidateAll(); 2151 _getRequestPageCache().invalidateAll(); 2152 } 2153 2154 private Cache<String, List<Pair<String, String>>> _getMemorySiteAssociationCache() 2155 { 2156 return _cacheManager.get(MEMORY_SITEASSOCIATION_CACHE); 2157 } 2158 2159 private Cache<String, String> _getMemoryProjectCache() 2160 { 2161 return _cacheManager.get(MEMORY_PROJECTIDBYNAMECACHE); 2162 } 2163 2164 private Cache<ModuleCacheKey, Set<String>> _getMemoryPageCache() 2165 { 2166 return _cacheManager.get(MEMORY_PAGESBYIDCACHE); 2167 } 2168 2169 private Cache<RequestProjectCacheKey, Project> _getRequestProjectCache() 2170 { 2171 return _cacheManager.get(REQUEST_PROJECTBYID_CACHE); 2172 } 2173 2174 private Cache<RequestModuleCacheKey, Set<Page>> _getRequestPageCache() 2175 { 2176 return _cacheManager.get(REQUEST_PAGESBYPROJECTANDMODULE_CACHE); 2177 } 2178 2179 2180 /** 2181 * Creates the caches 2182 */ 2183 protected void _createCaches() 2184 { 2185 _cacheManager.createMemoryCache(MEMORY_SITEASSOCIATION_CACHE, 2186 new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CACHE_PROJECT_MANAGER_LABEL"), 2187 new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CACHE_PROJECT_MANAGER_DESCRIPTION"), 2188 true, 2189 null); 2190 _cacheManager.createMemoryCache(MEMORY_PROJECTIDBYNAMECACHE, 2191 new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_UUID_CACHE_LABEL"), 2192 new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_UUID_CACHE_DESCRIPTION"), 2193 true, 2194 null); 2195 _cacheManager.createMemoryCache(MEMORY_PAGESBYIDCACHE, 2196 new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_PAGEUUID_CACHE_LABEL"), 2197 new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_PAGEUUID_CACHE_DESCRIPTION"), 2198 true, 2199 null); 2200 _cacheManager.createRequestCache(REQUEST_PROJECTBYID_CACHE, 2201 new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_REQUEST_CACHE_LABEL"), 2202 new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_REQUEST_CACHE_DESCRIPTION"), 2203 false); 2204 _cacheManager.createRequestCache(REQUEST_PAGESBYPROJECTANDMODULE_CACHE, 2205 new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_PAGEREQUEST_CACHE_LABEL"), 2206 new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_MANAGER_PAGEREQUEST_CACHE_DESCRIPTION"), 2207 false); 2208 } 2209 2210 private synchronized Map<String, String> _getUUIDCache() 2211 { 2212 if (!_getMemoryProjectCache().hasKey(__IS_CACHE_FILLED)) 2213 { 2214 Session defaultSession = null; 2215 try 2216 { 2217 // Force default workspace to execute query 2218 defaultSession = _repository.login(RepositoryConstants.DEFAULT_WORKSPACE); 2219 2220 String jcrQuery = "//element(*, ametys:project)"; 2221 2222 AmetysObjectIterable<Project> projects = _resolver.query(jcrQuery, defaultSession); 2223 2224 for (Project project : projects) 2225 { 2226 _getMemoryProjectCache().put(project.getName(), project.getId()); 2227 } 2228 2229 _getMemoryProjectCache().put(__IS_CACHE_FILLED, null); 2230 } 2231 catch (RepositoryException e) 2232 { 2233 throw new AmetysRepositoryException(e); 2234 } 2235 finally 2236 { 2237 if (defaultSession != null) 2238 { 2239 defaultSession.logout(); 2240 } 2241 } 2242 } 2243 2244 Map<String, String> cacheAsMap = _getMemoryProjectCache().asMap(); 2245 cacheAsMap.remove(__IS_CACHE_FILLED); 2246 return cacheAsMap; 2247 } 2248 2249 private static final class RequestProjectCacheKey extends AbstractCacheKey 2250 { 2251 private RequestProjectCacheKey(String projectName, String workspaceName) 2252 { 2253 super(projectName, workspaceName); 2254 } 2255 2256 static RequestProjectCacheKey of(String projectName, String workspaceName) 2257 { 2258 return new RequestProjectCacheKey(projectName, workspaceName); 2259 } 2260 } 2261 2262 private static final class ModuleCacheKey extends AbstractCacheKey 2263 { 2264 private ModuleCacheKey(String projectName, String moduleId) 2265 { 2266 super(projectName, moduleId); 2267 } 2268 2269 static ModuleCacheKey of(String projectName, String moduleId) 2270 { 2271 return new ModuleCacheKey(projectName, moduleId); 2272 } 2273 } 2274 2275 private static final class RequestModuleCacheKey extends AbstractCacheKey 2276 { 2277 private RequestModuleCacheKey(String projectName, String moduleId, String workspaceName) 2278 { 2279 super(projectName, moduleId, workspaceName); 2280 } 2281 2282 static RequestModuleCacheKey of(String projectName, String moduleId, String workspaceName) 2283 { 2284 return new RequestModuleCacheKey(projectName, moduleId, workspaceName); 2285 } 2286 } 2287 2288 private Request _getRequest () 2289 { 2290 try 2291 { 2292 return (Request) _context.get(ContextHelper.CONTEXT_REQUEST_OBJECT); 2293 } 2294 catch (ContextException ce) 2295 { 2296 getLogger().info("Unable to get the request", ce); 2297 return null; 2298 } 2299 } 2300 2301 /** 2302 * Retrieves all projects for client side 2303 * @return the projects 2304 */ 2305 @Callable(right = "Runtime_Rights_Admin_Access", context = "/admin") 2306 public List<Map<String, Object>> getProjectsStatisticsForClientSide() 2307 { 2308 return getProjects() 2309 .stream() 2310 .map(p -> getProjectStatistics(p)) 2311 .collect(Collectors.toList()); 2312 } 2313 2314 /** 2315 * Retrieves the standard information of a project 2316 * @param project The project 2317 * @return The map of information 2318 */ 2319 public Map<String, Object> getProjectStatistics(Project project) 2320 { 2321 Map<String, Object> statistics = new HashMap<>(); 2322 2323 statistics.put("title", project.getTitle()); 2324 2325 long totalSize = 0; 2326 for (WorkspaceModule moduleManager : _moduleManagerEP.getModules()) 2327 { 2328 Map<String, Object> moduleStatistics = moduleManager.getStatistics(project); 2329 statistics.putAll(moduleStatistics); 2330 Long size = (Long) moduleStatistics.get(moduleManager.getModuleSizeKey()); 2331 totalSize += (size != null && size >= 0) ? (Long) moduleStatistics.get(moduleManager.getModuleSizeKey()) : 0; 2332 } 2333 2334 statistics.put("totalSize", totalSize); 2335 2336 ZonedDateTime creationDate = project.getCreationDate(); 2337 2338 statistics.put("creationDate", creationDate); 2339 statistics.put("managers", Arrays.stream(project.getManagers()) 2340 .map(u -> _userHelper.user2json(u)) 2341 .collect(Collectors.toList())); 2342 2343 return statistics; 2344 } 2345 2346 /** 2347 * Retrieves all projects for client side 2348 * @return the projects 2349 */ 2350 @Callable(right = "Runtime_Rights_Admin_Access", context = "/admin") 2351 public List<Map<String, Object>> getProjectsStatisticsColumnsModel() 2352 { 2353 return getStatisticHeaders() 2354 .stream() 2355 .map(p -> p.convertToJSON()) 2356 .collect(Collectors.toList()); 2357 } 2358 2359 private List<StatisticColumn> getStatisticHeaders() 2360 { 2361 2362 List<StatisticColumn> flatStatisticHeaders = getFlatStatisticHeaders(); 2363 List<StatisticColumn> headers = new ArrayList<>(); 2364 for (StatisticColumn statisticColumn : flatStatisticHeaders) 2365 { 2366 // this column have a parent, we have to find it and attach it 2367 if (statisticColumn.getGroup() != null) 2368 { 2369 Optional<StatisticColumn> parent = flatStatisticHeaders.stream() 2370 .filter(column -> column.getId().equals(statisticColumn.getGroup())) 2371 .findAny(); 2372 if (parent.isPresent()) 2373 { 2374 parent.get().addSubColumn(statisticColumn); 2375 } 2376 } 2377 else 2378 { 2379 headers.add(statisticColumn); 2380 } 2381 } 2382 2383 return headers; 2384 } 2385 2386 private List<StatisticColumn> getFlatStatisticHeaders() 2387 { 2388 List<StatisticColumn> headers = new ArrayList<>(); 2389 headers.add(new StatisticColumn("title", new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_TITLE")) 2390 .withType(StatisticsColumnType.STRING) 2391 .withWidth(200) 2392 .withRenderer("Ametys.plugins.workspaces.project.tool.ProjectsGridHelper.renderTitle")); 2393 headers.add(new StatisticColumn("creationDate", new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_CREATION")) 2394 .withType(StatisticsColumnType.DATE) 2395 .withWidth(150)); 2396 headers.add(new StatisticColumn("managers", new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_MANAGERS")) 2397 .withRenderer("Ametys.plugins.workspaces.project.tool.ProjectsGridHelper.renderManagers") 2398 .withFilter(false)); 2399 for (WorkspaceModule moduleManager : _moduleManagerEP.getModules()) 2400 { 2401 headers.addAll(moduleManager.getStatisticModel()); 2402 } 2403 2404 StatisticColumn elements = new StatisticColumn(WorkspaceModule.GROUP_HEADER_ELEMENTS_ID, new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_ELEMENTS")) 2405 .withFilter(false); 2406 headers.add(elements); 2407 2408 StatisticColumn activatedModules = new StatisticColumn(WorkspaceModule.GROUP_HEADER_ACTIVATED_ID, new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_ACTIVE_MODULES")) 2409 .isHidden(true) 2410 .withFilter(false); 2411 headers.add(activatedModules); 2412 2413 StatisticColumn lastActivity = new StatisticColumn(WorkspaceModule.GROUP_HEADER_LAST_ACTIVITY_ID, new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_LAST_ACTIVITY")).isHidden(true); 2414 headers.add(lastActivity); 2415 2416 StatisticColumn modulesSize = new StatisticColumn(WorkspaceModule.GROUP_HEADER_SIZE_ID, new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_MODULES_SIZE")) 2417 .withFilter(false); 2418 modulesSize.addSubColumn(new StatisticColumn("totalSize", new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_PROJECT_STATISTICS_TOOL_COLUMN_MODULES_SIZE_TOTAL")) 2419 .withRenderer("Ametys.plugins.workspaces.project.tool.ProjectsGridHelper.renderSize") 2420 .withType(StatisticsColumnType.LONG)); 2421 2422 headers.add(modulesSize); 2423 2424 return headers; 2425 } 2426 2427 /** 2428 * Check if the user is in one of the populations of project 2429 * @param project the project 2430 * @param user the user 2431 * @return true if the user is in one of the populations of project 2432 */ 2433 public boolean isUserInProjectPopulations(Project project, UserIdentity user) 2434 { 2435 Site site = project.getSite(); 2436 2437 if (site == null) 2438 { 2439 throw new IllegalArgumentException("Cannot determine if user " + UserIdentity.userIdentityToString(user) + " can connect to the project " + project.getName() + " since the project has no associated site to determine the populations"); 2440 } 2441 String siteName = site.getName(); 2442 2443 Set<String> populations = _populationContextHelper.getUserPopulationsOnContext("/sites/" + siteName, false); 2444 Set<String> frontPopulations = _populationContextHelper.getUserPopulationsOnContext("/sites-fo/" + siteName, false); 2445 2446 return populations.contains(user.getPopulationId()) || frontPopulations.contains(user.getPopulationId()); 2447 } 2448}