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