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