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