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