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