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