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.HashMap;
021import java.util.HashSet;
022import java.util.Iterator;
023import java.util.Map;
024import java.util.Set;
025
026import javax.jcr.Repository;
027import javax.jcr.RepositoryException;
028import javax.jcr.Session;
029import javax.jcr.observation.Event;
030import javax.jcr.observation.ObservationManager;
031
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;
042
043import org.ametys.core.right.RightManager;
044import org.ametys.core.user.CurrentUserProvider;
045import org.ametys.core.user.UserIdentity;
046import org.ametys.core.user.population.PopulationContextHelper;
047import org.ametys.plugins.repository.AmetysObject;
048import org.ametys.plugins.repository.AmetysObjectIterable;
049import org.ametys.plugins.repository.AmetysObjectResolver;
050import org.ametys.plugins.repository.AmetysRepositoryException;
051import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
052import org.ametys.plugins.repository.RepositoryConstants;
053import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
054import org.ametys.plugins.repository.TraversableAmetysObject;
055import org.ametys.plugins.repository.UnknownAmetysObjectException;
056import org.ametys.plugins.repository.provider.AdminSessionProvider;
057import org.ametys.plugins.repository.provider.WorkspaceSelector;
058import org.ametys.web.live.LiveWorkspaceListener;
059import org.ametys.web.repository.page.CopySiteComponent;
060import org.ametys.web.synchronization.SynchronizeComponent;
061
062/**
063 * Helper component for managing sites.
064 */
065public class SiteManager extends AbstractLogEnabled implements Serviceable, Component, Contextualizable
066{
067    /** Avalon Role */
068    public static final String ROLE = SiteManager.class.getName();
069 
070    /** Constant for storing the sites cache in request attribute. */
071    public static final String SITES_KEY = SiteManager.class.getName();
072    
073    /** sites root's JCR node name */
074    public static final String ROOT_SITES = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":sites";
075    
076    /** sites root's JCR path */
077    public static final String ROOT_SITES_PATH = "/" + ROOT_SITES;
078    
079    private AmetysObjectResolver _resolver;
080    private Repository _repository;
081    private AdminSessionProvider _adminSessionProvider;
082    private SynchronizeComponent _synchronizeComponent;
083    private CopySiteComponent _copySiteComponent;
084    private WorkspaceSelector _workspaceSelector;
085    private RightManager _rightManager;
086    private CurrentUserProvider _currentUserProvider;
087    private PopulationContextHelper _populationContextHelper;
088    
089    private Context _context;
090
091
092    
093    @Override
094    public void service(ServiceManager manager) throws ServiceException
095    {
096        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
097        _repository = (Repository) manager.lookup("javax.jcr.Repository");
098        _adminSessionProvider = (AdminSessionProvider) manager.lookup(AdminSessionProvider.ROLE);
099        _synchronizeComponent = (SynchronizeComponent) manager.lookup(SynchronizeComponent.ROLE);
100        _copySiteComponent = (CopySiteComponent) manager.lookup(CopySiteComponent.ROLE);
101        _workspaceSelector = (WorkspaceSelector) manager.lookup(WorkspaceSelector.ROLE);
102        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
103        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
104        _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE);
105    }
106    
107    public void contextualize(Context context) throws ContextException
108    {
109        _context = context;
110    }
111    
112    private Request _getRequest ()
113    {
114        try 
115        {
116            return (Request) _context.get(ContextHelper.CONTEXT_REQUEST_OBJECT);
117        } 
118        catch (ContextException ce)
119        {
120            getLogger().info("Unable to get the request", ce);
121            return null;
122        }
123    }
124    
125    /**
126     * Returns the sites names.
127     * @return the sites names.
128     */
129    public Collection<String> getRootSiteNames()
130    {
131        TraversableAmetysObject root = _resolver.resolveByPath(ROOT_SITES_PATH);
132        AmetysObjectIterable<AmetysObject> sites = root.getChildren();
133        
134        ArrayList<String> result = new ArrayList<>();
135        
136        for (AmetysObject object : sites)
137        {
138            result.add(object.getName());
139        }
140        
141        return result;
142    }
143    
144    /**
145     * Returns the sites names.
146     * @return the sites names.
147     */
148    public Collection<String> getSiteNames()
149    {
150        ArrayList<String> result = new ArrayList<>();
151        AmetysObjectIterable<Site> sites = getSites();
152        
153        for (Site site : sites)
154        {
155            result.add(site.getName());
156        }
157        
158        return result;
159    }
160    
161    /**
162     * Get the granted site names for the current user
163     * @return The name of sites the current user can access
164     */
165    public Set<String> getGrantedSites()
166    {
167        return getGrantedSites(_currentUserProvider.getUser());
168    }
169    
170    /**
171     * Get the granted site names for user
172     * @param user The user
173     * @return The name of sites the user can access
174     */
175    public Set<String> getGrantedSites(UserIdentity user)
176    {
177        Set<String> grantedSiteNames = new HashSet<>();
178        
179        Request request = _getRequest();
180        
181        for (String siteName : getSiteNames())
182        {
183            if (_populationContextHelper.getUserPopulationsOnContext("/sites/" + siteName, false).contains(user.getPopulationId()))
184            {
185                try
186                {
187                    request.setAttribute("siteName", siteName); // Setting temporarily this attribute to check user rights on any object on this site
188                    if (!_rightManager.getUserRights(user, "/cms").isEmpty())
189                    {
190                        grantedSiteNames.add(siteName);
191                    }
192                }
193                finally
194                {
195                    request.setAttribute("siteName", null);
196                }
197            }
198        }
199        
200        return grantedSiteNames;
201    }
202    
203    /**
204     * Determines if the user has granted access to the site 
205     * @param user The user
206     * @param siteName The site name
207     * @return <code>true</code> if the user can access to the site
208     */
209    public boolean isGrantedSite (UserIdentity user, String siteName)
210    {
211        Object oldValue = null;
212        
213        Request request = _getRequest();
214        try
215        {
216            oldValue = request.getAttribute("siteName");
217            request.setAttribute("siteName", siteName); // Setting temporarily this attribute to check user rights on any object on this site
218            return !_rightManager.getUserRights(user, "/cms").isEmpty();
219        }
220        finally
221        {
222            request.setAttribute("siteName", oldValue);
223        }
224    }
225    
226    /**
227     * Returns the root for sites.
228     * @return the root for sites.
229     */
230    public ModifiableTraversableAmetysObject getRoot()
231    {
232        return _resolver.resolveByPath(ROOT_SITES_PATH);
233    }
234    
235    /**
236     * Returns the root sites.
237     * @return the root sites.
238     */
239    public AmetysObjectIterable<Site> getRootSites()
240    {
241        TraversableAmetysObject root = _resolver.resolveByPath(ROOT_SITES_PATH);
242        return root.getChildren();
243    }
244    
245    /**
246     * Returns the all sites.
247     * @return the all sites.
248     */
249    public AmetysObjectIterable<Site> getSites()
250    {
251        String jcrQuery = "//element(*, ametys:site)";
252        return _resolver.query(jcrQuery);
253    }
254    
255    /**
256     * Creates a site with the given name.
257     * @param siteName the site name.
258     * @param parentId the id of the parent site. Can be null.
259     * @return the newly created {@link Site}.
260     * @throws RepositoryIntegrityViolationException if the named site already exists.
261     */
262    public Site createSite(String siteName, String parentId) throws RepositoryIntegrityViolationException
263    {
264        ModifiableTraversableAmetysObject root = null;
265        if (parentId != null)
266        {
267            Site site = _resolver.resolveById(parentId);
268            if (!site.hasChild(ROOT_SITES))
269            {
270                site.createChild(ROOT_SITES, "ametys:sites");
271            }
272            root = site.getChild(ROOT_SITES);
273        }
274        else
275        {
276            root = _resolver.resolveByPath(ROOT_SITES_PATH);
277        }
278        
279        Site site = (Site) root.createChild(siteName, "ametys:site");
280        try
281        {
282            String sitePath = site.getNode().getPath();
283            
284            // Create the resource explorer root.
285            site.getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources", "ametys:resources-collection");
286
287            Session session = _adminSessionProvider.getAdminSession();
288            ObservationManager manager = session.getWorkspace().getObservationManager();
289            
290            manager.addEventListener(new LiveWorkspaceListener(_repository, _synchronizeComponent, getLogger()), 
291                                     Event.NODE_ADDED
292                                     | Event.NODE_REMOVED
293                                     | Event.NODE_MOVED
294                                     | Event.PROPERTY_ADDED
295                                     | Event.PROPERTY_CHANGED
296                                     | Event.PROPERTY_REMOVED, 
297                                     sitePath + "/ametys-internal:plugins", true, null, null, false);
298            
299            manager.addEventListener(new LiveWorkspaceListener(_repository, _synchronizeComponent, getLogger()), 
300                                     Event.NODE_ADDED
301                                     | Event.NODE_REMOVED
302                                     | Event.NODE_MOVED
303                                     | Event.PROPERTY_ADDED
304                                     | Event.PROPERTY_CHANGED
305                                     | Event.PROPERTY_REMOVED, 
306                                     sitePath + "/ametys-internal:resources", true, null, null, false);
307        }
308        catch (RepositoryException ex)
309        {
310            throw new AmetysRepositoryException(ex);
311        }
312        
313        return site;
314    }
315    
316    /**
317     * Creates a site with the given name.
318     * @param siteName the site name.
319     * @return the newly created {@link Site}.
320     * @throws RepositoryIntegrityViolationException if the named site already exists.
321     */
322    public Site createSite(String siteName) throws RepositoryIntegrityViolationException
323    {
324        return createSite (siteName, null);
325    }
326    
327    /**
328     * Creates a site with the given name, from another site to copy 
329     * @param site the site to be copied
330     * @param siteName the site name
331     * @return the newly created {@link Site}.
332     * @throws RepositoryIntegrityViolationException if the named site already exists.
333     */
334    public Site copySite(Site site, String siteName) throws RepositoryIntegrityViolationException
335    {
336        return copySite(site, null, siteName);
337    }
338    
339    /**
340     * Creates a site with the given name, from another site to copy 
341     * @param site the site to be copied
342     * @param parentId the id of the parent site. Can be null.
343     * @param siteName the site name
344     * @return the newly created {@link Site}.
345     * @throws RepositoryIntegrityViolationException if the named site already exists.
346     */
347    public Site copySite(Site site, String parentId, String siteName) throws RepositoryIntegrityViolationException
348    {
349        ModifiableTraversableAmetysObject root = null;
350        if (parentId != null)
351        {
352            root = _resolver.resolveById(parentId);
353        }
354        else
355        {
356            root = _resolver.resolveByPath(ROOT_SITES_PATH);
357        }
358        
359        Site cSite = site.copyTo(root, siteName);
360        
361        // Clear url and title
362        cSite.removeValue("url");
363        cSite.removeValue("title");
364        
365        // Change reference to ametys object, re-init contents workflow, update site name, ...
366        _copySiteComponent.updateSiteAfterCopy(site, cSite);
367        
368        return cSite;
369        
370    }
371    
372    
373    /**
374     * Returns true if the given site exists.
375     * @param siteName the site name.
376     * @return true if the given site exists.
377     */
378    public boolean hasSite(String siteName)
379    {
380        if (siteName == null)
381        {
382            return false;
383        }
384        
385        Request request = _getRequest();
386        if (request == null)
387        {
388            // There is no request to store cache
389            return _hasSite(siteName);
390        }
391        
392        @SuppressWarnings("unchecked")
393        Map<String, Site> sitesCache = (Map<String, Site>) request.getAttribute(SITES_KEY);
394        if (sitesCache == null)
395        {
396            sitesCache = new HashMap<>();
397            request.setAttribute(SITES_KEY, sitesCache);
398        }
399        
400        // The site key in the cache is of the form "site/workspace".
401        String siteKey = siteName + "/" + _workspaceSelector.getWorkspace();
402        
403        Site site = sitesCache.get(siteKey);
404        if (site != null)
405        {
406            // Site is in cache
407            return true;
408        }
409        
410        site = _getSite(siteName);
411        if (site != null)
412        {
413            // Store site in cache
414            sitesCache.put(siteKey, site);
415            return true;
416        }
417        
418        return false;
419    }
420    
421    private boolean _hasSite (String siteName)
422    {
423        String jcrQuery = "//element(" + siteName + ", ametys:site)";
424        AmetysObjectIterable<Site> sites = _resolver.query(jcrQuery);
425        
426        return sites.iterator().hasNext();
427    }
428    
429    /**
430     * Returns the named {@link Site}.
431     * @param siteName the site name.
432     * @return the named {@link Site}.
433     * @throws UnknownAmetysObjectException if the named site does not exist.
434     */
435    public Site getSite(String siteName) throws UnknownAmetysObjectException
436    {
437        Request request = _getRequest();
438        if (request == null)
439        {
440            // There is no request to store cache
441            return _getSite(siteName);
442        }
443        
444        @SuppressWarnings("unchecked")
445        Map<String, Site> sitesCache = (Map<String, Site>) request.getAttribute(SITES_KEY);
446        if (sitesCache == null)
447        {
448            sitesCache = new HashMap<>();
449            request.setAttribute(SITES_KEY, sitesCache);
450        }
451        
452        // The site key in the cache is of the form "site/workspace".
453        String siteKey = siteName + "/" + _workspaceSelector.getWorkspace();
454        
455        Site site = sitesCache.get(siteKey);
456        if (site == null)
457        {
458            site = _getSite(siteName);
459            if (site != null)
460            {
461                // Store site in cache
462                sitesCache.put(siteKey, site);
463            }
464        }
465        
466        return site;
467    }
468    
469    /**
470     * Clear the site cache
471     */
472    public void clearCache ()
473    {
474        Request request = _getRequest();
475        request.setAttribute(SITES_KEY, null);
476    }
477    
478    private Site _getSite (String siteName) throws UnknownAmetysObjectException
479    {
480        String jcrQuery = "//element(" + siteName + ", ametys:site)";
481        AmetysObjectIterable<Site> sites = _resolver.query(jcrQuery);
482        Iterator<Site> it = sites.iterator();
483        
484        if (it.hasNext())
485        {
486            return it.next();
487        }
488        
489        return null;
490    }
491}