001/*
002 *  Copyright 2010 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.web.repository.site;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.HashSet;
021import java.util.Map;
022import java.util.Set;
023import java.util.stream.Collectors;
024
025import javax.jcr.RepositoryException;
026import javax.jcr.Session;
027import javax.jcr.observation.Event;
028import javax.jcr.observation.ObservationManager;
029
030import org.apache.avalon.framework.activity.Initializable;
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.context.Context;
033import org.apache.avalon.framework.context.ContextException;
034import org.apache.avalon.framework.context.Contextualizable;
035import org.apache.avalon.framework.logger.AbstractLogEnabled;
036import org.apache.avalon.framework.service.ServiceException;
037import org.apache.avalon.framework.service.ServiceManager;
038import org.apache.avalon.framework.service.Serviceable;
039import org.apache.cocoon.components.ContextHelper;
040import org.apache.cocoon.environment.Request;
041import org.apache.commons.lang.StringUtils;
042
043import org.ametys.core.cache.AbstractCacheManager;
044import org.ametys.core.cache.AbstractCacheManager.CacheType;
045import org.ametys.core.cache.Cache;
046import org.ametys.core.cache.CacheException;
047import org.ametys.core.right.RightManager;
048import org.ametys.core.user.CurrentUserProvider;
049import org.ametys.core.user.UserIdentity;
050import org.ametys.core.user.population.PopulationContextHelper;
051import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
052import org.ametys.plugins.repository.AmetysObject;
053import org.ametys.plugins.repository.AmetysObjectIterable;
054import org.ametys.plugins.repository.AmetysObjectResolver;
055import org.ametys.plugins.repository.AmetysRepositoryException;
056import org.ametys.plugins.repository.CollectionIterable;
057import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
058import org.ametys.plugins.repository.RepositoryConstants;
059import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
060import org.ametys.plugins.repository.TraversableAmetysObject;
061import org.ametys.plugins.repository.UnknownAmetysObjectException;
062import org.ametys.plugins.repository.activities.ActivityHolder;
063import org.ametys.plugins.repository.provider.AbstractRepository;
064import org.ametys.plugins.repository.provider.JackrabbitRepository;
065import org.ametys.plugins.repository.provider.WorkspaceSelector;
066import org.ametys.runtime.i18n.I18nizableText;
067import org.ametys.web.live.LiveWorkspaceListener;
068import org.ametys.web.repository.page.CopySiteComponent;
069import org.ametys.web.synchronization.SynchronizeComponent;
070
071/**
072 * Helper component for managing sites.
073 */
074public class SiteManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable, Initializable
075{
076    /** Avalon Role */
077    public static final String ROLE = SiteManager.class.getName();
078
079    /** Constant for the {@link Cache} id (the {@link Cache} is in {@link CacheType#REQUEST REQUEST} attribute) for the {@link Site}s objects in cache by {@link RequestSiteCacheKey} (composition of site name and workspace name). */
080    public static final String REQUEST_CACHE = SiteManager.class.getName() + "$Request";
081    
082    /** Constant for the {@link Cache} id for the {@link Site} ids (as {@link String}s) in cache by site name (for whole application). */
083    public static final String MEMORY_CACHE = SiteManager.class.getName() + "$UUID";
084    
085    /** sites root's JCR node name */
086    public static final String ROOT_SITES = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":sites";
087    
088    /** sites root's JCR path */
089    public static final String ROOT_SITES_PATH = "/" + ROOT_SITES;
090    
091    private static final String __IS_CACHE_FILLED = "###iscachefilled###";
092        
093    private AmetysObjectResolver _resolver;
094    private JackrabbitRepository _repository;
095    private SynchronizeComponent _synchronizeComponent;
096    private CopySiteComponent _copySiteComponent;
097    private WorkspaceSelector _workspaceSelector;
098    private RightManager _rightManager;
099    private CurrentUserProvider _currentUserProvider;
100    private PopulationContextHelper _populationContextHelper;
101    private AbstractCacheManager _cacheManager;
102    
103    private Context _context;
104
105    @Override
106    public void service(ServiceManager manager) throws ServiceException
107    {
108        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
109        _repository = (JackrabbitRepository) manager.lookup(AbstractRepository.ROLE);
110        _synchronizeComponent = (SynchronizeComponent) manager.lookup(SynchronizeComponent.ROLE);
111        _copySiteComponent = (CopySiteComponent) manager.lookup(CopySiteComponent.ROLE);
112        _workspaceSelector = (WorkspaceSelector) manager.lookup(WorkspaceSelector.ROLE);
113        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
114        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
115        _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE);
116        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
117    }
118
119    @Override
120    public void initialize() throws Exception
121    {
122        _createCaches();
123    }
124    
125    /**
126     * Creates the caches
127     */
128    protected void _createCaches()
129    {
130        _cacheManager.createMemoryCache(MEMORY_CACHE, 
131                new I18nizableText("plugin.web", "PLUGINS_WEB_SITE_MANAGER_UUID_CACHE_LABEL"),
132                new I18nizableText("plugin.web", "PLUGINS_WEB_SITE_MANAGER_UUID_CACHE_DESCRIPTION"),
133                true,
134                null);
135        _cacheManager.createRequestCache(REQUEST_CACHE, 
136                new I18nizableText("plugin.web", "PLUGINS_WEB_SITE_MANAGER_REQUEST_CACHE_LABEL"),
137                new I18nizableText("plugin.web", "PLUGINS_WEB_SITE_MANAGER_REQUEST_CACHE_DESCRIPTION"),
138                false);
139    }
140    
141    public void contextualize(Context context) throws ContextException
142    {
143        _context = context;
144    }
145
146    private synchronized Map<String, String> _getUUIDCache()
147    {
148        if (!_getMemoryCache().hasKey(__IS_CACHE_FILLED))
149        {
150            Session defaultSession = null;
151            try
152            {
153                // Force default workspace to execute query
154                defaultSession = _repository.login(RepositoryConstants.DEFAULT_WORKSPACE);
155                
156                String jcrQuery = "//element(*, ametys:site)";
157                
158                AmetysObjectIterable<Site> sites = _resolver.query(jcrQuery, defaultSession);
159                
160                for (Site site : sites)
161                {
162                    _getMemoryCache().put(site.getName(), site.getId());
163                }
164                
165                _getMemoryCache().put(__IS_CACHE_FILLED, null);
166            }
167            catch (RepositoryException e)
168            {
169                throw new AmetysRepositoryException(e);
170            }
171            finally
172            {
173                if (defaultSession != null)
174                {
175                    defaultSession.logout();
176                }
177            }
178        }
179        
180        Map<String, String> cacheAsMap = _getMemoryCache().asMap();
181        cacheAsMap.remove(__IS_CACHE_FILLED);
182        return cacheAsMap;
183    }
184    
185    private Request _getRequest ()
186    {
187        try 
188        {
189            return (Request) _context.get(ContextHelper.CONTEXT_REQUEST_OBJECT);
190        } 
191        catch (ContextException ce)
192        {
193            getLogger().info("Unable to get the request", ce);
194            return null;
195        }
196    }
197    
198    /**
199     * Returns the sites names.
200     * @return the sites names.
201     */
202    public Collection<String> getRootSiteNames()
203    {
204        TraversableAmetysObject root = _resolver.resolveByPath(ROOT_SITES_PATH);
205        AmetysObjectIterable<AmetysObject> sites = root.getChildren();
206        
207        ArrayList<String> result = new ArrayList<>();
208        
209        for (AmetysObject object : sites)
210        {
211            result.add(object.getName());
212        }
213        
214        return result;
215    }
216    
217    /**
218     * Returns the sites names.
219     * @return the sites names.
220     */
221    public Collection<String> getSiteNames()
222    {
223        // As cache is computed from default JCR workspace, we need to filter on sites that exist into the current JCR workspace 
224        return _getUUIDCache().entrySet().stream()
225                .filter(e -> _resolver.hasAmetysObjectForId(e.getValue()))
226                .map(Map.Entry::getKey)
227                .collect(Collectors.toList());
228    }
229    
230    /**
231     * Get the granted site names for the current user
232     * @return The name of sites the current user can access
233     */
234    public Set<String> getGrantedSites()
235    {
236        return getGrantedSites(_currentUserProvider.getUser());
237    }
238    
239    /**
240     * Get the granted site names for user
241     * @param user The user
242     * @return The name of sites the user can access
243     */
244    public Set<String> getGrantedSites(UserIdentity user)
245    {
246        Set<String> grantedSiteNames = new HashSet<>();
247        
248        Request request = _getRequest();
249        
250        for (String siteName : getSiteNames())
251        {
252            if (_populationContextHelper.getUserPopulationsOnContext("/sites/" + siteName, false).contains(user.getPopulationId()))
253            {
254                try
255                {
256                    request.setAttribute("siteName", siteName); // Setting temporarily this attribute to check user rights on any object on this site
257                    if (!_rightManager.getUserRights(user, "/cms").isEmpty())
258                    {
259                        grantedSiteNames.add(siteName);
260                    }
261                }
262                finally
263                {
264                    request.setAttribute("siteName", null);
265                }
266            }
267        }
268        
269        return grantedSiteNames;
270    }
271    
272    /**
273     * Determines if the user has granted access to the site 
274     * @param user The user
275     * @param siteName The site name
276     * @return <code>true</code> if the user can access to the site
277     */
278    public boolean isGrantedSite (UserIdentity user, String siteName)
279    {
280        Object oldValue = null;
281        
282        Request request = _getRequest();
283        try
284        {
285            oldValue = request.getAttribute("siteName");
286            request.setAttribute("siteName", siteName); // Setting temporarily this attribute to check user rights on any object on this site
287            return !_rightManager.getUserRights(user, "/cms").isEmpty();
288        }
289        finally
290        {
291            request.setAttribute("siteName", oldValue);
292        }
293    }
294    
295    /**
296     * Returns the root for sites.
297     * @return the root for sites.
298     */
299    public ModifiableTraversableAmetysObject getRoot()
300    {
301        return _resolver.resolveByPath(ROOT_SITES_PATH);
302    }
303    
304    /**
305     * Returns the root sites.
306     * @return the root sites.
307     */
308    public AmetysObjectIterable<Site> getRootSites()
309    {
310        TraversableAmetysObject root = _resolver.resolveByPath(ROOT_SITES_PATH);
311        return root.getChildren();
312    }
313    
314    /**
315     * Returns the all sites.
316     * @return the all sites.
317     */
318    public AmetysObjectIterable<Site> getSites()
319    {
320        // As cache is computed from default JCR workspace, we need to filter on sites that exist into the current JCR workspace 
321        Set<Site> sites = _getUUIDCache().values().stream()
322                .filter(_resolver::hasAmetysObjectForId)
323                .map(_resolver::<Site>resolveById)
324                .collect(Collectors.toSet());
325        
326        return new CollectionIterable<>(sites);
327    }
328    
329    /**
330     * Creates a site with the given name.
331     * @param siteName the site name.
332     * @param parentId the id of the parent site. Can be null.
333     * @return the newly created {@link Site}.
334     * @throws RepositoryIntegrityViolationException if the named site already exists.
335     */
336    public Site createSite(String siteName, String parentId) throws RepositoryIntegrityViolationException
337    {
338        ModifiableTraversableAmetysObject root = null;
339        if (parentId != null)
340        {
341            Site site = _resolver.resolveById(parentId);
342            if (!site.hasChild(ROOT_SITES))
343            {
344                site.createChild(ROOT_SITES, "ametys:sites");
345            }
346            root = site.getChild(ROOT_SITES);
347        }
348        else
349        {
350            root = _resolver.resolveByPath(ROOT_SITES_PATH);
351        }
352        
353        Site site = (Site) root.createChild(siteName, "ametys:site");
354        String sitePath;
355        try
356        {
357            sitePath = site.getNode().getPath();
358            
359            // Create the resource explorer root.
360            site.getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources", "ametys:resources-collection");
361        }
362        catch (RepositoryException ex)
363        {
364            throw new AmetysRepositoryException(ex);
365        }
366        finally
367        {
368            _getMemoryCache().invalidateAll();
369        }
370
371        try
372        {
373            Session session = _repository.getAdminSession();
374            ObservationManager manager = session.getWorkspace().getObservationManager();
375            
376            manager.addEventListener(new LiveWorkspaceListener(_repository, _synchronizeComponent, getLogger()), 
377                                     Event.NODE_ADDED
378                                     | Event.NODE_REMOVED
379                                     | Event.NODE_MOVED
380                                     | Event.PROPERTY_ADDED
381                                     | Event.PROPERTY_CHANGED
382                                     | Event.PROPERTY_REMOVED, 
383                                     sitePath + "/ametys-internal:plugins", true, null, null, false);
384            
385            manager.addEventListener(new LiveWorkspaceListener(_repository, _synchronizeComponent, getLogger()), 
386                                     Event.NODE_ADDED
387                                     | Event.NODE_REMOVED
388                                     | Event.NODE_MOVED
389                                     | Event.PROPERTY_ADDED
390                                     | Event.PROPERTY_CHANGED
391                                     | Event.PROPERTY_REMOVED, 
392                                     sitePath + "/ametys-internal:resources", true, null, null, false);
393            
394            manager.addEventListener(new LiveWorkspaceListener(_repository, _synchronizeComponent, getLogger()), 
395                                    Event.NODE_ADDED
396                                    | Event.NODE_REMOVED
397                                    | Event.NODE_MOVED
398                                    | Event.PROPERTY_ADDED
399                                    | Event.PROPERTY_CHANGED
400                                    | Event.PROPERTY_REMOVED, 
401                                    sitePath + "/" + ActivityHolder.ACTIVITIES_ROOT_NODE_NAME, true, null, null, false);
402        }
403        catch (RepositoryException ex)
404        {
405            throw new AmetysRepositoryException(ex);
406        }
407        finally
408        {
409            _getMemoryCache().invalidateAll();
410        }
411        
412        return site;
413    }
414    
415    /**
416     * Creates a site with the given name.
417     * @param siteName the site name.
418     * @return the newly created {@link Site}.
419     * @throws RepositoryIntegrityViolationException if the named site already exists.
420     */
421    public Site createSite(String siteName) throws RepositoryIntegrityViolationException
422    {
423        return createSite(siteName, null);
424    }
425    
426    /**
427     * Creates a site with the given name, from another site to copy 
428     * @param site the site to be copied
429     * @param siteName the site name
430     * @return the newly created {@link Site}.
431     * @throws RepositoryIntegrityViolationException if the named site already exists.
432     */
433    public Site copySite(Site site, String siteName) throws RepositoryIntegrityViolationException
434    {
435        return copySite(site, null, siteName);
436    }
437    
438    /**
439     * Creates a site with the given name, from another site to copy 
440     * @param site the site to be copied
441     * @param parentId the id of the parent site. Can be null.
442     * @param siteName the site name
443     * @return the newly created {@link Site}.
444     * @throws RepositoryIntegrityViolationException if the named site already exists.
445     */
446    public Site copySite(Site site, String parentId, String siteName) throws RepositoryIntegrityViolationException
447    {
448        ModifiableTraversableAmetysObject root = null;
449        if (parentId != null)
450        {
451            root = _resolver.resolveById(parentId);
452        }
453        else
454        {
455            root = _resolver.resolveByPath(ROOT_SITES_PATH);
456        }
457        
458        Site cSite = site.copyTo(root, siteName);
459        
460        _getMemoryCache().invalidateAll();
461        
462        // Clear url and title
463        cSite.removeValue("url");
464        cSite.removeValue("title");
465        
466        // Change reference to ametys object, re-init contents workflow, update site name, ...
467        _copySiteComponent.updateSiteAfterCopy(site, cSite);
468        
469        return cSite;
470        
471    }
472    
473    /**
474     * Returns true if the given site exists.
475     * @param siteName the site name.
476     * @return true if the given site exists.
477     */
478    public boolean hasSite(String siteName)
479    {
480        Map<String, String> uuidCache = _getUUIDCache();
481        if (uuidCache.containsKey(siteName))
482        {
483            // As cache is computed from default JCR workspace, we need to check if the site exists into the current JCR workspace 
484            return _resolver.hasAmetysObjectForId(uuidCache.get(siteName));
485        }
486        return false;
487    }
488    
489    /**
490     * Returns the named {@link Site}.
491     * @param siteName the site name.
492     * @return the named {@link Site} or null if the siteName is null
493     * @throws UnknownAmetysObjectException if the named site does not exist
494     */
495    public Site getSite(String siteName) throws UnknownAmetysObjectException
496    {
497        return getSite(siteName, null);
498    }
499    
500    /**
501     * Returns the named {@link Site}.
502     * @param siteName the site name.
503     * @param session the JCR Session to use to retrieve the site
504     * @return the named {@link Site} or null if the siteName is null
505     * @throws UnknownAmetysObjectException if the named site does not exist
506     */
507    public Site getSite(String siteName, Session session) throws UnknownAmetysObjectException
508    {
509        if (StringUtils.isBlank(siteName))
510        {
511            return null;
512        }
513        
514        Request request = _getRequest();
515        if (request == null)
516        {
517            // There is no request to store cache
518            return _computeSite(siteName, session);
519        }
520        
521        Cache<RequestSiteCacheKey, Site> sitesCache = _getRequestCache();
522        
523        // The site key in the cache is of the form {site + workspace}.
524        String workspaceName = session != null
525                               ? session.getWorkspace().getName()
526                               : _workspaceSelector.getWorkspace();
527        RequestSiteCacheKey siteKey = RequestSiteCacheKey.of(siteName, workspaceName);
528        
529        try
530        {
531            Site site = sitesCache.get(siteKey, __ -> _computeSite(siteName, session));
532            return site;
533        }
534        catch (CacheException e)
535        {
536            if (e.getCause() instanceof UnknownAmetysObjectException)
537            {
538                throw (UnknownAmetysObjectException) e.getCause();
539            }
540            else 
541            {
542                throw e;
543            }
544        }
545    }
546    
547    private Site _computeSite(String siteName, Session session)
548    {
549        try
550        {
551            if (hasSite(siteName))
552            {
553                String uuid = _getUUIDCache().get(siteName);
554                return _resolver.<Site>resolveById(uuid, session);
555            }
556            else
557            {
558                // Site may be present in cache for 'default' workspace but does not exist in current JCR workspace
559                throw new UnknownAmetysObjectException("There is no site named '" + siteName + "'");
560            }
561        }
562        catch (RepositoryException e)
563        {
564            throw new AmetysRepositoryException(e);
565        }
566    }
567    
568    /**
569     * Clear the site cache
570     */
571    public void clearCache ()
572    {
573        _getMemoryCache().invalidateAll();
574        _getRequestCache().invalidateAll();
575    }
576
577    private Cache<String, String> _getMemoryCache()
578    {
579        return _cacheManager.get(MEMORY_CACHE);
580    }
581    
582    private Cache<RequestSiteCacheKey, Site> _getRequestCache()
583    {
584        return _cacheManager.get(REQUEST_CACHE);
585    }
586    
587    private static final class RequestSiteCacheKey extends AbstractCacheKey
588    {
589        private RequestSiteCacheKey(String siteName, String workspaceName)
590        {
591            super(siteName, workspaceName);
592        }
593        
594        static RequestSiteCacheKey of(String siteName, String workspaceName)
595        {
596            return new RequestSiteCacheKey(siteName, workspaceName);
597        }
598    }
599}