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