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