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