001/*
002 *  Copyright 2016 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.HashMap;
023import java.util.HashSet;
024import java.util.Iterator;
025import java.util.List;
026import java.util.Map;
027import java.util.Objects;
028import java.util.Set;
029import java.util.function.Function;
030import java.util.stream.Collectors;
031import java.util.stream.Stream;
032import java.util.stream.StreamSupport;
033
034import javax.jcr.Node;
035import javax.jcr.PathNotFoundException;
036import javax.jcr.Property;
037import javax.jcr.RepositoryException;
038import javax.jcr.Value;
039
040import org.apache.avalon.framework.activity.Initializable;
041import org.apache.avalon.framework.component.Component;
042import org.apache.avalon.framework.context.Context;
043import org.apache.avalon.framework.context.ContextException;
044import org.apache.avalon.framework.context.Contextualizable;
045import org.apache.avalon.framework.logger.AbstractLogEnabled;
046import org.apache.avalon.framework.service.ServiceException;
047import org.apache.avalon.framework.service.ServiceManager;
048import org.apache.avalon.framework.service.Serviceable;
049import org.apache.commons.collections.CollectionUtils;
050import org.apache.commons.lang.ArrayUtils;
051import org.apache.commons.lang3.StringUtils;
052import org.apache.commons.lang3.tuple.Pair;
053
054import org.ametys.cms.repository.ContentDAO.TagMode;
055import org.ametys.cms.tag.Tag;
056import org.ametys.core.cache.AbstractCacheManager;
057import org.ametys.core.cache.AbstractCacheManager.CacheType;
058import org.ametys.core.cache.Cache;
059import org.ametys.core.observation.Event;
060import org.ametys.core.observation.ObservationManager;
061import org.ametys.core.ui.Callable;
062import org.ametys.core.user.CurrentUserProvider;
063import org.ametys.core.user.UserIdentity;
064import org.ametys.core.util.I18nUtils;
065import org.ametys.core.util.LambdaUtils;
066import org.ametys.plugins.core.user.UserHelper;
067import org.ametys.plugins.explorer.ExplorerNode;
068import org.ametys.plugins.repository.AmetysObject;
069import org.ametys.plugins.repository.AmetysObjectIterable;
070import org.ametys.plugins.repository.AmetysObjectResolver;
071import org.ametys.plugins.repository.AmetysRepositoryException;
072import org.ametys.plugins.repository.ModifiableAmetysObject;
073import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
074import org.ametys.plugins.repository.RepositoryConstants;
075import org.ametys.plugins.repository.UnknownAmetysObjectException;
076import org.ametys.plugins.repository.jcr.JCRAmetysObject;
077import org.ametys.plugins.repository.jcr.NodeTypeHelper;
078import org.ametys.plugins.repository.query.expression.Expression;
079import org.ametys.plugins.repository.query.expression.Expression.Operator;
080import org.ametys.plugins.workspaces.ObservationConstants;
081import org.ametys.plugins.workspaces.categories.CategoryProviderExtensionPoint;
082import org.ametys.plugins.workspaces.members.ProjectMemberManager;
083import org.ametys.plugins.workspaces.project.modules.WorkspaceModule;
084import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
085import org.ametys.plugins.workspaces.project.objects.Project;
086import org.ametys.plugins.workspaces.project.rights.ProjectRightHelper;
087import org.ametys.plugins.workspaces.tags.ProjectTagProviderExtensionPoint;
088import org.ametys.runtime.config.Config;
089import org.ametys.runtime.i18n.I18nizableText;
090import org.ametys.runtime.plugin.component.PluginAware;
091import org.ametys.web.repository.page.Page;
092import org.ametys.web.repository.page.PageQueryHelper;
093import org.ametys.web.repository.site.Site;
094import org.ametys.web.repository.site.SiteDAO;
095import org.ametys.web.repository.site.SiteManager;
096import org.ametys.web.repository.sitemap.Sitemap;
097import org.ametys.web.site.SiteConfigurationManager;
098import org.ametys.web.tags.TagExpression;
099
100import com.google.common.collect.ImmutableMap;
101import com.google.common.collect.Iterables;
102
103/**
104 * Helper component for managing project workspaces
105 */
106public class ProjectManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable, PluginAware, Initializable
107{
108    /** Avalon Role */
109    public static final String ROLE = ProjectManager.class.getName();
110    
111    /** Workspaces plugin node name */
112    private static final String __WORKSPACES_PLUGIN_NODE_NAME = "workspaces";
113    
114    /** Workspaces plugin node name */
115    private static final String __WORKSPACES_PLUGIN_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":unstructured";
116    
117    /** The name of the projects root node */
118    private static final String __PROJECTS_ROOT_NODE_NAME = "projects";
119    
120    /** The type of the projects root node */
121    private static final String __PROJECTS_ROOT_NODE_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":unstructured";
122    
123    /** Constants for tags metadata */
124    private static final String __PROJECTS_TAGS_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":tags";
125    
126    /** Constants for places metadata */
127    private static final String __PROJECTS_PLACES_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":places";
128
129    /** Ametys object resolver */
130    protected AmetysObjectResolver _resolver;
131    
132    /** The i18n utils. */
133    protected I18nUtils _i18nUtils;
134    
135    /** Site manager */
136    protected SiteManager _siteManager;
137    
138    /** Site DAO */
139    protected SiteDAO _siteDao;
140    
141    /** Site configuration manager */
142    protected SiteConfigurationManager _siteConfigurationManager;
143    
144    /** Module Managers EP */
145    protected WorkspaceModuleExtensionPoint _moduleManagerEP;
146    
147    /** Avalon context */
148    protected Context _context;
149
150    private ObservationManager _observationManager;
151    
152    private CurrentUserProvider _currentUserProvider;
153
154    private ProjectMemberManager _projectMembers;
155
156    private String _pluginName;
157
158    private ProjectRightHelper _projectRightHelper;
159
160    private ProjectTagProviderExtensionPoint _projectTagProviderEP;
161
162    private CategoryProviderExtensionPoint _categoryProviderEP;
163
164    private UserHelper _userHelper;
165
166    private AbstractCacheManager _cacheManager;
167
168    
169    @Override
170    public void contextualize(Context context) throws ContextException
171    {
172        _context = context;
173    }
174    
175    @Override
176    public void service(ServiceManager manager) throws ServiceException
177    {
178        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
179        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
180        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
181        _siteDao = (SiteDAO) manager.lookup(SiteDAO.ROLE);
182        _siteConfigurationManager = (SiteConfigurationManager) manager.lookup(SiteConfigurationManager.ROLE);
183        _projectMembers = (ProjectMemberManager) manager.lookup(ProjectMemberManager.ROLE);
184        _observationManager = (ObservationManager) manager.lookup(ObservationManager.ROLE);
185        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
186        _moduleManagerEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE);
187        _projectRightHelper = (ProjectRightHelper) manager.lookup(ProjectRightHelper.ROLE);
188        _projectTagProviderEP = (ProjectTagProviderExtensionPoint) manager.lookup(ProjectTagProviderExtensionPoint.ROLE);
189        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
190        _categoryProviderEP = (CategoryProviderExtensionPoint) manager.lookup(CategoryProviderExtensionPoint.ROLE);
191        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
192    }
193    
194    public void initialize() throws Exception
195    {
196        _cacheManager.createCache(ROLE, 
197                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CACHE_PROJECT_MANAGER_LABEL"),
198                new I18nizableText("plugin.workspaces", "PLUGINS_WORKSPACES_CACHE_PROJECT_MANAGER_DESCRIPTION"),
199                CacheType.MEMORY,
200                true);
201    }
202    
203    @Override
204    public void setPluginInfo(String pluginName, String featureName, String id)
205    {
206        _pluginName = pluginName;
207    }
208    
209    /**
210     * Retrieves all projects
211     * @return the projects
212     */
213    public AmetysObjectIterable<Project> getProjects()
214    {
215        String jcrQuery = "//element(*, ametys:project)";
216        return _resolver.query(jcrQuery);
217    }
218    
219    /**
220     * Retrieves all projects for client side
221     * @return the projects
222     */
223    @Callable
224    public List<Map<String, Object>> getProjectsForClientSide()
225    {
226        return getProjects()
227                .stream()
228                .map(p -> getProjectProperties(p))
229                .collect(Collectors.toList());
230    }
231    
232    
233    /**
234     * Retrieves a project by its name
235     * @param name The project name
236     * @return the project or <code>null</code> if not found
237     */
238    public Project getProject(String name)
239    {
240        String jcrQuery = "//element(" + name + ", ametys:project)";
241        AmetysObjectIterable<Project> sites = _resolver.query(jcrQuery);
242        Iterator<Project> it = sites.iterator();
243        
244        if (it.hasNext())
245        {
246            return it.next();
247        }
248        
249        return null; 
250    }
251    
252    /**
253     * Get the user's projects
254     * @param user the user
255     * @return the user's projects
256     */
257    public List<Project> getUserProjects(UserIdentity user)
258    {
259        List<Project> userProjects = new ArrayList<>();
260        
261        AmetysObjectIterable<Project> projects = getProjects();
262        
263        for (Project project : projects)
264        {
265            if (_projectMembers.isProjectMember(project, user))
266            {
267                userProjects.add(project);
268            }
269        }
270        
271        return userProjects;
272    }
273    
274    
275    /**
276     * Returns true if the given project exists.
277     * @param projectName the project name.
278     * @return true if the given project exists.
279     */
280    public boolean hasProject(String projectName)
281    {
282        return getProject(projectName) != null;
283    }
284    
285    /**
286     * Retrieves the mapping of all the projects name with their title on which the current user has access
287     * @return the map (projectName, projectTitle) for all projects
288     */
289    @Callable
290    public List<Map<String, String>> getUserProjectsData()
291    {
292        return getUserProjects(_currentUserProvider.getUser())
293                .stream()
294                .map(p -> ImmutableMap.of("title", p.getTitle(), "name", p.getName()))
295                .collect(Collectors.toList());
296    }
297    
298    /**
299     * Retrieves the mapping of all the projects name with their title (regarless user rights)
300     * @return the map (projectName, projectTitle) for all projects
301     */
302    @Callable
303    public List<Map<String, String>> getProjectsData()
304    {
305        return getProjects()
306                .stream()
307                .map(p -> ImmutableMap.of("title", p.getTitle(), "name", p.getName()))
308                .collect(Collectors.toList());
309    }
310    
311    /**
312     * Retrieves the project names
313     * @return the project names
314     */
315    @Callable
316    public Collection<String> getProjectNames()
317    {
318        return getProjects()
319                .stream()
320                .map(Project::getName)
321                .collect(Collectors.toList());
322    }
323    
324    /**
325     * Return the root for projects
326     * The root node will be created if necessary
327     * @return The root for projects
328     */
329    public ModifiableTraversableAmetysObject getProjectsRoot()
330    {
331        try
332        {
333            ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins");
334            ModifiableTraversableAmetysObject workspacesPluginNode = _getOrCreateObject(pluginsNode, __WORKSPACES_PLUGIN_NODE_NAME, __WORKSPACES_PLUGIN_NODE_TYPE);
335            return _getOrCreateObject(workspacesPluginNode, __PROJECTS_ROOT_NODE_NAME, __PROJECTS_ROOT_NODE_TYPE);
336        }
337        catch (AmetysRepositoryException e)
338        {
339            throw new AmetysRepositoryException("Error getting the projects root node.", e);
340        }
341    }
342    
343    /**
344     * Retrieves the standard information of a project
345     * @param projectId Identifier of the project
346     * @return The map of information
347     */
348    @Callable
349    public Map<String, Object> getProjectProperties(String projectId)
350    {
351        return getProjectProperties((Project) _resolver.resolveById(projectId));
352    }
353    
354    /**
355     * Retrieves the standard information of a project
356     * @param project The project
357     * @return The map of information
358     */
359    public Map<String, Object> getProjectProperties(Project project)
360    {
361        Map<String, Object> info = new HashMap<>();
362
363        info.put("id", project.getId());
364        info.put("name", project.getName());
365        info.put("type", "project");
366
367        info.put("title", project.getTitle());
368        info.put("description", project.getDescription());
369        info.put("mailingList", project.getMailingList());
370        info.put("inscriptionStatus", project.getInscriptionStatus().toString());
371        info.put("defaultProfile", project.getDefaultProfile());
372
373        info.put("creationDate", project.getCreationDate());
374
375        // check if the project workspace configuration is valid
376        Collection<Site> sites = project.getSites();
377        boolean valid = sites.size() > 0;
378        if (valid)
379        {
380            Iterator<Site> siteIterator = sites.iterator();
381            while (valid && siteIterator.hasNext())
382            {
383                Site site = siteIterator.next();
384                valid = _siteConfigurationManager.isSiteConfigurationValid(site);
385            }
386        }
387        
388        Set<String> categories = project.getCategories();
389        info.put("categories", categories.stream()
390                .map(c -> _tag2json(_categoryProviderEP.getTag(c, new HashMap<>())))
391                .collect(Collectors.toList()));
392        
393        Set<String> tags = project.getTags();
394        info.put("tags", tags.stream()
395                .map(c -> _tag2json(_projectTagProviderEP.getTag(c, new HashMap<>())))
396                .collect(Collectors.toList()));
397        
398        info.put("valid", valid);
399        
400        UserIdentity[] managers = project.getManagers();
401        info.put("managers", Arrays.stream(managers)
402                .map(u -> _userHelper.user2json(u))
403                .collect(Collectors.toList()));
404        
405        // sites is a list of map entry with id ,name, title and url property
406        // { id: site id, name: site name, title: site title, url: site url }
407        info.put("sites", sites.stream().map(site ->
408        {
409            Map<String, String> siteProps = new HashMap<>();
410            siteProps.put("id", site.getId());
411            siteProps.put("name", site.getName());
412            siteProps.put("title", site.getTitle());
413            siteProps.put("url", site.getUrl());
414            return siteProps;
415        }).collect(Collectors.toList()));
416
417        return info;
418    }
419    
420    private Map<String, Object> _tag2json(Tag tag)
421    {
422        Map<String, Object> json = new HashMap<>();
423        json.put("id", tag.getId());
424        json.put("name", tag.getName());
425        json.put("title", tag.getTitle());
426        return json;
427    }
428    
429    /**
430     * Get the availables project URLs.
431     * @param project The project
432     * @return The availables project URLs, can be empty.
433     */
434    public Set<String> getProjectUrls(Project project)
435    {
436        return _getProjectNonEmptyElements(project, Site::getUrl);
437    }
438    
439    /**
440     * Get the availables project names.
441     * @param project The project
442     * @return The availables project names, can be empty.
443     */
444    public Set<String> getProjectNames(Project project)
445    {
446        return _getProjectNonEmptyElements(project, Site::getName);
447    }
448    
449    private Set<String> _getProjectNonEmptyElements(Project project, Function<? super Site, ? extends String> function)
450    {
451        return project.getSites()                       // Get the sites of the project
452                      .stream()                         // Build it as a stream
453                      .map(function)                    // Get the element of each site
454                      .filter(StringUtils::isNotEmpty)  // Filter empty strings
455                      .collect(Collectors.toSet());     // Get only the first value
456    }
457    
458    /**
459     * Create a project
460     * @param name The project name
461     * @param title The project title
462     * @param description The project description
463     * @param emailList Project mailing list
464     * @param inscriptionStatus The inscription status of the project
465     * @param defaultProfile The default profile for new members
466     * @return A map containing the id of the new project or an error key.
467     */
468    @Callable
469    public Map<String, Object> createProject(String name, String title, String description, String emailList, String inscriptionStatus, String defaultProfile)
470    {
471        Map<String, Object> result = new HashMap<>();
472        List<String> errors = new ArrayList<>();
473        
474        Map<String, Object> additionalValues = new HashMap<>();
475        additionalValues.put("description", description);
476        additionalValues.put("emailList", emailList);
477        additionalValues.put("inscriptionStatus", inscriptionStatus);
478        additionalValues.put("defaultProfile", defaultProfile);
479        
480        Project project = createProject(name, title, additionalValues, null, errors);
481        
482        if (CollectionUtils.isEmpty(errors))
483        {
484            result.put("id", project.getId());
485        }
486        else
487        {
488            result.put("error", errors.get(0));
489        }
490        
491        return result;
492    }
493    
494    /**
495     * Create a project
496     * @param name The project name
497     * @param title The project title
498     * @param additionalValues A list of optional additional values. Accepted values are : description, mailingList, inscriptionStatus, defaultProfile, tags, categoryTags and keywords
499     * @param modulesIds The list of modules to activate. Can be null to activate all modules
500     * @param errors A list that will be populated with the encountered errors. If null, errors will not be tracked.
501     * @return The id of the new project
502     */
503    public Project createProject(String name, String title, Map<String, Object> additionalValues, Set<String> modulesIds, List<String> errors)
504    {
505        if (StringUtils.isEmpty(title))
506        {
507            throw new IllegalArgumentException(String.format("Cannot create project. Title is mandatory"));
508        }
509        
510        ModifiableTraversableAmetysObject projectsRoot = getProjectsRoot();
511        
512        // Project name should be unique
513        if (hasProject(name))
514        {
515            if (getLogger().isWarnEnabled())
516            {
517                getLogger().warn(String.format("A project with the name '%s' already exists", name));
518            }
519            
520            if (errors != null)
521            {
522                errors.add("project-exists");
523            }
524            
525            return null;
526        }
527        
528        Project project = projectsRoot.createChild(name, Project.NODE_TYPE);
529        project.setTitle(title);
530        String description = (String) additionalValues.getOrDefault("description", null);
531        if (StringUtils.isNotEmpty(description))
532        {
533            project.setDescription(description);
534        }
535        String mailingList = (String) additionalValues.getOrDefault("emailList", null);
536        if (StringUtils.isNotEmpty(mailingList))
537        {
538            project.setMailingList(mailingList);
539        }
540        String inscriptionStatus = (String) additionalValues.getOrDefault("inscriptionStatus", null);
541        if (StringUtils.isNotEmpty(inscriptionStatus))
542        {
543            project.setInscriptionStatus(inscriptionStatus);
544        }
545        String defaultProfile = (String) additionalValues.getOrDefault("defaultProfile", null);
546        if (StringUtils.isNotEmpty(defaultProfile))
547        {
548            project.setDefaultProfile(defaultProfile);
549        }
550        
551        @SuppressWarnings("unchecked")
552        List<String> tags = (List<String>) additionalValues.getOrDefault("tags", null);
553        if (tags != null)
554        {
555            project.setTags(tags);
556        }
557        @SuppressWarnings("unchecked")
558        List<String> categoryTags = (List<String>) additionalValues.getOrDefault("categoryTags", null);
559        if (categoryTags != null)
560        {
561            project.setCategoryTags(categoryTags);
562        }
563        
564        @SuppressWarnings("unchecked")
565        List<String> keywords = (List<String>) additionalValues.getOrDefault("keywords", null);
566        if (keywords != null)
567        {
568            project.setKeywords(keywords.toArray(new String[keywords.size()]));
569        }
570
571        project.setCreationDate(ZonedDateTime.now());
572        
573        // Create the project workspace = a site + a set of pages
574        _createProjectWorkspace(project, errors);
575        
576        activateModules(project, modulesIds);
577        
578        if (CollectionUtils.isEmpty(errors))
579        {
580            project.saveChanges();
581         
582            // Notify observers
583            Map<String, Object> eventParams = new HashMap<>();
584            eventParams.put(ObservationConstants.ARGS_PROJECT, project);
585            _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_ADDED, _currentUserProvider.getUser(), eventParams));
586            
587        }
588        else
589        {
590            deleteProject(project);
591        }
592        
593        return project;
594    }
595    
596    /**
597     * Edit a project
598     * @param id The project identifier
599     * @param title The title to set
600     * @param description The description to set
601     * @param mailingList Project mailing list
602     * @param inscriptionStatus The inscription status of the project
603     * @param defaultProfile The default profile for new members
604     */
605    @Callable
606    public void editProject(String id, String title, String description, String mailingList, String inscriptionStatus, String defaultProfile)
607    {
608        Project project = _resolver.resolveById(id);
609        editProject(project, title, description, mailingList, inscriptionStatus, defaultProfile);
610    }
611    
612    /**
613     * Edit a project
614     * @param project The project
615     * @param title The title to set
616     * @param description The description to set
617     * @param mailingList Project mailing list
618     * @param inscriptionStatus The inscription status of the project
619     * @param defaultProfile The default profile for new members
620     */
621    public void editProject(Project project, String title, String description, String mailingList, String inscriptionStatus, String defaultProfile)
622    {
623        project.setTitle(title);
624        
625        if (StringUtils.isNotEmpty(description))
626        {
627            project.setDescription(description);
628        }
629        else
630        {
631            project.removeDescription();
632        }
633        
634        if (StringUtils.isNotEmpty(mailingList))
635        {
636            project.setMailingList(mailingList);
637        }
638        else
639        {
640            project.removeMailingList();
641        }
642
643        project.setInscriptionStatus(inscriptionStatus);
644        project.setDefaultProfile(defaultProfile);
645        
646        project.saveChanges();
647        
648        // Notify observers
649        Map<String, Object> eventParams = new HashMap<>();
650        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
651        _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_UPDATED, _currentUserProvider.getUser(), eventParams));
652    }
653    
654    /**
655     * Delete a list of project.
656     * @param ids The ids of projects to delete
657     * @return The ids of the deleted projects, unknowns projects and the deleted sites
658     */
659    @Callable
660    public Map<String, Object> deleteProjectsByIds(List<String> ids)
661    {
662        Map<String, Object> result = new HashMap<>();
663        List<Map<String, Object>> deleted = new ArrayList<>();
664        List<String> unknowns = new ArrayList<>();
665        
666        for (String id : ids)
667        {
668            try
669            {
670                Project project = _resolver.resolveById(id);
671                
672                Map<String, Object> projectInfo = new HashMap<>();
673                projectInfo.put("id", id);
674                projectInfo.put("title", project.getTitle());
675                projectInfo.put("sites", deleteProject(project));
676                
677                deleted.add(projectInfo);
678            }
679            catch (UnknownAmetysObjectException e)
680            {
681                getLogger().warn(String.format("Unable to delete the definition of id '%s', because it does not exist.", id), e);
682                unknowns.add(id);
683            }
684        }
685        
686        result.put("deleted", deleted);
687        result.put("unknowns", unknowns);
688        
689        return result;
690    }
691    
692    /**
693     * Delete a project.
694     * @param projects The list of projects to delete
695     * @return list of deleted sites (each list entry contains a data map with
696     *         the id and the name of the delete site).
697     */
698    public List<Map<String, String>> deleteProject(List<Project> projects)
699    {
700        List<Map<String, String>> deletedSitesInfo = new ArrayList<>();
701        
702        for (Project project : projects)
703        {
704            deletedSitesInfo.addAll(deleteProject(project));
705        }
706        
707        return deletedSitesInfo;
708    }
709    
710    /**
711     * Delete a project and its sites
712     * @param project The project to delete
713     * @return list of deleted sites (each list entry contains a data map with
714     *         the id and the name of the delete site).
715     */
716    public List<Map<String, String>> deleteProject(Project project)
717    {
718        ModifiableAmetysObject parent = project.getParent();
719        
720        Collection<Site> sites = project.getSites();
721        
722        // list of map entry with id, name and title property
723        // { id: site id, name: site name }
724        List<Map<String, String>> deletedSitesInfo = new ArrayList<>();
725        
726        sites.forEach(site -> 
727        {
728            try
729            {
730                Map<String, String> siteProps = new HashMap<>();
731                siteProps.put("id", site.getId());
732                siteProps.put("name", site.getName());
733
734                _siteDao.deleteSite(site.getId());
735                deletedSitesInfo.add(siteProps);
736            }
737            catch (RepositoryException e)
738            {
739                String errorMsg = String.format("Error while trying to delete the site '%s' for the project '%s'.", site.getName(), project.getName());
740                getLogger().error(errorMsg, e);
741            }
742        });
743        
744        String projectId = project.getId();
745        project.remove();
746        parent.saveChanges();
747        
748        // Notify observers
749        Map<String, Object> eventParams = new HashMap<>();
750        eventParams.put(ObservationConstants.ARGS_PROJECT, project);
751        eventParams.put(ObservationConstants.ARGS_PROJECT_ID, projectId);
752        _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_DELETED, _currentUserProvider.getUser(), eventParams));
753        
754        return deletedSitesInfo;
755    }
756    
757    /**
758     * Utility method to get or create an ametys object
759     * @param <A> A sub class of AmetysObject
760     * @param parent The parent object
761     * @param name The ametys object name
762     * @param type The ametys object type
763     * @return ametys object
764     * @throws AmetysRepositoryException if an repository error occurs
765     */
766    private <A extends AmetysObject> A _getOrCreateObject(ModifiableTraversableAmetysObject parent, String name, String type) throws AmetysRepositoryException
767    {
768        A object;
769        
770        if (parent.hasChild(name))
771        {
772            object = parent.getChild(name);
773        }
774        else
775        {
776            object = parent.createChild(name, type);
777            parent.saveChanges();
778        }
779        
780        return object;
781    }
782    
783    /**
784     * Get the project of an ametys object inside a project.
785     * It can be an explorer node, or any type of resource in a module.
786     * @param id The identifier of the ametys object
787     * @return the project or null if not found
788     */
789    public Project getParentProject(String id)
790    {
791        return getParentProject(_resolver.<AmetysObject>resolveById(id));
792    }
793    
794    /**
795     * Get the project of an ametys object inside a project.
796     * It can be an explorer node, or any type of resource in a module.
797     * @param object The ametys object
798     * @return the project or null if not found
799     */
800    public Project getParentProject(AmetysObject object)
801    {
802        AmetysObject ametysObject = object;
803        // Go back to the local explorer root.
804        do
805        {
806            ametysObject = ametysObject.getParent();
807        }
808        while (ametysObject instanceof ExplorerNode);
809        
810        if (!(ametysObject instanceof Project))
811        {
812            getLogger().warn(String.format("No project found for ametys object with id '%s'", ametysObject.getId()));
813            return null;
814        }
815        
816        return (Project) ametysObject; 
817    }
818    
819    /**
820     * Get the list of project names for a given site
821     * @param siteName The site name
822     * @return the list of project names
823     */
824    @Callable
825    public List<String> getProjectsForSite(String siteName)
826    {
827        Cache<String, List<Pair<String, String>>> cache = _getCache();
828        if (cache.hasKey(siteName))
829        {
830            return cache.get(siteName).stream()
831                    .map(p -> p.getRight())
832                    .collect(Collectors.toList());
833        }
834        else
835        {
836            List<String> projectNames = new ArrayList<>();
837            
838            if (_siteManager.hasSite(siteName))
839            {
840                Site site = _siteManager.getSite(siteName);
841                getProjectsForSite(site)
842                    .stream()
843                    .map(Project::getName)
844                    .forEach(projectNames::add);
845            }
846            
847            return projectNames;
848        }
849    }
850    
851    /**
852     * Get the list of project for a given site
853     * @param site The site
854     * @return the list of project
855     */
856    public List<Project> getProjectsForSite(Site site)
857    {
858        Cache<String, List<Pair<String, String>>> cache = _getCache();
859        if (cache.hasKey(site.getName()))
860        {
861            cache.get(site.getName()).stream()
862                 .map(p -> _resolver.resolveById(p.getLeft()))
863                 .collect(Collectors.toList());
864        }
865        
866        try
867        {
868            // Stream over the weak reference properties pointing to this
869            // node to find referencing projects 
870            Iterator<Property> propertyIterator = site.getNode().getWeakReferences();
871            Iterable<Property> propertyIterable = () -> propertyIterator;
872            
873            List<Project> projects = StreamSupport.stream(propertyIterable.spliterator(), false)
874                    .map(p -> 
875                    {
876                        try
877                        {
878                            // Parent should be a composite with name "ametys:sites"
879                            Node parent = p.getParent();
880                            if (NodeTypeHelper.isNodeType(parent, "ametys:compositeMetadata") && (RepositoryConstants.NAMESPACE_PREFIX + ":" + Project.DATA_SITES).equals(parent.getName()))
881                            {
882                                // Parent should be the project
883                                parent = parent.getParent();
884                                if (NodeTypeHelper.isNodeType(parent, "ametys:project"))
885                                {
886                                    Project project = _resolver.resolve(parent, false);
887                                    return project;
888                                }
889                            }
890                        }
891                        catch (Exception e)
892                        {
893                            if (getLogger().isWarnEnabled())
894                            {
895                                // this weak reference is not from a project
896                                String propertyPath = null;
897                                try
898                                {
899                                    propertyPath = p.getPath();
900                                }
901                                catch (Exception e2)
902                                {
903                                    // ignore
904                                }
905                                
906                                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);
907                                getLogger().warn(warnMsg);
908                            }
909                        }
910                        
911                        return null;
912                    })
913                    .filter(Objects::nonNull)
914                    .collect(Collectors.toList());
915            
916            List<Pair<String, String>> projectsPairs = projects.stream().map(p -> Pair.of(p.getId(), p.getName())).collect(Collectors.toList());
917            cache.put(site.getName(), projectsPairs);
918            return projects;
919        }
920        catch (RepositoryException e)
921        {
922            getLogger().error(String.format("Unable to find projects for site '%s'", site.getName()), e);
923        }
924        
925        return new ArrayList<>();
926    }
927    
928    /**
929     * Create the project workspace for a given project.
930     * @param project The project for which the workspace must be created
931     * @param errors A list of possible errors to populate. Can be null if the caller is not interested in error tracking.
932     * @return The site created for this workspace
933     */
934    protected Site _createProjectWorkspace(Project project, List<String> errors)
935    {
936        String initialSiteName = project.getName();
937        Site site = null;
938        
939        Map<String, Object> result = _siteDao.createSite(null, initialSiteName, ProjectWorkspaceSiteType.TYPE_ID, true);
940        
941        String siteId = (String) result.get("id");
942        String siteName = (String) result.get("name");
943        if (StringUtils.isNotEmpty(siteId)) 
944        {
945            // Creation success
946            site = _siteManager.getSite(siteName);
947            
948            I18nizableText i18nSiteTitle = new I18nizableText("plugin." + _pluginName, "PLUGINS_WORKSPACES_PROJECT_DEFAULT_PROJECT_WORKSPACE_TITLE", Arrays.asList(project.getTitle()));
949            site.setTitle(_i18nUtils.translate(i18nSiteTitle));
950            
951            // Add site to project
952            project.setSites(Arrays.asList(site.getName()));
953            
954            site.saveChanges();
955        }
956        
957        return site;
958    }
959    
960    /**
961     * Get the project's tags
962     * @return The project's tags 
963     */
964    @Callable
965    public List<String> getTags()
966    {
967        AmetysObject projectsRootNode = getProjectsRoot();
968        if (projectsRootNode instanceof JCRAmetysObject)
969        {
970            Node node = ((JCRAmetysObject) projectsRootNode).getNode();
971            
972            try
973            {
974                return Arrays.stream(node.getProperty(__PROJECTS_TAGS_PROPERTY).getValues())
975                    .map(LambdaUtils.wrap(Value::getString))
976                    .collect(Collectors.toList());
977            }
978            catch (PathNotFoundException e)
979            {
980                // property is not set, empty list will be returned.
981            }
982            catch (RepositoryException e)
983            {
984                throw new AmetysRepositoryException(e);
985            }
986        }
987        
988        return new ArrayList<>();
989    }
990    
991    /**
992     * Set the tags
993     * @param tags The tags to set
994     */
995    @Callable
996    public synchronized void setTags(List<String> tags)
997    {
998        AmetysObject projectsRootNode = getProjectsRoot();
999        if (projectsRootNode instanceof JCRAmetysObject)
1000        {
1001            JCRAmetysObject jcrProjectsRootNode = (JCRAmetysObject) projectsRootNode;
1002            
1003            if (CollectionUtils.isNotEmpty(tags))
1004            {
1005                String[] tagsArray = tags.stream()
1006                        .map(String::trim)
1007                        .map(String::toLowerCase)
1008                        .filter(StringUtils::isNotEmpty)
1009                        .distinct()
1010                        .toArray(String[]::new);
1011                
1012                try
1013                {
1014                    jcrProjectsRootNode.getNode().setProperty(__PROJECTS_TAGS_PROPERTY, tagsArray);
1015                    jcrProjectsRootNode.saveChanges();
1016                }
1017                catch (RepositoryException e)
1018                {
1019                    throw new AmetysRepositoryException(e);
1020                }
1021            }
1022            else
1023            {
1024                Node node = jcrProjectsRootNode.getNode();
1025                try
1026                {
1027                    if (node.hasProperty(__PROJECTS_TAGS_PROPERTY))
1028                    {
1029                        node.getProperty(__PROJECTS_TAGS_PROPERTY).remove();
1030                        jcrProjectsRootNode.saveChanges();
1031                    }
1032                }
1033                catch (RepositoryException e)
1034                {
1035                    throw new AmetysRepositoryException(e);
1036                }
1037            }
1038        }
1039    }
1040    
1041    /**
1042     * Add project's tags
1043     * @param newTags The new tags to add
1044     */
1045    public synchronized void addTags(Collection<String> newTags)
1046    {
1047        if (CollectionUtils.isNotEmpty(newTags))
1048        {
1049            AmetysObject projectsRootNode = getProjectsRoot();
1050            if (projectsRootNode instanceof JCRAmetysObject)
1051            {
1052                // Concat existing tags with new lowercased tags
1053                String[] tags = Stream.concat(getTags().stream(), newTags.stream().map(String::trim).map(String::toLowerCase).filter(StringUtils::isNotEmpty))
1054                        .distinct()
1055                        .toArray(String[]::new);
1056                
1057                try
1058                {
1059                    ((JCRAmetysObject) projectsRootNode).getNode().setProperty(__PROJECTS_TAGS_PROPERTY, tags);
1060                }
1061                catch (RepositoryException e)
1062                {
1063                    throw new AmetysRepositoryException(e);
1064                }
1065            }
1066        }
1067    }
1068    
1069    /**
1070     * Get the project's places
1071     * @return The project's places 
1072     */
1073    @Callable
1074    public List<String> getPlaces()
1075    {
1076        AmetysObject projectsRootNode = getProjectsRoot();
1077        if (projectsRootNode instanceof JCRAmetysObject)
1078        {
1079            Node node = ((JCRAmetysObject) projectsRootNode).getNode();
1080            
1081            try
1082            {
1083                return Arrays.stream(node.getProperty(__PROJECTS_PLACES_PROPERTY).getValues())
1084                    .map(LambdaUtils.wrap(Value::getString))
1085                    .collect(Collectors.toList());
1086            }
1087            catch (PathNotFoundException e)
1088            {
1089                // property is not set, empty list will be returned.
1090            }
1091            catch (RepositoryException e)
1092            {
1093                throw new AmetysRepositoryException(e);
1094            }
1095        }
1096        
1097        return new ArrayList<>();
1098    }
1099    
1100    /**
1101     * Add project's places
1102     * @param newPlaces The new places to add
1103     */
1104    public synchronized void addPlaces(Collection<String> newPlaces)
1105    {
1106        if (CollectionUtils.isNotEmpty(newPlaces))
1107        {
1108            AmetysObject projectsRootNode = getProjectsRoot();
1109            if (projectsRootNode instanceof JCRAmetysObject)
1110            {
1111                Set<String> lowercasedPlaces = new HashSet<>();
1112                
1113                // Concat existing places with new places
1114                String[] places = Stream.concat(getPlaces().stream(), newPlaces.stream().map(String::trim).filter(StringUtils::isNotEmpty))
1115                        // duplicates are filtered out
1116                        .filter(p -> lowercasedPlaces.add(p.toLowerCase()))
1117                        .toArray(String[]::new);
1118                
1119                try
1120                {
1121                    ((JCRAmetysObject) projectsRootNode).getNode().setProperty(__PROJECTS_PLACES_PROPERTY, places);
1122                }
1123                catch (RepositoryException e)
1124                {
1125                    throw new AmetysRepositoryException(e);
1126                }
1127            }
1128        }
1129    }
1130    
1131    /**
1132     * Set the places
1133     * @param places The places to set
1134     */
1135    @Callable
1136    public synchronized void setPlaces(List<String> places)
1137    {
1138        AmetysObject projectsRootNode = getProjectsRoot();
1139        if (projectsRootNode instanceof JCRAmetysObject)
1140        {
1141            JCRAmetysObject jcrProjectsRootNode = (JCRAmetysObject) projectsRootNode;
1142            
1143            if (CollectionUtils.isNotEmpty(places))
1144            {
1145                Set<String> lowercasedPlaces = new HashSet<>();
1146                
1147                String[] placesArray = places.stream()
1148                        .map(String::trim)
1149                        .filter(StringUtils::isNotEmpty)
1150                        // duplicates are filtered out
1151                        .filter(p -> lowercasedPlaces.add(p.toLowerCase()))
1152                        .toArray(String[]::new);
1153                
1154                try
1155                {
1156                    jcrProjectsRootNode.getNode().setProperty(__PROJECTS_PLACES_PROPERTY, placesArray);
1157                    jcrProjectsRootNode.saveChanges();
1158                }
1159                catch (RepositoryException e)
1160                {
1161                    throw new AmetysRepositoryException(e);
1162                }
1163            }
1164            else
1165            {
1166                Node node = jcrProjectsRootNode.getNode();
1167                try
1168                {
1169                    if (node.hasProperty(__PROJECTS_PLACES_PROPERTY))
1170                    {
1171                        node.getProperty(__PROJECTS_PLACES_PROPERTY).remove();
1172                        jcrProjectsRootNode.saveChanges();
1173                    }
1174                }
1175                catch (RepositoryException e)
1176                {
1177                    throw new AmetysRepositoryException(e);
1178                }
1179            }
1180        }
1181    }
1182    
1183    /**
1184     * Get the list of activated modules for a project
1185     * @param project The project
1186     * @return The list of activated modules
1187     */
1188    public List<WorkspaceModule> getModules(Project project)
1189    {
1190        return _moduleManagerEP.getModules().stream()
1191                .filter(module -> isModuleActivated(project, module.getId()))
1192                .collect(Collectors.toList());
1193    }
1194    
1195    /**
1196     * Retrieves the page of the module for all available languages
1197     * @param project The project
1198     * @param moduleId The project module id
1199     * @param language the sitemap language or <code>null</code> for all sitemap languages.
1200     * @return the page or null if not found
1201     */
1202    public AmetysObjectIterable<Page> getModulePages(Project project, String moduleId, String language)
1203    {
1204        if (_moduleManagerEP.hasExtension(moduleId))
1205        {
1206            WorkspaceModule moduleManager = _moduleManagerEP.getExtension(moduleId);
1207            return moduleManager.getModulePages(project, language);
1208        }
1209        return null;
1210    }
1211    
1212    /**
1213     * Get a page in the site of a given project with a specific tag
1214     * @param project The project
1215     * @param tagName The name of the tag
1216     * @param language the sitemap language or <code>null</code> for all sitemap languages.
1217     * @return The module's pages
1218     */
1219    public AmetysObjectIterable<Page> getProjectPages(Project project, String tagName, String language)
1220    {
1221        String siteName = Iterables.getFirst(getProjectNames(project), null);
1222        if (StringUtils.isEmpty(siteName))
1223        {
1224            return null;
1225        }
1226        
1227        Expression expression = new TagExpression(Operator.EQ, tagName);
1228        String query = PageQueryHelper.getPageXPathQuery(siteName, language, null, expression, null);
1229
1230        return _resolver.query(query);
1231    }
1232    
1233    
1234    /**
1235     * Get the dashboard page in the site of a given project
1236     * @param project The project
1237     * @param language the sitemap language or <code>null</code> for all sitemap languages.
1238     * @return The module's dashboard pages
1239     */
1240    public AmetysObjectIterable<Page> getProjectDashboardPage(Project project, String language)
1241    {
1242        String siteName = Iterables.getFirst(getProjectNames(project), null);
1243        if (StringUtils.isEmpty(siteName))
1244        {
1245            return null;
1246        }
1247        
1248        String query = "//element(" + siteName + ", ametys:site)/ametys-internal:sitemaps/" 
1249                + (language == null ? "*" : language)
1250                + "//element(index, ametys:page)";
1251                
1252        return _resolver.query(query);
1253    }
1254    
1255    /**
1256     * Activate the list of module of the project
1257     * @param project The project
1258     * @param moduleIds The list of modules. Can be null to activate all modules 
1259     */
1260    public void activateModules(Project project, Set<String> moduleIds)
1261    {
1262        Set<String> modules = moduleIds == null ? _moduleManagerEP.getExtensionsIds() : moduleIds;
1263        
1264        for (String moduleId : modules)
1265        {
1266            WorkspaceModule module = _moduleManagerEP.getModule(moduleId);
1267            if (module != null && !isModuleActivated(project, moduleId))
1268            {
1269                module.activateModule(project);
1270                project.addModule(moduleId);
1271            }
1272        }
1273        
1274        project.saveChanges();
1275    }    
1276    
1277    /**
1278     * Initialize the sitemap with the active module of the project
1279     * @param project The project
1280     * @param sitemap The sitemap
1281     */
1282    public void initializeModulesSitemap(Project project, Sitemap sitemap)
1283    {
1284        Set<String> modules = _moduleManagerEP.getExtensionsIds();
1285        
1286        for (String moduleId : modules)
1287        {
1288            if (_moduleManagerEP.hasExtension(moduleId))
1289            {
1290                WorkspaceModule module = _moduleManagerEP.getExtension(moduleId);
1291                
1292                if (isModuleActivated(project, moduleId))
1293                {
1294                    module.initializeSitemap(sitemap);
1295                }
1296            }
1297        }
1298    }
1299    
1300    /**
1301     * Determines if a module is activated
1302     * @param project The project
1303     * @param moduleId The id of module
1304     * @return true if the module the currently activated
1305     */
1306    public boolean isModuleActivated(Project project, String moduleId)
1307    {
1308        return ArrayUtils.contains(project.getModules(), moduleId);
1309    }
1310    
1311    /**
1312     * Remove the explorer root node of the project module, remove all events 
1313     * related to that module and set it to deactivated
1314     * @param project The project
1315     * @param moduleIds The id of module to activate
1316     */
1317    public void deactivateModules(Project project, Set<String> moduleIds)
1318    {
1319        for (String moduleId : moduleIds)
1320        {
1321            WorkspaceModule module = _moduleManagerEP.getModule(moduleId);
1322            if (module != null && isModuleActivated(project, moduleId))
1323            {
1324                module.deactivateModule(project);
1325                project.removeModule(moduleId);
1326            }
1327        }
1328        
1329        project.saveChanges();
1330    }
1331    
1332    
1333    /**
1334     * Get the list of profiles configured for the workspaces' projects
1335     * @return The list of profiles as JSON
1336     */
1337    @Callable
1338    public Map<String, Object> getProjectProfiles()
1339    {
1340        Map<String, Object> result = new HashMap<>();
1341        List<Map<String, Object>> profiles = _projectRightHelper.getProfileList().stream().map(p -> p.toJSON()).collect(Collectors.toList());
1342        result.put("profiles", profiles);
1343        return result;
1344    }
1345    
1346    /**
1347     * Get the tags from the projects
1348     * @param projectIds The ids of the projects
1349     * @return the tags of the projects
1350     */
1351    @Callable
1352    public Set<String> getTags(List<String> projectIds)
1353    {
1354        Set<String> tags = new HashSet<>();
1355        
1356        for (String projectId : projectIds)
1357        {
1358            Project project = _resolver.resolveById(projectId);
1359            tags.addAll(project.getTags());
1360        }
1361        
1362        return tags;
1363    }
1364    
1365    /**
1366     * Tag the projects
1367     * @param projectIds the project ids
1368     * @param tagNames the tag names
1369     * @param contextualParameters the contextuals parameters
1370     * @return results
1371     */
1372    @Callable
1373    public Map<String, Object> tag(List<String> projectIds, List<String> tagNames, Map<String, Object> contextualParameters)
1374    {
1375        return tag(projectIds, tagNames, TagMode.REPLACE.toString(), contextualParameters);
1376    }
1377    
1378    /**
1379     * Tag the projects
1380     * @param projectIds the project ids
1381     * @param tagNames the tag names
1382     * @param mode the mode The mode for updating tags: 'REPLACE' to replace tags, 'INSERT' to add tags or 'REMOVE' to remove tags.
1383     * @param contextualParameters the contextual parameters
1384     * @return results
1385     */
1386    @Callable
1387    public Map<String, Object> tag(List<String> projectIds, List<String> tagNames, String mode, Map<String, Object> contextualParameters)
1388    {
1389        Map<String, Object> result = new HashMap<>();
1390        
1391        result.put("invalid-tags", new ArrayList<String>());
1392        result.put("allright-projects", new ArrayList<Map<String, Object>>());
1393        
1394        for (String projectId : projectIds)
1395        {
1396            Project project = _resolver.resolveById(projectId);
1397            
1398            Map<String, Object> project2json = new HashMap<>();
1399            project2json.put("id", project.getId());
1400            project2json.put("title", project.getTitle());
1401            
1402            TagMode tagMode = TagMode.valueOf(mode);
1403            
1404            Set<String> oldTags = project.getTags();
1405            if (TagMode.REPLACE.equals(tagMode))
1406            {
1407                // First delete old tags
1408                for (String tagName : oldTags)
1409                {
1410                    project.untag(tagName);
1411                }
1412            }
1413            
1414            // Then set new tags
1415            for (String tagName : tagNames)
1416            {
1417                if (isTagValid(tagName))
1418                {
1419                    if (TagMode.REMOVE.equals(tagMode))
1420                    {
1421                        project.untag(tagName);
1422                    }
1423                    else if (TagMode.REPLACE.equals(tagMode) || !oldTags.contains(tagName))
1424                    {
1425                        project.tag(tagName);
1426                    }
1427                }
1428                else
1429                {
1430                    @SuppressWarnings("unchecked")
1431                    List<String> invalidTags = (List<String>) result.get("invalid-tags");
1432                    invalidTags.add(tagName);
1433                }
1434            }
1435            
1436            project.saveChanges();
1437            
1438            project2json.put("tags", project.getTags());
1439            @SuppressWarnings("unchecked")
1440            List<Map<String, Object>> allRightProjects = (List<Map<String, Object>>) result.get("allright-projects");
1441            allRightProjects.add(project2json);
1442            
1443            if (!oldTags.equals(project.getTags()))
1444            {
1445                // Notify observers that the project has been tagged
1446                Map<String, Object> eventParams = new HashMap<>();
1447                eventParams.put(ObservationConstants.ARGS_PROJECT, project);
1448                _observationManager.notify(new Event(ObservationConstants.EVENT_PROJECT_UPDATED, _currentUserProvider.getUser(), eventParams));
1449            }
1450        }
1451        
1452        return result;
1453    }
1454    
1455    /**
1456     * Test if a tag is valid
1457     * @param tagName The tag name
1458     * @return True if the tag is valid
1459     */
1460    public boolean isTagValid (String tagName)
1461    {
1462        Map<String, Object> params = new HashMap<>();
1463        Tag tag = _projectTagProviderEP.getTag(tagName, params);
1464        
1465        return tag != null;
1466    }
1467    
1468    /**
1469     * Get the site name holding the catalog of projects
1470     * @return the catalog's site name
1471     */
1472    public String getCatalogSiteName()
1473    {
1474        return Config.getInstance().getValue("workspaces.catalog.site.name");
1475    }
1476    
1477    /**
1478     * Get the site name holding the users directory
1479     * @return the users directory's site name
1480     */
1481    public String getUsersDirectorySiteName()
1482    {
1483        return Config.getInstance().getValue("workspaces.member.userdirectory.site.name");
1484    }
1485    
1486    private Cache<String, List<Pair<String, String>>> _getCache() 
1487    {
1488        return this._cacheManager.get(ROLE);
1489    }    
1490}