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        Request request = _getRequest();
212        try
213        {
214            request.setAttribute("siteName", siteName); // Setting temporarily this attribute to check user rights on any object on this site
215            return !_rightManager.getUserRights(user, "/cms").isEmpty();
216        }
217        finally
218        {
219            request.setAttribute("siteName", null);
220        }
221    }
222    
223    /**
224     * Returns the root for sites.
225     * @return the root for sites.
226     */
227    public ModifiableTraversableAmetysObject getRoot()
228    {
229        return _resolver.resolveByPath(ROOT_SITES_PATH);
230    }
231    
232    /**
233     * Returns the root sites.
234     * @return the root sites.
235     */
236    public AmetysObjectIterable<Site> getRootSites()
237    {
238        TraversableAmetysObject root = _resolver.resolveByPath(ROOT_SITES_PATH);
239        return root.getChildren();
240    }
241    
242    /**
243     * Returns the all sites.
244     * @return the all sites.
245     */
246    public AmetysObjectIterable<Site> getSites()
247    {
248        String jcrQuery = "//element(*, ametys:site)";
249        return _resolver.query(jcrQuery);
250    }
251    
252    /**
253     * Creates a site with the given name.
254     * @param siteName the site name.
255     * @param parentId the id of the parent site. Can be null.
256     * @return the newly created {@link Site}.
257     * @throws RepositoryIntegrityViolationException if the named site already exists.
258     */
259    public Site createSite(String siteName, String parentId) throws RepositoryIntegrityViolationException
260    {
261        ModifiableTraversableAmetysObject root = null;
262        if (parentId != null)
263        {
264            Site site = _resolver.resolveById(parentId);
265            if (!site.hasChild(ROOT_SITES))
266            {
267                site.createChild(ROOT_SITES, "ametys:sites");
268            }
269            root = site.getChild(ROOT_SITES);
270        }
271        else
272        {
273            root = _resolver.resolveByPath(ROOT_SITES_PATH);
274        }
275        
276        Site site = (Site) root.createChild(siteName, "ametys:site");
277        try
278        {
279            String sitePath = site.getNode().getPath();
280            
281            // Create the resource explorer root.
282            site.getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":resources", "ametys:resources-collection");
283
284            Session session = _adminSessionProvider.getAdminSession();
285            ObservationManager manager = session.getWorkspace().getObservationManager();
286            
287            manager.addEventListener(new LiveWorkspaceListener(_repository, _synchronizeComponent, getLogger()), 
288                                     Event.NODE_ADDED
289                                     | Event.NODE_REMOVED
290                                     | Event.NODE_MOVED
291                                     | Event.PROPERTY_ADDED
292                                     | Event.PROPERTY_CHANGED
293                                     | Event.PROPERTY_REMOVED, 
294                                     sitePath + "/ametys-internal:plugins", true, null, null, false);
295            
296            manager.addEventListener(new LiveWorkspaceListener(_repository, _synchronizeComponent, getLogger()), 
297                                     Event.NODE_ADDED
298                                     | Event.NODE_REMOVED
299                                     | Event.NODE_MOVED
300                                     | Event.PROPERTY_ADDED
301                                     | Event.PROPERTY_CHANGED
302                                     | Event.PROPERTY_REMOVED, 
303                                     sitePath + "/ametys-internal:resources", true, null, null, false);
304        }
305        catch (RepositoryException ex)
306        {
307            throw new AmetysRepositoryException(ex);
308        }
309        
310        return site;
311    }
312    
313    /**
314     * Creates a site with the given name.
315     * @param siteName the site name.
316     * @return the newly created {@link Site}.
317     * @throws RepositoryIntegrityViolationException if the named site already exists.
318     */
319    public Site createSite(String siteName) throws RepositoryIntegrityViolationException
320    {
321        return createSite (siteName, null);
322    }
323    
324    /**
325     * Creates a site with the given name, from another site to copy 
326     * @param site the site to be copied
327     * @param siteName the site name
328     * @return the newly created {@link Site}.
329     * @throws RepositoryIntegrityViolationException if the named site already exists.
330     */
331    public Site copySite(Site site, String siteName) throws RepositoryIntegrityViolationException
332    {
333        return copySite(site, null, siteName);
334    }
335    
336    /**
337     * Creates a site with the given name, from another site to copy 
338     * @param site the site to be copied
339     * @param parentId the id of the parent site. Can be null.
340     * @param siteName the site name
341     * @return the newly created {@link Site}.
342     * @throws RepositoryIntegrityViolationException if the named site already exists.
343     */
344    public Site copySite(Site site, String parentId, String siteName) throws RepositoryIntegrityViolationException
345    {
346        ModifiableTraversableAmetysObject root = null;
347        if (parentId != null)
348        {
349            root = _resolver.resolveById(parentId);
350        }
351        else
352        {
353            root = _resolver.resolveByPath(ROOT_SITES_PATH);
354        }
355        
356        Site cSite = site.copyTo(root, siteName);
357        
358        // Clear url and title
359        cSite.getMetadataHolder().removeMetadata("url");
360        cSite.getMetadataHolder().removeMetadata("title");
361        
362        // Change reference to ametys object, re-init contents workflow, update site name, ...
363        _copySiteComponent.updateSiteAfterCopy(site, cSite);
364        
365        return cSite;
366        
367    }
368    
369    
370    /**
371     * Returns true if the given site exists.
372     * @param siteName the site name.
373     * @return true if the given site exists.
374     */
375    public boolean hasSite(String siteName)
376    {
377        if (siteName == null)
378        {
379            return false;
380        }
381        
382        Request request = _getRequest();
383        if (request == null)
384        {
385            // There is no request to store cache
386            return _hasSite(siteName);
387        }
388        
389        @SuppressWarnings("unchecked")
390        Map<String, Site> sitesCache = (Map<String, Site>) request.getAttribute(SITES_KEY);
391        if (sitesCache == null)
392        {
393            sitesCache = new HashMap<>();
394            request.setAttribute(SITES_KEY, sitesCache);
395        }
396        
397        // The site key in the cache is of the form "site/workspace".
398        String siteKey = siteName + "/" + _workspaceSelector.getWorkspace();
399        
400        Site site = sitesCache.get(siteKey);
401        if (site != null)
402        {
403            // Site is in cache
404            return true;
405        }
406        
407        site = _getSite(siteName);
408        if (site != null)
409        {
410            // Store site in cache
411            sitesCache.put(siteKey, site);
412            return true;
413        }
414        
415        return false;
416    }
417    
418    private boolean _hasSite (String siteName)
419    {
420        String jcrQuery = "//element(" + siteName + ", ametys:site)";
421        AmetysObjectIterable<Site> sites = _resolver.query(jcrQuery);
422        
423        return sites.iterator().hasNext();
424    }
425    
426    /**
427     * Returns the named {@link Site}.
428     * @param siteName the site name.
429     * @return the named {@link Site}.
430     * @throws UnknownAmetysObjectException if the named site does not exist.
431     */
432    public Site getSite(String siteName) throws UnknownAmetysObjectException
433    {
434        Request request = _getRequest();
435        if (request == null)
436        {
437            // There is no request to store cache
438            return _getSite(siteName);
439        }
440        
441        @SuppressWarnings("unchecked")
442        Map<String, Site> sitesCache = (Map<String, Site>) request.getAttribute(SITES_KEY);
443        if (sitesCache == null)
444        {
445            sitesCache = new HashMap<>();
446            request.setAttribute(SITES_KEY, sitesCache);
447        }
448        
449        // The site key in the cache is of the form "site/workspace".
450        String siteKey = siteName + "/" + _workspaceSelector.getWorkspace();
451        
452        Site site = sitesCache.get(siteKey);
453        if (site == null)
454        {
455            site = _getSite(siteName);
456            if (site != null)
457            {
458                // Store site in cache
459                sitesCache.put(siteKey, site);
460            }
461        }
462        
463        return site;
464    }
465    
466    /**
467     * Clear the site cache
468     */
469    public void clearCache ()
470    {
471        Request request = _getRequest();
472        request.setAttribute(SITES_KEY, null);
473    }
474    
475    private Site _getSite (String siteName) throws UnknownAmetysObjectException
476    {
477        String jcrQuery = "//element(" + siteName + ", ametys:site)";
478        AmetysObjectIterable<Site> sites = _resolver.query(jcrQuery);
479        Iterator<Site> it = sites.iterator();
480        
481        if (it.hasNext())
482        {
483            return it.next();
484        }
485        
486        return null;
487    }
488}