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