001/*
002 *  Copyright 2016 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 */
016
017package org.ametys.web.transformation.xslt;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.net.MalformedURLException;
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collection;
025import java.util.Collections;
026import java.util.HashMap;
027import java.util.Iterator;
028import java.util.List;
029import java.util.Map;
030
031import org.apache.avalon.framework.service.ServiceException;
032import org.apache.avalon.framework.service.ServiceManager;
033import org.apache.cocoon.components.ContextHelper;
034import org.apache.cocoon.environment.Request;
035import org.apache.cocoon.environment.Response;
036import org.apache.commons.lang.StringUtils;
037import org.apache.excalibur.source.SourceResolver;
038import org.apache.excalibur.source.impl.FileSource;
039import org.w3c.dom.Element;
040import org.w3c.dom.Node;
041import org.w3c.dom.NodeList;
042
043import org.ametys.cms.repository.Content;
044import org.ametys.cms.transformation.ImageResolverHelper;
045import org.ametys.core.right.RightManager;
046import org.ametys.core.right.RightManager.RightResult;
047import org.ametys.core.util.I18nUtils;
048import org.ametys.core.util.dom.AmetysNodeList;
049import org.ametys.core.util.dom.EmptyElement;
050import org.ametys.core.util.dom.FileElement;
051import org.ametys.core.util.dom.MapElement;
052import org.ametys.core.util.dom.MapElement.MapNode;
053import org.ametys.core.util.dom.StringElement;
054import org.ametys.plugins.explorer.resources.Resource;
055import org.ametys.plugins.explorer.resources.ResourceCollection;
056import org.ametys.plugins.explorer.resources.dom.ResourceCollectionElement;
057import org.ametys.plugins.explorer.resources.dom.ResourceElement;
058import org.ametys.plugins.repository.AmetysObject;
059import org.ametys.plugins.repository.AmetysObjectIterable;
060import org.ametys.plugins.repository.AmetysObjectResolver;
061import org.ametys.plugins.repository.UnknownAmetysObjectException;
062import org.ametys.plugins.repository.metadata.CompositeMetadata;
063import org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType;
064import org.ametys.plugins.repository.metadata.UnknownMetadataException;
065import org.ametys.plugins.repository.query.expression.Expression.Operator;
066import org.ametys.runtime.parameter.ParameterHelper;
067import org.ametys.web.URIPrefixHandler;
068import org.ametys.web.WebConstants;
069import org.ametys.web.renderingcontext.RenderingContext;
070import org.ametys.web.renderingcontext.RenderingContextHandler;
071import org.ametys.web.repository.content.WebContent;
072import org.ametys.web.repository.dom.PageElement;
073import org.ametys.web.repository.dom.SitemapElement;
074import org.ametys.web.repository.page.Page;
075import org.ametys.web.repository.page.PageQueryHelper;
076import org.ametys.web.repository.page.Zone;
077import org.ametys.web.repository.page.ZoneItem;
078import org.ametys.web.repository.site.Site;
079import org.ametys.web.repository.site.SiteManager;
080import org.ametys.web.repository.sitemap.Sitemap;
081import org.ametys.web.service.ServiceExtensionPoint;
082import org.ametys.web.service.ServiceParameter;
083import org.ametys.web.service.ServiceParameterOrRepeater;
084import org.ametys.web.service.ServiceParameterRepeater;
085import org.ametys.web.site.SiteConfigurationExtensionPoint;
086import org.ametys.web.tags.TagExpression;
087
088/**
089 * Helper component to be used from XSL stylesheets.
090 */
091public class AmetysXSLTHelper extends org.ametys.cms.transformation.xslt.AmetysXSLTHelper
092{
093    private static SiteManager _siteManager;
094    private static RenderingContextHandler _renderingContextHandler;
095    private static SiteConfigurationExtensionPoint _siteConf;
096    private static RightManager _rightManager;
097    private static URIPrefixHandler _prefixHandler;
098    private static SourceResolver _sourceResolver;
099    private static ServiceExtensionPoint _serviceEP;
100    
101    @Override
102    public void service(ServiceManager manager) throws ServiceException
103    {
104        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
105        _renderingContextHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE);
106        _siteConf = (SiteConfigurationExtensionPoint) manager.lookup(SiteConfigurationExtensionPoint.ROLE);
107        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
108        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
109        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
110        _prefixHandler = (URIPrefixHandler) manager.lookup(URIPrefixHandler.ROLE);
111        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
112        _serviceEP = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE);
113    }
114    
115    /**
116     * Returns the current URI prefix, depending on the rendering context.
117     * @return the current URI prefix.
118     */
119    public static String uriPrefix()
120    {
121        return _prefixHandler.getUriPrefix();
122    }
123    
124    /**
125     * Returns the URI prefix corresponding to the current site, depending on the rendering context.
126     * @return the URI prefix corresponding to the current site.
127     */
128    public static String siteUriPrefix()
129    {
130        Request request = ContextHelper.getRequest(_context);
131        String siteName = (String) request.getAttribute("site");
132        return _prefixHandler.getUriPrefix(siteName);
133    }
134    
135    /**
136     * Returns the absolute URI prefix, depending on the rendering context.
137     * @return the absolute URI prefix.
138     */
139    public static String absoluteUriPrefix()
140    {
141        return _prefixHandler.getAbsoluteUriPrefix();
142    }
143    
144    /**
145     * Returns the absolute URI prefix corresponding to the current site, depending on the rendering context.
146     * @return the absolute URI prefix corresponding to the current site.
147     */
148    public static String absoluteSiteUriPrefix()
149    {
150        Request request = ContextHelper.getRequest(_context);
151        String siteName = (String) request.getAttribute("site");
152        return _prefixHandler.getAbsoluteUriPrefix(siteName);
153    }
154    
155    /**
156     * Returns the absolute URI prefix corresponding to the given site, depending on the rendering context.
157     * @param siteName The site name. Can be null to get the current site.
158     * @return the absolute URI prefix corresponding to the current site.
159     */
160    public static String absoluteSiteUriPrefix(String siteName)
161    {
162        if (StringUtils.isEmpty(siteName))
163        {
164            return absoluteSiteUriPrefix();
165        }
166        return _prefixHandler.getAbsoluteUriPrefix(siteName);
167    }
168    
169    /**
170     * Returns the current skin name.
171     * @return the current skin name.
172     */
173    public static String skin()
174    {
175        Request request = ContextHelper.getRequest(_context);
176        return (String) request.getAttribute("skin");
177    }
178    
179    /**
180     * Returns the current template name.
181     * @return the current template name.
182     */
183    public static String template()
184    {
185        Request request = ContextHelper.getRequest(_context);
186        return (String) request.getAttribute("template");
187    }
188    
189    /**
190     * Returns the current sitemap name.
191     * @return the current sitemap name.
192     */
193    public static String lang()
194    {
195        Request request = ContextHelper.getRequest(_context);
196        return (String) request.getAttribute("sitemapLanguage");
197    }
198    
199    /**
200     * Returns the current sitemap name.
201     * @param pageId The page identifier to get sitemap on
202     * @return the current sitemap name.
203     */
204    public static String lang(String pageId)
205    {
206        try
207        {
208            Page page = _getPage(pageId);
209            return page.getSitemapName();
210        }
211        catch (UnknownAmetysObjectException e)
212        {
213            _logger.error("Can not get sitemap lang on page '" + pageId + "'", e);
214            return "";
215        }
216    }
217    
218    /**
219     * Computes the URI for the given resource in the current site's skin.<br>
220     * If the URI is requested by the front-office, it will be absolutized.
221     * @param path the resource path.
222     * @return the URI for the given resource.
223     */
224    public static String skinURL(String path)
225    {
226        Request request = ContextHelper.getRequest(_context);
227        String siteName = (String) request.getAttribute("site");
228        Site site = _siteManager.getSite(siteName);
229        String skin = (String) request.getAttribute("skin");
230        
231        String resourcePath = "/skins/" + skin + "/resources/" + path;
232        
233        return _getResourceURL(request, site, resourcePath);
234    }
235    
236    /**
237     * Computes the URI for the given image with a given heigth and width in the current site's skin.<br>
238     * If the URI is requested by the front-office, it will be absolutized.
239     * @param path the resource path
240     * @param height the height for the resource to get
241     * @param width the width for the resource to get
242     * @return the URI of the given resource
243     */
244    public static String skinImageURL(String path, int height, int width)
245    {
246        String skinPath = skinURL(path);
247        return StringUtils.substringBeforeLast(skinPath, ".") + "_" + height + "x" + width + "." + StringUtils.substringAfterLast(skinPath, "."); 
248    }
249    
250    /**
251     * Computes the base 64 representation of the image at the specified path. <br>
252     * @param path the path of the image
253     * @return the base 64-encoded image
254     * @throws IOException if an error occurs while trying to get the file
255     */
256    public static String skinImageBase64 (String path) throws IOException
257    {
258        FileSource source = (FileSource) _sourceResolver.resolveURI("skin://resources/" + path);
259        return _getResourceBase64(source); 
260    }
261    
262    /**
263     * Computes the URI for the given image with a given heigth and width in the current site's skin.<br>
264     * If the URI is requested by the front-office, it will be absolutized.
265     * @param path the resource path
266     * @param maxHeight the maximum height for the resource to get
267     * @param maxWidth the maximum width for the resource to get
268     * @return the URI of the given resource
269     */
270    public static String skinBoundedImageURL(String path, int maxHeight, int maxWidth)
271    {
272        String skinPath = skinURL(path);
273        return StringUtils.substringBeforeLast(skinPath, ".") + "_max" + maxHeight + "x" + maxWidth + "." + StringUtils.substringAfterLast(skinPath, "."); 
274    }
275    
276    /**
277     * Computes the URI for the given resource in the current template.<br>
278     * If the URI is requested by the front-office, it will be absolutized.
279     * @param path the resource path.
280     * @return the URI for the given resource.
281     */
282    public static String templateURL(String path)
283    {
284        Request request = ContextHelper.getRequest(_context);
285        String siteName = (String) request.getAttribute("site");
286        Site site = _siteManager.getSite(siteName);
287        String skin = (String) request.getAttribute("skin");
288        String template = (String) request.getAttribute("template");
289        
290        String resourcePath = "/skins/" + skin + "/templates/" + template + "/resources/" + path;
291        
292        return _getResourceURL(request, site, resourcePath);
293    }
294    
295    /**
296     * Computes the URI for the given resource in the given plugin.<br>
297     * If the URI is requested by the front-office, it will be absolutized.
298     * @param plugin the plugin name.
299     * @param path the resource path.
300     * @return the URI for the given resource.
301     */
302    public static String pluginResourceURL(String plugin, String path)
303    {
304        Request request = ContextHelper.getRequest(_context);
305        String siteName = (String) request.getAttribute("site");
306        Site site = _siteManager.getSite(siteName);
307        
308        String resourcePath = "/plugins/" + plugin + "/resources/" + path;
309        
310        return _getResourceURL(request, site, resourcePath);
311    }
312    
313    /**
314     * Computes the base 64 representation of the image at the specified path in the given plugin.<br>
315     * @param plugin the plugin's name.
316     * @param path the resource path.
317     * @return the base 64 encoding for the given resource.
318     * @throws IOException if an error occurs when trying to get the file
319     * @throws MalformedURLException if the url is invalid
320     */
321    public static String pluginImageBase64(String plugin, String path) throws MalformedURLException, IOException
322    {
323        FileSource source = (FileSource) _sourceResolver.resolveURI("plugin:" + plugin + "://resources/" + path);
324        return _getResourceBase64(source); 
325    }
326
327    
328    private static String _getResourceURL(Request request, Site site, String resourcePath)
329    {
330        String prefix;
331        switch (_renderingContextHandler.getRenderingContext())
332        {
333            case FRONT:
334                String[] aliases = site.getUrlAliases();
335                int position = Math.abs(resourcePath.hashCode()) % aliases.length;
336                
337                boolean absolute = request.getAttribute("forceAbsoluteUrl") != null ? (Boolean) request.getAttribute("forceAbsoluteUrl") : false;
338                prefix = position == 0 && !absolute ? siteUriPrefix() : aliases[position];  
339                return prefix + resourcePath;
340                
341            default:
342                prefix = StringUtils.trimToEmpty((String) request.getAttribute(WebConstants.PATH_PREFIX));
343                return request.getContextPath() + prefix + resourcePath;
344        }
345    }
346    
347    /**
348     * Get the base 64 encoding for the given source
349     * @param source the source 
350     * @return the base 64 encoding of the source
351     */
352    private static String _getResourceBase64(FileSource source)
353    {
354        if (source.exists())
355        {
356            
357            try (InputStream dataIs = source.getInputStream())
358            {
359                return ImageResolverHelper.resolveImageAsBase64(dataIs, source.getMimeType(), 0, 0, 0, 0);
360            }
361            catch (Exception e)
362            {
363                throw new IllegalStateException(e);
364            }
365        }
366
367        return "";
368    }
369    
370    /**
371     * Returns the current {@link RenderingContext}.
372     * @return the current {@link RenderingContext}.
373     */
374    public static String renderingContext()
375    {
376        return _renderingContextHandler.getRenderingContext().toString();
377    }
378    
379    /**
380     * Return the name of the zone beeing handled
381     * @param defaultValue If no page is handled currently, this value is returned (can be null, empty...)
382     * @return the name or the default value (so can be null or empty)
383     */
384    public static String zone(String defaultValue)
385    {
386        Request request = ContextHelper.getRequest(_context);
387        
388        return StringUtils.defaultIfEmpty((String) request.getAttribute(Zone.class.getName()), defaultValue);
389    }
390
391    /**
392     * Return the value of a site parameter as a String.
393     * @param parameter the parameter ID.
394     * @return the parameter value as a String.
395     */
396    public static String siteParameter(String parameter)
397    {
398        Request request = ContextHelper.getRequest(_context);
399        
400        String siteName = (String) request.getAttribute("site");
401        if (StringUtils.isBlank(siteName))
402        {
403            // In BO xsl
404            siteName = (String) request.getAttribute("siteName");
405        }
406        
407        return siteParameter(siteName, parameter);
408    }
409    
410    /**
411     * Return the value of a site parameter as a String.
412     * @param siteName the site name
413     * @param parameter the parameter ID.
414     * @return the parameter value as a String.
415     */
416    public static String siteParameter(String siteName, String parameter)
417    {
418        try
419        {
420            return _siteConf.getUntypedValue(siteName, parameter);
421        }
422        catch (Exception e)
423        {
424            String message = "Error retrieving the value of the site parameter " + parameter;
425            _logger.error(message, e);
426            throw new RuntimeException(message, e);
427        }
428    }
429    
430    /**
431     * Get the service parameters as a {@link Node}.
432     * @return the service parameters as a {@link Node}.
433     */
434    public static Node serviceParameters()
435    {
436        Request request = ContextHelper.getRequest(_context);
437        ZoneItem zoneItem = (ZoneItem) request.getAttribute(ZoneItem.class.getName());
438        CompositeMetadata parameters = zoneItem.getServiceParameters();
439        
440        String serviceId = zoneItem.getServiceId();
441        
442        Map<String, MapNode> paramValues = new HashMap<>();
443        
444        for (String paramName : parameters.getMetadataNames())
445        {
446            ServiceParameterOrRepeater paramDef = _serviceEP.getExtension(serviceId).getParameters().get(paramName);
447            if (paramDef != null && parameters.hasMetadata(paramName))
448            {
449                paramValues.putAll(_getParameterValue(paramName, parameters, paramDef, ""));
450            }
451        }
452        
453        return new MapElement("serviceParameters", paramValues);
454    }
455    
456    /**
457     * Returns the value of the given parameter for the current service, or the empty string if the parameter does not exist.
458     * @param parameter the parameter name.
459     * @return the value of the given parameter for the current service.
460     */
461    public static Node serviceParameter(String parameter)
462    {
463        return serviceParameter(parameter, "");
464    }
465    
466    /**
467     * Returns the value of the given parameter for the current service, or the provided default value if the parameter does not exist.
468     * @param parameter the parameter name or path.
469     * @param defaultValue the default value. Note that default value is ignored if the parameter is a composite parameter.
470     * @return the value of the given parameter for the current service.
471     */
472    public static Node serviceParameter(String parameter, String defaultValue)
473    {
474        Request request = ContextHelper.getRequest(_context);
475        ZoneItem zoneItem = (ZoneItem) request.getAttribute(ZoneItem.class.getName());
476        CompositeMetadata parameters = zoneItem.getServiceParameters();
477        
478        String serviceId = zoneItem.getServiceId();
479        ServiceParameterOrRepeater paramDef = _serviceEP.getExtension(serviceId).getParameters().get(parameter);
480        
481        if (paramDef == null)
482        {
483            // The parameter is unknown
484            if (StringUtils.isEmpty(defaultValue))
485            {
486                return null;
487            }
488            else
489            {
490                return new StringElement(parameter, Collections.EMPTY_MAP, defaultValue);
491            }
492        }
493        
494        Map<String, MapNode> value = _getParameterValue(parameter, parameters, paramDef, defaultValue);
495        
496        if (!value.containsKey(parameter))
497        {
498            return null;
499        }
500        else if (paramDef instanceof ServiceParameterRepeater || (paramDef instanceof ServiceParameter && ((ServiceParameter) paramDef).isMultiple()))
501        {
502            MapNode node = value.get(parameter);
503            @SuppressWarnings("unchecked")
504            Map<String, ? extends Object> values = (Map<String, ? extends Object>) node.getValue();
505            return new MapElement(parameter, node.getAttributes(), values);
506        }
507        else 
508        {
509            return new StringElement(parameter, value.get(parameter).getAttributes(), (String) value.get(parameter).getValue());
510        }
511    }
512    
513    private static String _convertTagName(String name)
514    {
515        char c = name.charAt(0);
516        if (c >= '0' && c <= '9')
517        {
518            String hex = Integer.toHexString(c);
519            return "_x" + StringUtils.leftPad(hex, 4, '0') + "_" + name.substring(1);
520        }
521        else
522        {
523            return name;
524        }
525    }
526    
527    private static Map<String, MapNode> _getParameterValue(String paramName, CompositeMetadata parentMetadata, ServiceParameterOrRepeater paramDef, String defaultValue)
528    {
529        Map<String, MapNode> paramValues = new HashMap<>();
530        
531        if (paramDef instanceof ServiceParameterRepeater)
532        {
533            if (!parentMetadata.hasMetadata(paramName))
534            {
535                return paramValues;
536            }
537                    
538            Map<String, String> attributes = new HashMap<>();
539            attributes.put("name", paramName);
540            attributes.put("type", MetadataType.COMPOSITE.name().toLowerCase());
541            
542            Map<String, Object> children = new HashMap<>();
543            
544            CompositeMetadata compositeMetadata = parentMetadata.getCompositeMetadata(paramName); 
545            String[] entryNames = compositeMetadata.getMetadataNames();
546            for (String entryName : entryNames)
547            {
548                Map<String, Object> entryValue = new HashMap<>();
549                
550                for (String childParamName : ((ServiceParameterRepeater) paramDef).getChildrenParameters().keySet())
551                {
552                    ServiceParameter childParamDef = ((ServiceParameterRepeater) paramDef).getChildrenParameters().get(childParamName);
553                    // Default value is ignored if parameter is a composite metadata
554                    Map<String, MapNode> childParamValues = _getParameterValue(childParamName, compositeMetadata.getCompositeMetadata(entryName), childParamDef, "");
555                    entryValue.putAll(childParamValues);
556                }
557                
558                Map<String, String> entryAttributes = new HashMap<>();
559                entryAttributes.put("name", entryName);
560                entryAttributes.put("type", MetadataType.COMPOSITE.name().toLowerCase());
561                
562                MapNode entryNode = new MapNode(entryValue, entryAttributes);
563                children.put(_convertTagName(entryName), entryNode);
564            }
565            
566            MapNode node = new MapNode(children, attributes);
567            paramValues.put(paramName, node);
568        }
569        else 
570        {
571            if (!parentMetadata.hasMetadata(paramName) && StringUtils.isEmpty(defaultValue))
572            {
573                return paramValues;
574            }
575            
576            Map<String, String> attributes = new HashMap<>();
577            attributes.put("name", paramName);
578            attributes.put("type", ParameterHelper.typeToString(((ServiceParameter) paramDef).getType()));
579            
580            if (((ServiceParameter) paramDef).isMultiple())
581            {
582                Map<String, Object> values = new HashMap<>();
583                values.put("value", Arrays.asList(parentMetadata.getStringArray(paramName, new String[] {defaultValue})));
584                
585                MapNode node = new MapNode(values, attributes);
586                paramValues.put(paramName, node);
587            }
588            else
589            {
590                String value = parentMetadata.getString(paramName, defaultValue);
591                if (StringUtils.isEmpty(value))
592                {
593                    value = defaultValue;
594                }
595                MapNode node = new MapNode(value, attributes);
596                paramValues.put(paramName, node);
597            }
598        }
599        
600        return paramValues;
601    }
602    
603    /**
604     * Returns the current site
605     * @return the current site
606     */
607    public static String site()
608    {
609        Request request = ContextHelper.getRequest(_context);
610        return (String) request.getAttribute("site");
611    }
612    
613    /**
614     * Returns the current site
615     * @param pageId The identifier ot the page
616     * @return the current site
617     */
618    public static String site(String pageId)
619    {
620        try
621        {
622            Page page = _getPage(pageId);
623            return page.getSiteName();
624        }
625        catch (UnknownAmetysObjectException e)
626        {
627            _logger.error("Can not get site on page '" + pageId + "'", e);
628            return "";
629        }
630    }
631
632    /**
633     * Return the current sitemap as a {@link Node}.
634     * Invisible pages will not be displayed
635     * @return the current sitemap.
636     */
637    public static Node sitemap()
638    {
639        return sitemap(false);
640    }
641    
642    /**
643     * Return the current sitemap as a {@link Node}.
644     * @param includeInvisiblePages Should return child invisible pages
645     * @return the current sitemap.
646     */
647    public static Node sitemap(boolean includeInvisiblePages)
648    {
649        Request request = ContextHelper.getRequest(_context);
650        Sitemap sitemap = (Sitemap) request.getAttribute(Sitemap.class.getName());
651        
652        if (sitemap == null)
653        {
654            // Try to get sitemap from content
655            Content content = (Content) request.getAttribute(Content.class.getName());
656            if (content instanceof WebContent)
657            {
658                sitemap = ((WebContent) content).getSite().getSitemap(content.getLanguage());
659            }
660        }
661        
662        if (sitemap == null)
663        {
664            return new EmptyElement("sitemap");
665        }
666        
667        Page page = (Page) request.getAttribute(Page.class.getName());
668        
669        return new SitemapElement(sitemap, page != null ? page.getPathInSitemap() : null, _rightManager, _renderingContextHandler, _currentUserProvider.getUser(), includeInvisiblePages);
670    }
671
672    /**
673     * Return the subsitemap of the given page as a {@link Node}.
674     * Invisible child pages will not be returned;
675     * @param pageId The root page
676     * @return The page as node.
677     */
678    public static Node sitemap(String pageId)
679    {
680        return sitemap(pageId, false);
681    }
682        
683    /**
684     * Return the subsitemap of the given page as a {@link Node}.
685     * @param pageId The root page
686     * @param includeInvisiblePages Should return child invisible pages
687     * @return The page as node.
688     */
689    public static Node sitemap(String pageId, boolean includeInvisiblePages)
690    {
691        Page rootPage = null;
692        try
693        {
694            rootPage = _ametysObjectResolver.resolveById(pageId);
695        }
696        catch (UnknownAmetysObjectException e)
697        {
698            return new EmptyElement("page");
699        }
700        
701        Request request = ContextHelper.getRequest(_context);
702        Page page = (Page) request.getAttribute(Page.class.getName());
703
704        return new PageElement(rootPage, _rightManager, _renderingContextHandler, page != null ? page.getPathInSitemap() : null,  _currentUserProvider.getUser(), includeInvisiblePages);
705    }
706    
707    /**
708     * Computes the breadcrumb of the current page.
709     * @return a NodeList containing all ancestor pages, rooted at the sitemap.
710     */
711    public static NodeList breadcrumb()
712    {
713        Request request = ContextHelper.getRequest(_context);
714        Page page = (Page) request.getAttribute(Page.class.getName());
715  
716        List<Element> result = new ArrayList<>();
717
718        AmetysObject parent = page.getParent();
719        while (parent instanceof Page)
720        {
721            Element node = new StringElement("page", (Map<String, String>) null, parent.getId());
722            result.add(node);
723            parent = parent.getParent();
724        }
725        
726        Collections.reverse(result);
727        return new AmetysNodeList(result);
728    }
729
730    /**
731     * Returns a DOM {@link Element} representing files and folder of the resources explorer, 
732     * under the {@link ResourceCollection} corresponding to the given id.
733     * @param collectionId the id of the root {@link ResourceCollection}.
734     * @return an Element containing files and folders.
735     */
736    public static Node resourcesById(String collectionId)
737    {
738        ResourceCollection collection = _ametysObjectResolver.resolveById(collectionId);
739        return new ResourceCollectionElement(collection);
740    }
741    
742    /**
743     * Returns a DOM {@link Element} representing files and folder of the resources explorer, 
744     * under the {@link ResourceCollection} corresponding to the given path. <br>
745     * This path is intended to be relative to the current site's resource explorer.
746     * @param path the path of the root {@link ResourceCollection}, relative to the current site's resource explorer.
747     * @return an Element containing files and folders or null if the specified resource does not exist.
748     */
749    public static Node resourcesByPath(String path)
750    {
751        Request request = ContextHelper.getRequest(_context);
752        String siteName = (String) request.getAttribute("site");
753        Site site = _siteManager.getSite(siteName);
754        
755        try
756        {
757            ResourceCollection collection = site.getRootResources().getChild(path);
758            return new ResourceCollectionElement(collection);
759        }
760        catch (UnknownAmetysObjectException ex)
761        {
762            return null;
763        }
764    }
765
766    /**
767     * Returns a DOM {@link Element} representing a single file of the resources explorer. <br>
768     * This path is intended to be relative to the current site's resource explorer.
769     * @param path the path of the {@link Resource}, relative to the current site's resource explorer.
770     * @return an Element containing a file or null if the specified resource does not exist.
771     */
772    public static Node resourceByPath(String path)
773    {
774        Request request = ContextHelper.getRequest(_context);
775        String siteName = (String) request.getAttribute("site");
776        Site site = _siteManager.getSite(siteName);
777
778        try
779        {
780            Resource resource = site.getRootResources().getChild(path);
781            return new ResourceElement(resource, null);
782        }
783        catch (UnknownAmetysObjectException ex)
784        {
785            return null;
786        }
787    }
788
789    /**
790     * Returns a DOM {@link Element} representing files and folder of a skin directory. <br>
791     * This path is intended to be relative to the current skin's 'resources' directory.
792     * @param path the path of the root {@link ResourceCollection}, relative to the current skin's 'resources' directory. Can be a file path to test its existance.
793     * @return an Element containing files and folders. node name is 'collection' or 'resource' with an attribute 'name'. Return null if the path does not exits.
794     * @throws IOException if an error occured while listing files.
795     */
796    public static Node skinResources(String path) throws IOException
797    {
798        FileSource source = (FileSource) _sourceResolver.resolveURI("skin://resources/" + path);
799        if (source.exists())
800        {
801            return new FileElement(source.getFile());
802        }
803        else
804        {
805            return null;
806        }
807    }
808    
809    //*************************
810    // Page methods
811    //*************************
812    
813    private static String _getMetadata(CompositeMetadata cm, String metadataName)
814    {
815        int i = metadataName.indexOf("/");
816        if (i == -1)
817        {
818            return cm.getString(metadataName);
819        }
820        else
821        {
822            return _getMetadata(cm.getCompositeMetadata(metadataName.substring(0, i)), metadataName.substring(i + 1));
823        }
824    }
825    
826    private static Page _getPage(String sitename, String lang, String path)
827    {
828        Site site = _siteManager.getSite(sitename);
829        Sitemap sitemap = site.getSitemap(lang);
830        return sitemap.getChild(path);
831    }
832    
833    private static Page _getPage(String id)
834    {
835        return _ametysObjectResolver.resolveById(id);
836    }
837    
838    /**
839     * Get the site name of a page.
840     * @param pageId The page id.
841     * @return The name or empty if the page does not exist.
842     */
843    public static String pageSiteName(String pageId)
844    {
845        try
846        {
847            Page page = _getPage(pageId);
848            return page.getSiteName();
849        }
850        catch (UnknownAmetysObjectException e)
851        {
852            _logger.error("Can not get site name on page with id '" + pageId + "'", e);
853            return "";
854        }
855    }
856    
857    /**
858     * Get the title of a page.
859     * @param sitename the site name.
860     * @param lang the sitemap name.
861     * @param path the page path.
862     * @return The name or empty if the meta or the page does not exist.
863     */
864    public static String pageTitle(String sitename, String lang, String path)
865    {
866        try
867        {
868            Page page = _getPage(sitename, lang, path);
869            return page.getTitle();
870        }
871        catch (UnknownAmetysObjectException e)
872        {
873            _logger.error("Can not get title on page '" + sitename + "/" + lang + "/" + path + "'", e);
874            return "";
875        }
876    }
877    
878    /**
879     * Get the title of a page.
880     * @param pageId The page id.
881     * @return The name or empty if the meta or the page does not exist.
882     */
883    public static String pageTitle(String pageId)
884    {
885        try
886        {
887            Page page = _getPage(pageId);
888            return page.getTitle();
889        }
890        catch (UnknownAmetysObjectException e)
891        {
892            _logger.error("Can not get title on page with id '" + pageId + "'", e);
893            return "";
894        }
895    }
896
897    /**
898     * Get the long title of a page
899     * @param sitename the site name
900     * @param lang the page's language
901     * @param path the page's path
902     * @return The name or empty if the meta or the page does not exist
903     */
904    public static String pageLongTitle(String sitename, String lang, String path)
905    {
906        try
907        {
908            Page page = _getPage(sitename, lang, path);
909            return page.getLongTitle();
910        }
911        catch (UnknownAmetysObjectException e)
912        {
913            _logger.error("Can not get long title on page '" + sitename + "/" + lang + "/" + path + "'", e);
914            return "";
915        }
916    }
917    /**
918     * Get the long title of a page
919     * @param pageId The page id
920     * @return The name or empty if the meta or the page does not exist
921     */
922    public static String pageLongTitle(String pageId)
923    {
924        try
925        {
926            Page page = _getPage(pageId);
927            return page.getLongTitle();
928        }
929        catch (UnknownAmetysObjectException e)
930        {
931            _logger.error("Can not get long title on page with id '" + pageId + "'", e);
932            return "";
933        }
934    }
935
936    /**
937     * Get the meta of a page
938     * @param sitename the site name
939     * @param lang the page's language
940     * @param path the page's path
941     * @param metadataName The meta name (/ for composite)
942     * @return The name or empty if the meta or the page does not exist
943     */
944    public static String pageMetadata(String sitename, String lang, String path, String metadataName)
945    {
946        try
947        {
948            Page page = _getPage(sitename, lang, path);
949            try
950            {
951                return _getMetadata(page.getMetadataHolder(), metadataName);
952            }
953            catch (UnknownMetadataException e)
954            {
955                _logger.error("Can not get meta '" + metadataName + "' on page with id '" + page.getId() + "'", e);
956                return "";
957            }
958        }
959        catch (UnknownAmetysObjectException e)
960        {
961            _logger.error("Can not get meta '" + metadataName + "' on page '" + sitename + "/" + lang + "/" + path + "'", e);
962            return "";
963        }
964    }
965    
966    /**
967     * Get the meta of a page
968     * @param pageId The page id
969     * @param metadataName The meta name (/ for composite)
970     * @return The name or empty if the meta or the page does not exist
971     */
972    public static String pageMetadata(String pageId, String metadataName)
973    {
974        try
975        {
976            Page page = _getPage(pageId);
977            try
978            {
979                return _getMetadata(page.getMetadataHolder(), metadataName);
980            }
981            catch (UnknownMetadataException e)
982            {
983                _logger.error("Can not get meta '" + metadataName + "' on page with id '" + page.getId() + "'", e);
984                return "";
985            }
986        }
987        catch (UnknownAmetysObjectException e)
988        {
989            _logger.error("Can not get meta '" + metadataName + "' on page with id '" + pageId + "'", e);
990            return "";
991        }
992    }
993
994    /**
995     * Returns true if the given page is visible into navigation elements
996     * @param pageId the page id.
997     * @return true if the page is visible
998     */
999    public static boolean pageIsVisible (String pageId)
1000    {
1001        try
1002        {
1003            Page page = _getPage(pageId);
1004            return page.isVisible();
1005        }
1006        catch (UnknownAmetysObjectException e)
1007        {
1008            _logger.error("Can not get visibility status on page with id '" + pageId + "'", e);
1009            return false;
1010        }
1011    }
1012    
1013    /**
1014     * Returns true if the given page is visible into navigation elements
1015     * @param sitename the site name
1016     * @param lang the page's language
1017     * @param path the page's path
1018     * @return true if the page is visible
1019     */
1020    public static boolean pageIsVisible (String sitename, String lang, String path)
1021    {
1022        try
1023        {
1024            Page page = _getPage(sitename, lang, path);
1025            return page.isVisible();
1026        }
1027        catch (UnknownAmetysObjectException e)
1028        {
1029            _logger.error("Can not get visibility status on page with id '" + sitename + "/" + lang + "/" + path + "'", e);
1030            return false;
1031        }
1032    }
1033    
1034    /**
1035     * Returns true if the given page has restricted access.
1036     * @param pageId the page id.
1037     * @return true if the page exists and has restricted access.
1038     */
1039    public static boolean pageHasRestrictedAccess(String pageId)
1040    {
1041        try
1042        {
1043            Page page = _getPage(pageId);
1044            return !_rightManager.hasAnonymousReadAccess(page);
1045        }
1046        catch (UnknownAmetysObjectException e)
1047        {
1048            _logger.error("Can not get page access info on page with id '" + pageId + "'", e);
1049            return false;
1050        }
1051    }
1052
1053    /**
1054     * Returns true if the current user has the specified right on the current page
1055     * @param rightId Right Id
1056     * @return true if the current user has the specified right on the current page
1057     */
1058    public static boolean hasRightOnPage(String rightId)
1059    {
1060        Request request = ContextHelper.getRequest(_context);
1061        Page page = (Page) request.getAttribute(Page.class.getName());
1062        return _hasRightOnPage(rightId, page);
1063    }
1064
1065    /**
1066     * Returns true if the current user has the specified right on the specified page
1067     * @param rightId Right Id
1068     * @param pageId Page Id
1069     * @return true if the current user has the specified right on the specified page
1070     */
1071    public static boolean hasRightOnPage(String rightId, String pageId)
1072    {
1073        Page page = _getPage(pageId);
1074        return _hasRightOnPage(rightId, page);
1075    }
1076
1077    /**
1078     * Returns true if the current user has the specified right on the specified page
1079     * @param rightId Right Id
1080     * @param page Page
1081     * @return true if the current user has the specified right on the specified page
1082     */
1083    private static boolean _hasRightOnPage(String rightId, Page page)
1084    {
1085        RightResult rightResult = _rightManager.currentUserHasRight(rightId, page);
1086        return rightResult == RightResult.RIGHT_ALLOW;
1087    }
1088
1089    /**
1090     * Returns true if the given page has restricted access.
1091     * @param sitename the site name
1092     * @param lang the page's language
1093     * @param path the page's path
1094     * @return true if the page exists and has restricted access.
1095     */
1096    public static boolean pageHasRestrictedAccess(String sitename, String lang, String path)
1097    {
1098        try
1099        {
1100            Page page = _getPage(sitename, lang, path);
1101            return !_rightManager.hasAnonymousReadAccess(page);
1102        }
1103        catch (UnknownAmetysObjectException e)
1104        {
1105            _logger.error("Can not get page access info on page with id '" + sitename + "/" + lang + "/" + path + "'", e);
1106            return false;
1107        }
1108    }
1109    
1110    /**
1111     * Returns the path of the current page, relative to the sitemap's root.
1112     * @return the path of the current Page, or empty if there's no current page.
1113     */
1114    public static String pagePath()
1115    {
1116        Request request = ContextHelper.getRequest(_context);
1117        Page page = (Page) request.getAttribute(Page.class.getName());
1118        
1119        return page == null ? "" : page.getPathInSitemap();
1120    }
1121    
1122    /**
1123     * Returns the path in sitemap of a page
1124     * @param pageId The id of page
1125     * @return the path of the Page, or empty if not exists
1126     */
1127    public static String pagePath(String pageId)
1128    {
1129        try
1130        {
1131            Page page = _getPage(pageId);
1132            return page.getPathInSitemap();
1133        }
1134        catch (UnknownAmetysObjectException e)
1135        {
1136            _logger.error("Can not get title on page with id '" + pageId + "'", e);
1137            return "";
1138        }
1139    }
1140    
1141    /**
1142     * Returns the id of the current page.
1143     * @return the id of the current Page, or empty if there's no current page.
1144     */
1145    public static String pageId()
1146    {
1147        Request request = ContextHelper.getRequest(_context);
1148        Page page = (Page) request.getAttribute(Page.class.getName());
1149        
1150        return page == null ? "" : page.getId();
1151    }
1152    
1153    /**
1154     * Returns the id of the current zone item id.
1155     * @return the id of the current zone item id, or empty if there's no current zone item.
1156     */
1157    public static String zoneItemId()
1158    {
1159        Request request = ContextHelper.getRequest(_context);
1160        ZoneItem zoneItem = (ZoneItem) request.getAttribute(ZoneItem.class.getName());
1161        
1162        return zoneItem == null ? "" : StringUtils.defaultString(zoneItem.getId());
1163    }
1164    
1165    /**
1166     * Determines if the current zone item or (if there is no current zone item) the current page is cacheable
1167     * This method is only valid for a page.
1168     * @return true if the current zone item or page is cacheable.
1169     */
1170    public static boolean isCacheable()
1171    {
1172        Request request = ContextHelper.getRequest(_context);
1173        if (request.getAttribute("IsZoneItemCacheable") != null)
1174        {
1175            return (Boolean) request.getAttribute("IsZoneItemCacheable");
1176        }
1177        
1178        // The method was called from the skin, out of a zone item
1179        Response response = ContextHelper.getResponse(_context);
1180        if (response.containsHeader("X-Ametys-Cacheable"))
1181        {
1182            return true;
1183        }
1184        return false;
1185    }
1186    
1187    /**
1188     * Determines if we are in an edition mode
1189     * @return true if we are in edition mode
1190     */
1191    public static boolean isEditionMode()
1192    {
1193        RenderingContext renderingContext = _renderingContextHandler.getRenderingContext();
1194        Request request = ContextHelper.getRequest(_context);
1195        if (renderingContext == RenderingContext.FRONT && request.getParameter("_edition") != null && "true".equals(request.getParameter("_edition")))
1196        {
1197            return true;
1198        }
1199        return false;
1200    }
1201
1202    /**
1203     * Returns the id of pages referencing the content and for which the Front-office user can access
1204     * @param contentId The content's id
1205     * @return The pages' id
1206     */
1207    public static NodeList accessibleReferencedPages (String contentId)
1208    {
1209        RenderingContext renderingContext = _renderingContextHandler.getRenderingContext();
1210        boolean inBackOffice = renderingContext == RenderingContext.BACK || renderingContext == RenderingContext.PREVIEW;
1211        
1212        List<StringElement> pages = new ArrayList<>(); 
1213        
1214        Content content = _ametysObjectResolver.resolveById(contentId);
1215        if (content instanceof WebContent)
1216        {
1217            Collection<ZoneItem> zoneItems = ((WebContent) content).getReferencingZoneItems();
1218            
1219            for (ZoneItem zoneItem : zoneItems)
1220            {
1221                String metadataSetName = zoneItem.getMetadataSetName();
1222                Page page = zoneItem.getZone().getPage();
1223                
1224                if (inBackOffice || _rightManager.hasReadAccess(_currentUserProvider.getUser(), page))
1225                {
1226                    Map<String, String> attrs = new HashMap<>();
1227                    attrs.put("id", page.getId());
1228                    attrs.put("metadataSetName", metadataSetName);
1229                    pages.add(new StringElement("page", attrs));
1230                }
1231            }
1232        }
1233        
1234        return new AmetysNodeList(pages);
1235    }
1236    
1237    /**
1238     * Returns the id of pages referencing the content
1239     * @param contentId The content's id
1240     * @return The pages' id
1241     */
1242    public static NodeList referencedPages (String contentId)
1243    {
1244        List<StringElement> pages = new ArrayList<>(); 
1245        
1246        Content content = _ametysObjectResolver.resolveById(contentId);
1247        if (content instanceof WebContent)
1248        {
1249            Collection<ZoneItem> zoneItems = ((WebContent) content).getReferencingZoneItems();
1250            
1251            for (ZoneItem zoneItem : zoneItems)
1252            {
1253                String metadataSetName = zoneItem.getMetadataSetName();
1254                Page page = zoneItem.getZone().getPage();
1255                
1256                Map<String, String> attrs = new HashMap<>();
1257                attrs.put("id", page.getId());
1258                attrs.put("metadataSetName", metadataSetName);
1259                pages.add(new StringElement("page", attrs));
1260            }
1261        }
1262        
1263        return new AmetysNodeList(pages);
1264    }
1265    
1266    /**
1267     * Returns the ids of the pages
1268     * @param sitename The site id
1269     * @param lang The language code
1270     * @param tag The tag id
1271     * @return Array of pages ids
1272     */
1273    public static NodeList findPagesIdsByTag(String sitename, String lang, String tag)
1274    {
1275        String xpath = PageQueryHelper.getPageXPathQuery(sitename, lang, null, new TagExpression(Operator.EQ, tag), null);
1276        AmetysObjectIterable<Page> pages = _ametysObjectResolver.query(xpath);
1277        Iterator<Page> it = pages.iterator();
1278
1279        List<StringElement> list = new ArrayList<>(); 
1280        while (it.hasNext())
1281        {
1282            list.add(new StringElement("page", "id", it.next().getId()));
1283        }
1284        return new AmetysNodeList(list);
1285    }
1286    
1287    /**
1288     * Returns the ids of the pages
1289     * @param tag The tag id
1290     * @return Array of pages ids
1291     */
1292    public static NodeList findPagesIdsByTag(String tag)
1293    {
1294        Request request = ContextHelper.getRequest(_context);
1295        String siteName = (String) request.getAttribute("site");
1296        
1297        String lang = (String) request.getAttribute("sitemapLanguage");
1298        if (lang == null)
1299        {
1300            // Try to get current language from content
1301            Content content = (Content) request.getAttribute(Content.class.getName());
1302            if (content != null)
1303            {
1304                lang = content.getLanguage();
1305            }
1306        }
1307        
1308        return findPagesIdsByTag(siteName, lang, tag);
1309    }
1310}