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;
030import java.util.Set;
031
032import org.apache.avalon.framework.service.ServiceException;
033import org.apache.avalon.framework.service.ServiceManager;
034import org.apache.cocoon.components.ContextHelper;
035import org.apache.cocoon.environment.Request;
036import org.apache.cocoon.environment.Response;
037import org.apache.cocoon.xml.dom.DOMBuilder;
038import org.apache.commons.lang.StringUtils;
039import org.apache.excalibur.source.SourceResolver;
040import org.apache.excalibur.source.impl.FileSource;
041import org.w3c.dom.Element;
042import org.w3c.dom.Node;
043import org.w3c.dom.NodeList;
044import org.xml.sax.SAXException;
045
046import org.ametys.cms.repository.Content;
047import org.ametys.cms.repository.ContentQueryHelper;
048import org.ametys.cms.repository.LanguageExpression;
049import org.ametys.cms.transformation.ImageResolverHelper;
050import org.ametys.core.right.RightManager.RightResult;
051import org.ametys.core.user.User;
052import org.ametys.core.user.UserIdentity;
053import org.ametys.core.user.population.PopulationContextHelper;
054import org.ametys.core.util.I18nUtils;
055import org.ametys.core.util.dom.AmetysNodeList;
056import org.ametys.core.util.dom.EmptyElement;
057import org.ametys.core.util.dom.FileElement;
058import org.ametys.core.util.dom.MapElement;
059import org.ametys.core.util.dom.MapElement.MapNode;
060import org.ametys.core.util.dom.StringElement;
061import org.ametys.plugins.explorer.resources.Resource;
062import org.ametys.plugins.explorer.resources.ResourceCollection;
063import org.ametys.plugins.explorer.resources.dom.ResourceCollectionElement;
064import org.ametys.plugins.explorer.resources.dom.ResourceElement;
065import org.ametys.plugins.repository.AmetysObject;
066import org.ametys.plugins.repository.AmetysObjectIterable;
067import org.ametys.plugins.repository.AmetysObjectResolver;
068import org.ametys.plugins.repository.UnknownAmetysObjectException;
069import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
070import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeater;
071import org.ametys.plugins.repository.data.holder.group.impl.ModelAwareRepeaterEntry;
072import org.ametys.plugins.repository.data.holder.impl.DataHolderHelper;
073import org.ametys.plugins.repository.model.RepeaterDefinition;
074import org.ametys.plugins.repository.query.expression.AndExpression;
075import org.ametys.plugins.repository.query.expression.Expression;
076import org.ametys.plugins.repository.query.expression.Expression.Operator;
077import org.ametys.plugins.repository.query.expression.MetadataExpression;
078import org.ametys.plugins.repository.query.expression.NotExpression;
079import org.ametys.plugins.repository.query.expression.StringExpression;
080import org.ametys.runtime.model.ElementDefinition;
081import org.ametys.runtime.model.ModelItem;
082import org.ametys.runtime.model.type.ElementType;
083import org.ametys.runtime.model.type.ModelItemType;
084import org.ametys.web.URIPrefixHandler;
085import org.ametys.web.WebConstants;
086import org.ametys.web.renderingcontext.RenderingContext;
087import org.ametys.web.renderingcontext.RenderingContextHandler;
088import org.ametys.web.repository.content.WebContent;
089import org.ametys.web.repository.dom.PageElement;
090import org.ametys.web.repository.dom.SitemapElement;
091import org.ametys.web.repository.page.Page;
092import org.ametys.web.repository.page.PageQueryHelper;
093import org.ametys.web.repository.page.Zone;
094import org.ametys.web.repository.page.ZoneItem;
095import org.ametys.web.repository.site.Site;
096import org.ametys.web.repository.site.SiteManager;
097import org.ametys.web.repository.sitemap.Sitemap;
098import org.ametys.web.service.Service;
099import org.ametys.web.service.ServiceExtensionPoint;
100import org.ametys.web.tags.TagExpression;
101import org.ametys.web.url.UrlPreview;
102import org.ametys.web.url.UrlPreviewComponent;
103
104/**
105 * Helper component to be used from XSL stylesheets.
106 */
107public class AmetysXSLTHelper extends org.ametys.cms.transformation.xslt.AmetysXSLTHelper
108{
109    static final int _PAGINATION_CURRENT = -1;
110    static final int _PAGINATION_SEPARATOR = -2;
111    static final int _PAGINATION_SPACE = -3;
112    
113    private static final String __NAME_ATTRIBUTE = "name";
114    private static final String __TYPE_ATTRIBUTE = "type";
115    private static final String __REPEATER_ENTRY_TYPE = RepeaterDefinition.TYPE + "_entry";
116    
117    private static SiteManager _siteManager;
118    private static RenderingContextHandler _renderingContextHandler;
119    private static URIPrefixHandler _prefixHandler;
120    private static SourceResolver _sourceResolver;
121    private static ServiceExtensionPoint _serviceEP;
122    private static PopulationContextHelper _populationContextHelper;
123    private static UrlPreviewComponent _urlPreview;
124    
125    @Override
126    public void service(ServiceManager manager) throws ServiceException
127    {
128        _siteManager = (SiteManager) manager.lookup(SiteManager.ROLE);
129        _renderingContextHandler = (RenderingContextHandler) manager.lookup(RenderingContextHandler.ROLE);
130        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
131        _i18nUtils = (I18nUtils) manager.lookup(I18nUtils.ROLE);
132        _prefixHandler = (URIPrefixHandler) manager.lookup(URIPrefixHandler.ROLE);
133        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
134        _serviceEP = (ServiceExtensionPoint) manager.lookup(ServiceExtensionPoint.ROLE);
135        _populationContextHelper = (PopulationContextHelper) manager.lookup(PopulationContextHelper.ROLE);
136        _urlPreview = (UrlPreviewComponent) manager.lookup(UrlPreviewComponent.ROLE);
137    }
138    
139    /**
140     * Returns the current URI prefix, depending on the rendering context.
141     * @return the current URI prefix.
142     */
143    public static String uriPrefix()
144    {
145        return _prefixHandler.getUriPrefix();
146    }
147    
148    /**
149     * Returns the URI prefix corresponding to the current site, depending on the rendering context.
150     * @return the URI prefix corresponding to the current site.
151     */
152    public static String siteUriPrefix()
153    {
154        Request request = ContextHelper.getRequest(_context);
155        String siteName = (String) request.getAttribute("site");
156        return _prefixHandler.getUriPrefix(siteName);
157    }
158    
159    /**
160     * Returns the absolute URI prefix, depending on the rendering context.
161     * @return the absolute URI prefix.
162     */
163    public static String absoluteUriPrefix()
164    {
165        return _prefixHandler.getAbsoluteUriPrefix();
166    }
167    
168    /**
169     * Returns the absolute URI prefix corresponding to the current site, depending on the rendering context.
170     * @return the absolute URI prefix corresponding to the current site.
171     */
172    public static String absoluteSiteUriPrefix()
173    {
174        Request request = ContextHelper.getRequest(_context);
175        String siteName = (String) request.getAttribute("site");
176        return _prefixHandler.getAbsoluteUriPrefix(siteName);
177    }
178    
179    /**
180     * Returns the absolute URI prefix corresponding to the given site, depending on the rendering context.
181     * @param siteName The site name. Can be null to get the current site.
182     * @return the absolute URI prefix corresponding to the current site.
183     */
184    public static String absoluteSiteUriPrefix(String siteName)
185    {
186        if (StringUtils.isEmpty(siteName))
187        {
188            return absoluteSiteUriPrefix();
189        }
190        return _prefixHandler.getAbsoluteUriPrefix(siteName);
191    }
192    
193    /**
194     * Returns the current skin name.
195     * @return the current skin name.
196     */
197    public static String skin()
198    {
199        Request request = ContextHelper.getRequest(_context);
200        return (String) request.getAttribute("skin");
201    }
202    
203    /**
204     * Returns the current template name.
205     * @return the current template name.
206     */
207    public static String template()
208    {
209        Request request = ContextHelper.getRequest(_context);
210        return (String) request.getAttribute("template");
211    }
212    
213    /**
214     * Returns the current sitemap name.
215     * @return the current sitemap name.
216     */
217    public static String lang()
218    {
219        Request request = ContextHelper.getRequest(_context);
220        return (String) request.getAttribute("sitemapLanguage");
221    }
222    
223    /**
224     * Returns the current sitemap name.
225     * @param pageId The page identifier to get sitemap on
226     * @return the current sitemap name.
227     */
228    public static String lang(String pageId)
229    {
230        try
231        {
232            Page page = _getPage(pageId);
233            return page.getSitemapName();
234        }
235        catch (UnknownAmetysObjectException e)
236        {
237            _logger.error("Can not get sitemap lang on page '" + pageId + "'", e);
238            return "";
239        }
240    }
241    
242    /**
243     * Computes the URI for the given resource in the current site's skin.<br>
244     * If the URI is requested by the front-office, it will be absolutized.
245     * @param path the resource path.
246     * @return the URI for the given resource.
247     */
248    public static String skinURL(String path)
249    {
250        Request request = ContextHelper.getRequest(_context);
251        String siteName = (String) request.getAttribute("site");
252        Site site = _siteManager.getSite(siteName);
253        String skin = (String) request.getAttribute("skin");
254        
255        String resourcePath = "/skins/" + skin + "/resources/" + path;
256        
257        return _getResourceURL(request, site, resourcePath);
258    }
259    
260    /**
261     * Computes the URI for the given image with a given heigth and width in the current site's skin.<br>
262     * If the URI is requested by the front-office, it will be absolutized.
263     * @param path the resource path
264     * @param height the height for the resource to get
265     * @param width the width for the resource to get
266     * @return the URI of the given resource
267     */
268    public static String skinImageURL(String path, int height, int width)
269    {
270        String skinPath = skinURL(path);
271        return StringUtils.substringBeforeLast(skinPath, ".") + "_" + height + "x" + width + "." + StringUtils.substringAfterLast(skinPath, "."); 
272    }
273    
274    /**
275     * Computes the base 64 representation of the image at the specified path. <br>
276     * @param path the path of the image
277     * @return the base 64-encoded image
278     * @throws IOException if an error occurs while trying to get the file
279     */
280    public static String skinImageBase64 (String path) throws IOException
281    {
282        FileSource source = (FileSource) _sourceResolver.resolveURI("skin://resources/" + path);
283        return _getResourceBase64(source); 
284    }
285    
286    /**
287     * Computes the URI for the given image with a given heigth and width in the current site's skin.<br>
288     * If the URI is requested by the front-office, it will be absolutized.
289     * @param path the resource path
290     * @param maxHeight the maximum height for the resource to get
291     * @param maxWidth the maximum width for the resource to get
292     * @return the URI of the given resource
293     */
294    public static String skinBoundedImageURL(String path, int maxHeight, int maxWidth)
295    {
296        String skinPath = skinURL(path);
297        return StringUtils.substringBeforeLast(skinPath, ".") + "_max" + maxHeight + "x" + maxWidth + "." + StringUtils.substringAfterLast(skinPath, "."); 
298    }
299    
300    /**
301     * Computes the URI for the given resource in the current template.<br>
302     * If the URI is requested by the front-office, it will be absolutized.
303     * @param path the resource path.
304     * @return the URI for the given resource.
305     */
306    public static String templateURL(String path)
307    {
308        Request request = ContextHelper.getRequest(_context);
309        String siteName = (String) request.getAttribute("site");
310        Site site = _siteManager.getSite(siteName);
311        String skin = (String) request.getAttribute("skin");
312        String template = (String) request.getAttribute("template");
313        
314        String resourcePath = "/skins/" + skin + "/templates/" + template + "/resources/" + path;
315        
316        return _getResourceURL(request, site, resourcePath);
317    }
318    
319    /**
320     * Computes the URI for the given resource in the given plugin.<br>
321     * If the URI is requested by the front-office, it will be absolutized.
322     * @param plugin the plugin name.
323     * @param path the resource path.
324     * @return the URI for the given resource.
325     */
326    public static String pluginResourceURL(String plugin, String path)
327    {
328        Request request = ContextHelper.getRequest(_context);
329        String siteName = (String) request.getAttribute("site");
330        Site site = _siteManager.getSite(siteName);
331        
332        String resourcePath = "/plugins/" + plugin + "/resources/" + path;
333        
334        return _getResourceURL(request, site, resourcePath);
335    }
336    
337    /**
338     * Computes the base 64 representation of the image at the specified path in the given plugin.<br>
339     * @param plugin the plugin's name.
340     * @param path the resource path.
341     * @return the base 64 encoding for the given resource.
342     * @throws IOException if an error occurs when trying to get the file
343     * @throws MalformedURLException if the url is invalid
344     */
345    public static String pluginImageBase64(String plugin, String path) throws MalformedURLException, IOException
346    {
347        FileSource source = (FileSource) _sourceResolver.resolveURI("plugin:" + plugin + "://resources/" + path);
348        return _getResourceBase64(source); 
349    }
350
351    
352    private static String _getResourceURL(Request request, Site site, String resourcePath)
353    {
354        String prefix;
355        switch (_renderingContextHandler.getRenderingContext())
356        {
357            case FRONT:
358                String[] aliases = site.getUrlAliases();
359                int position = Math.abs(resourcePath.hashCode()) % aliases.length;
360                
361                boolean absolute = request.getAttribute("forceAbsoluteUrl") != null ? (Boolean) request.getAttribute("forceAbsoluteUrl") : false;
362                prefix = position == 0 && !absolute ? siteUriPrefix() : aliases[position];  
363                return prefix + resourcePath;
364                
365            default:
366                prefix = StringUtils.trimToEmpty((String) request.getAttribute(WebConstants.PATH_PREFIX));
367                return request.getContextPath() + prefix + resourcePath;
368        }
369    }
370    
371    /**
372     * Get the base 64 encoding for the given source
373     * @param source the source 
374     * @return the base 64 encoding of the source
375     */
376    private static String _getResourceBase64(FileSource source)
377    {
378        if (source.exists())
379        {
380            
381            try (InputStream dataIs = source.getInputStream())
382            {
383                return ImageResolverHelper.resolveImageAsBase64(dataIs, source.getMimeType(), 0, 0, 0, 0);
384            }
385            catch (Exception e)
386            {
387                throw new IllegalStateException(e);
388            }
389        }
390
391        return "";
392    }
393    
394    /**
395     * Returns the current {@link RenderingContext}.
396     * @return the current {@link RenderingContext}.
397     */
398    public static String renderingContext()
399    {
400        return _renderingContextHandler.getRenderingContext().toString();
401    }
402    
403    /**
404     * Return the name of the zone beeing handled
405     * @param defaultValue If no page is handled currently, this value is returned (can be null, empty...)
406     * @return the name or the default value (so can be null or empty)
407     */
408    public static String zone(String defaultValue)
409    {
410        Request request = ContextHelper.getRequest(_context);
411        
412        return StringUtils.defaultIfEmpty((String) request.getAttribute(Zone.class.getName()), defaultValue);
413    }
414
415    /**
416     * Return the value of a site parameter as a String.
417     * @param parameter the parameter ID.
418     * @return the parameter value as a String.
419     */
420    public static String siteParameter(String parameter)
421    {
422        Request request = ContextHelper.getRequest(_context);
423        
424        String siteName = (String) request.getAttribute("site");
425        if (StringUtils.isBlank(siteName))
426        {
427            // In BO xsl
428            siteName = (String) request.getAttribute("siteName");
429        }
430        
431        return siteParameter(siteName, parameter);
432    }
433    
434    /**
435     * Return the value of a site parameter as a String.
436     * @param siteName the site name
437     * @param parameter the parameter ID.
438     * @return the parameter value as a String.
439     */
440    public static String siteParameter(String siteName, String parameter)
441    {
442        try
443        {
444            Site site = _siteManager.getSite(siteName);
445            Object value = site.getValue(parameter);
446            if (value != null)
447            {
448                return value.toString();
449            }
450            else
451            {
452                return null;
453            }
454        }
455        catch (Exception e)
456        {
457            String message = "Error retrieving the value of the site parameter " + parameter;
458            _logger.error(message, e);
459            throw new RuntimeException(message, e);
460        }
461    }
462    
463    /**
464     * Get the service parameters as a {@link Node}.
465     * @return the service parameters as a {@link Node}.
466     */
467    public static Node serviceParameters()
468    {
469        Request request = ContextHelper.getRequest(_context);
470        ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM);
471        return _serviceParameters(zoneItem);
472    }
473    
474    static Node _serviceParameters(ZoneItem zoneItem)
475    {
476        Map<String, MapNode> values = new HashMap<>();
477        
478        ModelAwareDataHolder dataHolder = zoneItem.getServiceParameters();
479        String serviceId = zoneItem.getServiceId();
480        Service service = _serviceEP.getExtension(serviceId);
481        
482        for (String parameterName : dataHolder.getDataNames())
483        {
484            if (service.hasModelItem(parameterName) && dataHolder.hasValue(parameterName))
485            {
486                ModelItem modelItem = service.getModelItem(parameterName);
487                values.putAll(_getParameterValue(parameterName, modelItem, dataHolder, null));
488            }
489        }
490        
491        return new MapElement("serviceParameters", values);
492    }
493    
494    /**
495     * Returns the value of the given parameter for the current service, or the empty string if the parameter does not exist.
496     * @param parameterPath the parameter path.
497     * @return the value of the given parameter for the current service.
498     */
499    public static Node serviceParameter(String parameterPath)
500    {
501        return serviceParameter(parameterPath, "");
502    }
503    
504    /**
505     * Returns the value of the given parameter for the current service, or the provided default value if the parameter does not exist.
506     * @param parameterPath the parameter path.
507     * @param defaultValue the default value. Note that default value is ignored if the parameter is a composite parameter.
508     * @return the value of the given parameter for the current service.
509     */
510    public static Node serviceParameter(String parameterPath, Object defaultValue)
511    {
512        Request request = ContextHelper.getRequest(_context);
513        ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM);
514        return _serviceParameter(zoneItem, parameterPath, defaultValue);
515    }
516    
517    static Node _serviceParameter(ZoneItem zoneItem, String parameterPath, Object defaultValue)
518    {
519        ModelAwareDataHolder dataHolder = zoneItem.getServiceParameters();
520        
521        String serviceId = zoneItem.getServiceId();
522        String definitionPath = DataHolderHelper.getDefinitionPathFromDataPath(parameterPath);
523        
524        Service service = _serviceEP.getExtension(serviceId);
525        ModelItem paramDef;
526        if (service.hasModelItem(definitionPath))
527        {
528            paramDef = service.getModelItem(definitionPath);
529        }
530        else
531        {
532            // The parameter is unknown
533            if (defaultValue == null || (defaultValue instanceof String && StringUtils.isEmpty((String) defaultValue)))
534            {
535                return null;
536            }
537            else
538            {
539                return new StringElement(parameterPath, Collections.EMPTY_MAP, defaultValue.toString());
540            }
541        }
542        
543        Map<String, MapNode> value = _getParameterValue(parameterPath, paramDef, dataHolder, defaultValue);
544        
545        if (!value.containsKey(parameterPath))
546        {
547            return null;
548        }
549        else if (paramDef instanceof RepeaterDefinition || (paramDef instanceof ElementDefinition && ((ElementDefinition) paramDef).isMultiple()))
550        {
551            MapNode node = value.get(parameterPath);
552            @SuppressWarnings("unchecked")
553            Map<String, ? extends Object> values = (Map<String, ? extends Object>) node.getValue();
554            return new MapElement(parameterPath, node.getAttributes(), values);
555        }
556        else 
557        {
558            return new StringElement(parameterPath, value.get(parameterPath).getAttributes(), (String) value.get(parameterPath).getValue());
559        }
560    }
561    
562    private static String _convertTagName(String name)
563    {
564        char c = name.charAt(0);
565        if (c >= '0' && c <= '9')
566        {
567            String hex = Integer.toHexString(c);
568            return "_x" + StringUtils.leftPad(hex, 4, '0') + "_" + name.substring(1);
569        }
570        else
571        {
572            return name;
573        }
574    }
575    
576    @SuppressWarnings("unchecked")
577    private static Map<String, MapNode> _getParameterValue(String parameterPath, ModelItem modelItem, ModelAwareDataHolder dataHolder, Object defaultValue)
578    {
579        String[] pathSegments = StringUtils.split(parameterPath, ModelItem.ITEM_PATH_SEPARATOR);
580        String parameterName = pathSegments[pathSegments.length - 1];
581        Map<String, MapNode> paramValues = new HashMap<>();
582        
583        if (modelItem instanceof RepeaterDefinition)
584        {
585            if (!dataHolder.hasValue(parameterPath))
586            {
587                return paramValues;
588            }
589                    
590            Map<String, String> attributes = new HashMap<>();
591            attributes.put(__NAME_ATTRIBUTE, parameterName);
592            attributes.put(__TYPE_ATTRIBUTE, RepeaterDefinition.TYPE);
593            
594            Map<String, Object> children = new HashMap<>();
595            
596            ModelAwareRepeater repeater = dataHolder.getRepeater(parameterPath);
597            for (ModelAwareRepeaterEntry entry : repeater.getEntries())
598            {
599                Map<String, Object> entryValue = new HashMap<>();
600                
601                for (ModelItem childModelItem : ((RepeaterDefinition) modelItem).getChildren())
602                {
603                    // Default value is ignored if parameter is a repeater
604                    Map<String, MapNode> childParamValues = _getParameterValue(childModelItem.getName(), childModelItem, entry, null);
605                    entryValue.putAll(childParamValues);
606                }
607                
608                Map<String, String> entryAttributes = new HashMap<>();
609                entryAttributes.put(__NAME_ATTRIBUTE, String.valueOf(entry.getPosition()));
610                entryAttributes.put(__TYPE_ATTRIBUTE, __REPEATER_ENTRY_TYPE);
611                
612                MapNode entryNode = new MapNode(entryValue, entryAttributes);
613                children.put(_convertTagName(String.valueOf(entry.getPosition())), entryNode);
614            }
615            
616            MapNode node = new MapNode(children, attributes);
617            paramValues.put(parameterPath, node);
618        }
619        else 
620        {
621            if (!dataHolder.hasValue(parameterPath) && (defaultValue == null || (defaultValue instanceof String && StringUtils.isEmpty((String) defaultValue))))
622            {
623                return paramValues;
624            }
625            
626            Map<String, String> attributes = new HashMap<>();
627            attributes.put(__NAME_ATTRIBUTE, parameterName);
628            ElementDefinition elementDefinition = (ElementDefinition) modelItem;
629            ElementType elementType = elementDefinition.getType();
630            attributes.put(__TYPE_ATTRIBUTE, elementType.getId());
631            
632            if (elementDefinition.isMultiple())
633            {
634                Map<String, List<String>> values = new HashMap<>();
635                List<String> value = new ArrayList<>();
636                Object[] valueArray = (Object[]) dataHolder.getValue(parameterPath, false, defaultValue);
637                for (Object arrayItem : valueArray)
638                {
639                    value.add(elementType.toString(arrayItem));
640                }
641                values.put("value", value);
642                
643                MapNode node = new MapNode(values, attributes);
644                paramValues.put(parameterPath, node);
645            }
646            else
647            {
648                
649                String value = elementType.toString(dataHolder.getValue(parameterPath, false, defaultValue));
650                MapNode node = new MapNode(value, attributes);
651                paramValues.put(parameterPath, node);
652            }
653        }
654        
655        return paramValues;
656    }
657    
658    /**
659     * Returns the current site
660     * @return the current site
661     */
662    public static String site()
663    {
664        Request request = ContextHelper.getRequest(_context);
665        return (String) request.getAttribute("site");
666    }
667    
668    /**
669     * Returns the current site
670     * @param pageId The identifier ot the page
671     * @return the current site
672     */
673    public static String site(String pageId)
674    {
675        try
676        {
677            Page page = _getPage(pageId);
678            return page.getSiteName();
679        }
680        catch (UnknownAmetysObjectException e)
681        {
682            _logger.error("Can not get site on page '" + pageId + "'", e);
683            return "";
684        }
685    }
686
687    /**
688     * Return the current sitemap as a {@link Node}.
689     * Invisible pages will not be displayed
690     * @return the current sitemap.
691     */
692    public static Node sitemap()
693    {
694        return sitemap(false);
695    }
696    
697    /**
698     * Return the current sitemap as a {@link Node}.
699     * @param includeInvisiblePages Should return child invisible pages
700     * @return the current sitemap.
701     */
702    public static Node sitemap(boolean includeInvisiblePages)
703    {
704        Request request = ContextHelper.getRequest(_context);
705        Sitemap sitemap = (Sitemap) request.getAttribute(Sitemap.class.getName());
706        
707        if (sitemap == null)
708        {
709            // Try to get sitemap from content
710            Content content = (Content) request.getAttribute(Content.class.getName());
711            if (content instanceof WebContent)
712            {
713                sitemap = ((WebContent) content).getSite().getSitemap(content.getLanguage());
714            }
715        }
716        
717        if (sitemap == null)
718        {
719            return new EmptyElement("sitemap");
720        }
721        
722        Page page = (Page) request.getAttribute(Page.class.getName());
723        
724        return new SitemapElement(sitemap, page != null ? page.getPathInSitemap() : null, _rightManager, _renderingContextHandler, _currentUserProvider.getUser(), includeInvisiblePages);
725    }
726
727    /**
728     * Return the subsitemap of the given page as a {@link Node}.
729     * Invisible child pages will not be returned;
730     * @param pageId The root page
731     * @return The page as node.
732     */
733    public static Node sitemap(String pageId)
734    {
735        return sitemap(pageId, false);
736    }
737        
738    /**
739     * Return the subsitemap of the given page as a {@link Node}.
740     * @param pageId The root page
741     * @param includeInvisiblePages Should return child invisible pages
742     * @return The page as node.
743     */
744    public static Node sitemap(String pageId, boolean includeInvisiblePages)
745    {
746        Page rootPage = null;
747        try
748        {
749            rootPage = _ametysObjectResolver.resolveById(pageId);
750        }
751        catch (UnknownAmetysObjectException e)
752        {
753            return new EmptyElement("page");
754        }
755        
756        Request request = ContextHelper.getRequest(_context);
757        Page page = (Page) request.getAttribute(Page.class.getName());
758
759        return new PageElement(rootPage, _rightManager, _renderingContextHandler, page != null ? page.getPathInSitemap() : null,  _currentUserProvider.getUser(), includeInvisiblePages);
760    }
761    
762    /**
763     * Computes the breadcrumb of the current page.
764     * @return a NodeList containing all ancestor pages, rooted at the sitemap.
765     */
766    public static NodeList breadcrumb()
767    {
768        Request request = ContextHelper.getRequest(_context);
769        Page page = (Page) request.getAttribute(Page.class.getName());
770  
771        List<Element> result = new ArrayList<>();
772
773        AmetysObject parent = page.getParent();
774        while (parent instanceof Page)
775        {
776            Element node = new StringElement("page", (Map<String, String>) null, parent.getId());
777            result.add(node);
778            parent = parent.getParent();
779        }
780        
781        Collections.reverse(result);
782        return new AmetysNodeList(result);
783    }
784
785    /**
786     * Returns a DOM {@link Element} representing files and folder of the resources explorer, 
787     * under the {@link ResourceCollection} corresponding to the given id.
788     * @param collectionId the id of the root {@link ResourceCollection}.
789     * @return an Element containing files and folders.
790     */
791    public static Node resourcesById(String collectionId)
792    {
793        ResourceCollection collection = _ametysObjectResolver.resolveById(collectionId);
794        return new ResourceCollectionElement(collection);
795    }
796    
797    /**
798     * Returns a DOM {@link Element} representing files and folder of the resources explorer, 
799     * under the {@link ResourceCollection} corresponding to the given path. <br>
800     * This path is intended to be relative to the current site's resource explorer.
801     * @param path the path of the root {@link ResourceCollection}, relative to the current site's resource explorer.
802     * @return an Element containing files and folders or null if the specified resource does not exist.
803     */
804    public static Node resourcesByPath(String path)
805    {
806        Request request = ContextHelper.getRequest(_context);
807        String siteName = (String) request.getAttribute("site");
808        Site site = _siteManager.getSite(siteName);
809        
810        try
811        {
812            ResourceCollection collection = site.getRootResources().getChild(path);
813            return new ResourceCollectionElement(collection);
814        }
815        catch (UnknownAmetysObjectException ex)
816        {
817            return null;
818        }
819    }
820
821    /**
822     * Returns a DOM {@link Element} representing a single file of the resources explorer. <br>
823     * This path is intended to be relative to the current site's resource explorer.
824     * @param path the path of the {@link Resource}, relative to the current site's resource explorer.
825     * @return an Element containing a file or null if the specified resource does not exist.
826     */
827    public static Node resourceByPath(String path)
828    {
829        Request request = ContextHelper.getRequest(_context);
830        String siteName = (String) request.getAttribute("site");
831        Site site = _siteManager.getSite(siteName);
832
833        try
834        {
835            Resource resource = site.getRootResources().getChild(path);
836            return new ResourceElement(resource, null);
837        }
838        catch (UnknownAmetysObjectException ex)
839        {
840            return null;
841        }
842    }
843
844    /**
845     * Returns a DOM {@link Element} representing files and folder of a skin directory. <br>
846     * This path is intended to be relative to the current skin's 'resources' directory.
847     * @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.
848     * @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.
849     * @throws IOException if an error occured while listing files.
850     */
851    public static Node skinResources(String path) throws IOException
852    {
853        FileSource source = (FileSource) _sourceResolver.resolveURI("skin://resources/" + path);
854        if (source.exists())
855        {
856            return new FileElement(source.getFile());
857        }
858        else
859        {
860            return null;
861        }
862    }
863    
864    //*************************
865    // Page methods
866    //*************************
867    
868    /**
869     * Get the site name of a page.
870     * @param pageId The page id.
871     * @return The name or empty if the page does not exist.
872     */
873    public static String pageSiteName(String pageId)
874    {
875        try
876        {
877            Page page = _getPage(pageId);
878            return page.getSiteName();
879        }
880        catch (UnknownAmetysObjectException e)
881        {
882            _logger.error("Can not get site name on page with id '" + pageId + "'", e);
883            return "";
884        }
885    }
886    
887    /**
888     * Get the title of a page.
889     * @param sitename the site name.
890     * @param lang the sitemap name.
891     * @param path the page path.
892     * @return The name or empty if the meta or the page does not exist.
893     */
894    public static String pageTitle(String sitename, String lang, String path)
895    {
896        try
897        {
898            Page page = _getPage(sitename, lang, path);
899            return page.getTitle();
900        }
901        catch (UnknownAmetysObjectException e)
902        {
903            _logger.warn("Unknown page at path '" + sitename + "/" + lang + "/" + path + "'", e);
904            return "";
905        }
906    }
907    
908    /**
909     * Determines if page exists
910     * @param sitename the site name.
911     * @param lang the sitemap name.
912     * @param path the page path.
913     * @return <code>false</code> the page does not exist.
914     */
915    public static boolean pageExists(String sitename, String lang, String path)
916    {
917        try
918        {
919            _getPage(sitename, lang, path);
920            return true;
921        }
922        catch (UnknownAmetysObjectException e)
923        {
924            _logger.debug("Page at path '" + sitename + "/" + lang + "/" + path + "' does not exists", e);
925            return false;
926        }
927    }
928    
929    /**
930     * Get the title of a page.
931     * @param pageId The page id.
932     * @return The name or empty if the meta or the page does not exist.
933     */
934    public static String pageTitle(String pageId)
935    {
936        try
937        {
938            Page page = _getPage(pageId);
939            return page.getTitle();
940        }
941        catch (UnknownAmetysObjectException e)
942        {
943            _logger.error("Can not get title on page with id '" + pageId + "'", e);
944            return "";
945        }
946    }
947
948    /**
949     * Get the long title of a page
950     * @param sitename the site name
951     * @param lang the page's language
952     * @param path the page's path
953     * @return The name or empty if the meta or the page does not exist
954     */
955    public static String pageLongTitle(String sitename, String lang, String path)
956    {
957        try
958        {
959            Page page = _getPage(sitename, lang, path);
960            return page.getLongTitle();
961        }
962        catch (UnknownAmetysObjectException e)
963        {
964            _logger.error("Can not get long title on page '" + sitename + "/" + lang + "/" + path + "'", e);
965            return "";
966        }
967    }
968    /**
969     * Get the long title of a page
970     * @param pageId The page id
971     * @return The name or empty if the meta or the page does not exist
972     */
973    public static String pageLongTitle(String pageId)
974    {
975        try
976        {
977            Page page = _getPage(pageId);
978            return page.getLongTitle();
979        }
980        catch (UnknownAmetysObjectException e)
981        {
982            _logger.error("Can not get long title on page with id '" + pageId + "'", e);
983            return "";
984        }
985    }
986
987    /**
988     * Get the data of a page at the given path
989     * @param sitename the site name
990     * @param lang the page's language
991     * @param path the page's path
992     * @param dataPath The data path (use '/' as separator for composites and repeaters)
993     * @return The value or empty if the data or the page does not exist
994     */
995    public static String pageMetadata(String sitename, String lang, String path, String dataPath)
996    {
997        try
998        {
999            Page page = _getPage(sitename, lang, path);
1000            return _getPageData(page, dataPath);
1001        }
1002        catch (UnknownAmetysObjectException e)
1003        {
1004            _logger.error("Can not get data at path '" + dataPath + "' on page '" + sitename + "/" + lang + "/" + path + "'", e);
1005            return StringUtils.EMPTY;
1006        }
1007    }
1008    
1009    /**
1010     * Get the data of a page at the given path
1011     * @param pageId The page id
1012     * @param dataPath The data path (use '/' as separator for composites and repeaters)
1013     * @return The value or empty if the data or the page does not exist
1014     */
1015    public static String pageMetadata(String pageId, String dataPath)
1016    {
1017        try
1018        {
1019            Page page = _getPage(pageId);
1020            return _getPageData(page, dataPath);
1021        }
1022        catch (UnknownAmetysObjectException e)
1023        {
1024            _logger.error("Can not get data at path '" + dataPath + "' on page with id '" + pageId + "'", e);
1025            return StringUtils.EMPTY;
1026        }
1027    }
1028    
1029    @SuppressWarnings("unchecked")
1030    private static String _getPageData(Page page, String dataPath)
1031    {
1032        try
1033        {
1034            Object value = page.getValue(dataPath);
1035            ModelItemType type = page.getType(dataPath);
1036            if (value != null && type instanceof ElementType)
1037            {
1038                return ((ElementType) type).toString(value);
1039            }
1040            else
1041            {
1042                _logger.error("Can not get data at path '" + dataPath + "' on page with id '" + page.getId() + "'");
1043                return StringUtils.EMPTY;
1044            }
1045        }
1046        catch (Exception e)
1047        {
1048            _logger.error("Can not get data at path '" + dataPath + "' on page with id '" + page.getId() + "'", e);
1049            return StringUtils.EMPTY;
1050        }
1051    }
1052
1053    /**
1054     * Returns true if the given page is visible into navigation elements
1055     * @param pageId the page id.
1056     * @return true if the page is visible
1057     */
1058    public static boolean pageIsVisible (String pageId)
1059    {
1060        try
1061        {
1062            Page page = _getPage(pageId);
1063            return page.isVisible();
1064        }
1065        catch (UnknownAmetysObjectException e)
1066        {
1067            _logger.error("Can not get visibility status on page with id '" + pageId + "'", e);
1068            return false;
1069        }
1070    }
1071    
1072    /**
1073     * Returns true if the given page is visible into navigation elements
1074     * @param sitename the site name
1075     * @param lang the page's language
1076     * @param path the page's path
1077     * @return true if the page is visible
1078     */
1079    public static boolean pageIsVisible (String sitename, String lang, String path)
1080    {
1081        try
1082        {
1083            Page page = _getPage(sitename, lang, path);
1084            return page.isVisible();
1085        }
1086        catch (UnknownAmetysObjectException e)
1087        {
1088            _logger.error("Can not get visibility status on page with id '" + sitename + "/" + lang + "/" + path + "'", e);
1089            return false;
1090        }
1091    }
1092    
1093    /**
1094     * Returns true if the given page has restricted access.
1095     * @param pageId the page id.
1096     * @return true if the page exists and has restricted access.
1097     */
1098    public static boolean pageHasRestrictedAccess(String pageId)
1099    {
1100        try
1101        {
1102            Page page = _getPage(pageId);
1103            return !_rightManager.hasAnonymousReadAccess(page);
1104        }
1105        catch (UnknownAmetysObjectException e)
1106        {
1107            _logger.error("Can not get page access info on page with id '" + pageId + "'", e);
1108            return false;
1109        }
1110    }
1111
1112    /**
1113     * Returns true if the current user has the specified right on the current page
1114     * @param rightId Right Id
1115     * @return true if the current user has the specified right on the current page
1116     */
1117    public static boolean hasRightOnPage(String rightId)
1118    {
1119        Request request = ContextHelper.getRequest(_context);
1120        Page page = (Page) request.getAttribute(Page.class.getName());
1121        return _hasRightOnPage(rightId, page);
1122    }
1123
1124    /**
1125     * Returns true if the current user has the specified right on the specified page
1126     * @param rightId Right Id
1127     * @param pageId Page Id
1128     * @return true if the current user has the specified right on the specified page
1129     */
1130    public static boolean hasRightOnPage(String rightId, String pageId)
1131    {
1132        Page page = _getPage(pageId);
1133        return _hasRightOnPage(rightId, page);
1134    }
1135
1136    /**
1137     * Returns true if the current user has the specified right on the specified page
1138     * @param rightId Right Id
1139     * @param page Page
1140     * @return true if the current user has the specified right on the specified page
1141     */
1142    private static boolean _hasRightOnPage(String rightId, Page page)
1143    {
1144        RightResult rightResult = _rightManager.currentUserHasRight(rightId, page);
1145        return rightResult == RightResult.RIGHT_ALLOW;
1146    }
1147
1148    /**
1149     * Returns true if the given page has restricted access.
1150     * @param sitename the site name
1151     * @param lang the page's language
1152     * @param path the page's path
1153     * @return true if the page exists and has restricted access.
1154     */
1155    public static boolean pageHasRestrictedAccess(String sitename, String lang, String path)
1156    {
1157        try
1158        {
1159            Page page = _getPage(sitename, lang, path);
1160            return !_rightManager.hasAnonymousReadAccess(page);
1161        }
1162        catch (UnknownAmetysObjectException e)
1163        {
1164            _logger.error("Can not get page access info on page with id '" + sitename + "/" + lang + "/" + path + "'", e);
1165            return false;
1166        }
1167    }
1168    
1169    private static Page _getPage(String id)
1170    {
1171        return _ametysObjectResolver.resolveById(id);
1172    }
1173    
1174    private static Page _getPage(String sitename, String lang, String path)
1175    {
1176        Site site = _siteManager.getSite(sitename);
1177        Sitemap sitemap = site.getSitemap(lang);
1178        return sitemap.getChild(path);
1179    }
1180    
1181    /**
1182     * Returns the path of the current page, relative to the sitemap's root.
1183     * @return the path of the current Page, or empty if there's no current page.
1184     */
1185    public static String pagePath()
1186    {
1187        Request request = ContextHelper.getRequest(_context);
1188        Page page = (Page) request.getAttribute(Page.class.getName());
1189        
1190        return page == null ? "" : page.getPathInSitemap();
1191    }
1192    
1193    /**
1194     * Returns the path in sitemap of a page
1195     * @param pageId The id of page
1196     * @return the path of the Page, or empty if not exists
1197     */
1198    public static String pagePath(String pageId)
1199    {
1200        try
1201        {
1202            Page page = _getPage(pageId);
1203            return page.getPathInSitemap();
1204        }
1205        catch (UnknownAmetysObjectException e)
1206        {
1207            _logger.error("Can not get title on page with id '" + pageId + "'", e);
1208            return "";
1209        }
1210    }
1211    
1212    /**
1213     * Returns the id of the current page.
1214     * @return the id of the current Page, or empty if there's no current page.
1215     */
1216    public static String pageId()
1217    {
1218        Request request = ContextHelper.getRequest(_context);
1219        Page page = (Page) request.getAttribute(Page.class.getName());
1220        
1221        return page == null ? "" : page.getId();
1222    }
1223    
1224    /**
1225     * Returns the id of the current zone item id.
1226     * @return the id of the current zone item id, or empty if there's no current zone item.
1227     */
1228    public static String zoneItemId()
1229    {
1230        Request request = ContextHelper.getRequest(_context);
1231        ZoneItem zoneItem = (ZoneItem) request.getAttribute(WebConstants.REQUEST_ATTR_ZONEITEM);
1232        
1233        return zoneItem == null ? "" : StringUtils.defaultString(zoneItem.getId());
1234    }
1235    
1236    /**
1237     * Determines if the current zone item or (if there is no current zone item) the current page is cacheable
1238     * This method is only valid for a page.
1239     * @return true if the current zone item or page is cacheable.
1240     */
1241    public static boolean isCacheable()
1242    {
1243        Request request = ContextHelper.getRequest(_context);
1244        if (request.getAttribute("IsZoneItemCacheable") != null)
1245        {
1246            return (Boolean) request.getAttribute("IsZoneItemCacheable");
1247        }
1248        
1249        // The method was called from the skin, out of a zone item
1250        Response response = ContextHelper.getResponse(_context);
1251        if (response.containsHeader("X-Ametys-Cacheable"))
1252        {
1253            return true;
1254        }
1255        return false;
1256    }
1257    
1258    /**
1259     * Determines if we are in an edition mode
1260     * @return true if we are in edition mode
1261     */
1262    public static boolean isEditionMode()
1263    {
1264        RenderingContext renderingContext = _renderingContextHandler.getRenderingContext();
1265        Request request = ContextHelper.getRequest(_context);
1266        if (renderingContext == RenderingContext.FRONT && request.getParameter("_edition") != null && "true".equals(request.getParameter("_edition")))
1267        {
1268            return true;
1269        }
1270        return false;
1271    }
1272
1273    /**
1274     * Returns the id of pages referencing the content and for which the Front-office user can access
1275     * @param contentId The content's id
1276     * @return The pages' id
1277     */
1278    public static NodeList accessibleReferencedPages (String contentId)
1279    {
1280        RenderingContext renderingContext = _renderingContextHandler.getRenderingContext();
1281        boolean inBackOffice = renderingContext == RenderingContext.BACK || renderingContext == RenderingContext.PREVIEW;
1282        
1283        List<StringElement> pages = new ArrayList<>(); 
1284        
1285        Content content = _ametysObjectResolver.resolveById(contentId);
1286        if (content instanceof WebContent)
1287        {
1288            Collection<ZoneItem> zoneItems = ((WebContent) content).getReferencingZoneItems();
1289            
1290            for (ZoneItem zoneItem : zoneItems)
1291            {
1292                String metadataSetName = zoneItem.getMetadataSetName();
1293                Page page = zoneItem.getZone().getPage();
1294                
1295                if (inBackOffice || _rightManager.hasReadAccess(_currentUserProvider.getUser(), page))
1296                {
1297                    Map<String, String> attrs = new HashMap<>();
1298                    attrs.put("id", page.getId());
1299                    attrs.put("metadataSetName", metadataSetName);
1300                    pages.add(new StringElement("page", attrs));
1301                }
1302            }
1303        }
1304        
1305        return new AmetysNodeList(pages);
1306    }
1307    
1308    /**
1309     * Returns the id of pages referencing the content
1310     * @param contentId The content's id
1311     * @return The pages' id
1312     */
1313    public static NodeList referencedPages (String contentId)
1314    {
1315        List<StringElement> pages = new ArrayList<>(); 
1316        
1317        Content content = _ametysObjectResolver.resolveById(contentId);
1318        if (content instanceof WebContent)
1319        {
1320            Collection<ZoneItem> zoneItems = ((WebContent) content).getReferencingZoneItems();
1321            
1322            for (ZoneItem zoneItem : zoneItems)
1323            {
1324                String metadataSetName = zoneItem.getMetadataSetName();
1325                Page page = zoneItem.getZone().getPage();
1326                
1327                Map<String, String> attrs = new HashMap<>();
1328                attrs.put("id", page.getId());
1329                attrs.put("metadataSetName", metadataSetName);
1330                pages.add(new StringElement("page", attrs));
1331            }
1332        }
1333        
1334        return new AmetysNodeList(pages);
1335    }
1336    
1337    /**
1338     * Returns the ids of the contents
1339     * @param tag The tag id
1340     * @return Array of contents ids
1341     */
1342    public static NodeList findContentsIdsByTag(String tag)
1343    {
1344        Request request = ContextHelper.getRequest(_context);
1345        String siteName = (String) request.getAttribute("site");
1346
1347        String lang = (String) request.getAttribute("sitemapLanguage");
1348        if (lang == null)
1349        {
1350            // Try to get current language from content
1351            Content content = (Content) request.getAttribute(Content.class.getName());
1352            if (content != null)
1353            {
1354                lang = content.getLanguage();
1355            }
1356        }
1357        return findContentsIdsByTag(siteName, lang, tag);
1358    }
1359
1360    /**
1361     * Returns the ids of the contents
1362     * @param sitename site name. '+' for any site, '*'/null for any site, including contents without site, '^' for only contents without site
1363     * @param lang lang of the contents
1364     * @param tag The tag id
1365     * @return Array of contents ids
1366     */
1367    public static NodeList findContentsIdsByTag(String sitename, String lang, String tag)
1368    {
1369        List<Expression> expressions = new ArrayList<>();
1370        TagExpression tagExpression = new TagExpression(Operator.EQ, tag);
1371        expressions.add(tagExpression);
1372
1373        if (lang != null && !lang.equals("*"))
1374        {
1375            LanguageExpression le = new LanguageExpression(Operator.EQ, lang);
1376            expressions.add(le);
1377        }
1378
1379        if (sitename != null)
1380        {
1381            if (sitename.equals("+"))
1382            {
1383                MetadataExpression me = new MetadataExpression("site");
1384                expressions.add(me);
1385            }
1386            else if (sitename.equals("*"))
1387            {
1388                // no filter
1389            }
1390            else if (sitename.equals("^"))
1391            {
1392                MetadataExpression me = new MetadataExpression("site");
1393                expressions.add(new NotExpression(me));
1394            }
1395            else
1396            {
1397                StringExpression se = new StringExpression("site", Operator.EQ, sitename);
1398                expressions.add(se);
1399            }
1400        }
1401
1402        Expression[] expressionsArray = expressions.toArray(new Expression[expressions.size()]);
1403
1404        String xpath = ContentQueryHelper.getContentXPathQuery(new AndExpression(expressionsArray));
1405        AmetysObjectIterable<Content> contents = _ametysObjectResolver.query(xpath);
1406        Iterator<Content> it = contents.iterator();
1407
1408        List<StringElement> list = new ArrayList<>();
1409        while (it.hasNext())
1410        {
1411            list.add(new StringElement("content", "id", it.next().getId()));
1412        }
1413        return new AmetysNodeList(list);
1414    }
1415
1416    /**
1417     * Returns the ids of the pages
1418     * @param sitename The site id
1419     * @param lang The language code
1420     * @param tag The tag id
1421     * @return Array of pages ids
1422     */
1423    public static NodeList findPagesIdsByTag(String sitename, String lang, String tag)
1424    {
1425        String xpath = PageQueryHelper.getPageXPathQuery(sitename, lang, null, new TagExpression(Operator.EQ, tag), null);
1426        AmetysObjectIterable<Page> pages = _ametysObjectResolver.query(xpath);
1427        Iterator<Page> it = pages.iterator();
1428
1429        List<StringElement> list = new ArrayList<>(); 
1430        while (it.hasNext())
1431        {
1432            list.add(new StringElement("page", "id", it.next().getId()));
1433        }
1434        return new AmetysNodeList(list);
1435    }
1436    
1437    /**
1438     * Returns the ids of the pages
1439     * @param tag The tag id
1440     * @return Array of pages ids
1441     */
1442    public static NodeList findPagesIdsByTag(String tag)
1443    {
1444        Request request = ContextHelper.getRequest(_context);
1445        String siteName = (String) request.getAttribute("site");
1446        
1447        String lang = (String) request.getAttribute("sitemapLanguage");
1448        if (lang == null)
1449        {
1450            // Try to get current language from content
1451            Content content = (Content) request.getAttribute(Content.class.getName());
1452            if (content != null)
1453            {
1454                lang = content.getLanguage();
1455            }
1456        }
1457        
1458        return findPagesIdsByTag(siteName, lang, tag);
1459    }
1460    
1461    /**
1462     * Return the given user by its email over all authorized population on current site
1463     * @param email the concerned user's email
1464     * @return The informations about the given user
1465     * @throws SAXException If an error occurred while saxing the user
1466     */
1467    public static Node userByMail(String email) throws SAXException
1468    {
1469        String siteName = site();
1470        
1471        Set<String> userPopulationsOnSite = _populationContextHelper.getUserPopulationsOnContexts(Arrays.asList("/sites/" + siteName, "/sites-fo/" + siteName), false, false);
1472        
1473        for (String populationId : userPopulationsOnSite)
1474        {
1475            User user = _userHelper.getUserByEmail(populationId, email);
1476            if (user != null)
1477            {
1478                return user(UserIdentity.userIdentityToString(user.getIdentity()));
1479            }
1480        }
1481        
1482        return null;
1483    }
1484    
1485    /**
1486     * Get the preview of a url (title, description, image, favico)
1487     * @param url the web link
1488     * @return the url preview
1489     * @throws SAXException If an error occurred while saxing
1490     */
1491    public static Node urlPreview(String url) throws SAXException
1492    {
1493        try
1494        {
1495            String lang = StringUtils.defaultIfEmpty(lang(), "en");
1496            UrlPreview urlPreview = _urlPreview.getUrlPreview(url, lang);
1497            DOMBuilder domBuilder = new DOMBuilder();
1498            
1499            urlPreview.toSAX(domBuilder, "preview");
1500            
1501            return domBuilder.getDocument();
1502        }
1503        catch (IOException e)
1504        {
1505            _logger.error("Unable to get preview URL at " + url, e);
1506            return null;
1507        }
1508    }
1509    
1510    /**
1511     * Generates a XML structure to help to creates a complex pagination. 
1512     * <br>
1513     * <br>Example: pagination(39, 19, 2, 5, 2) will return
1514     * 
1515     * <pre>
1516     *   &lt;gotofirstpage enabled="true"&gt;1&lt;/gotofirstpage&gt;
1517     *   &lt;gotopreviouspage enabled="true"&gt;18&lt;/gotopreviouspage&gt;
1518     *   &lt;page&gt;1&lt;/page&gt;
1519     *   &lt;page&gt;2&lt;/page&gt;
1520     *   &lt;separator/&gt;
1521     *   &lt;page&gt;17&lt;/page&gt;
1522     *   &lt;page&gt;18&lt;/page&gt;
1523     *   &lt;current&gt;19&lt;/current&gt;
1524     *   &lt;page&gt;20&lt;/page&gt;
1525     *   &lt;page&gt;21&lt;/page&gt;
1526     *   &lt;separator/&gt;
1527     *   &lt;page&gt;38&lt;/page&gt;
1528     *   &lt;page&gt;39&lt;/page&gt;
1529     *   &lt;gotonextpage enabled="true"&gt;20&lt;/gotonextpage&gt;
1530     *   &lt;gotolastpage enabled="true"&gt;39&lt;/gotonextpage&gt;
1531     * </pre>
1532     *   
1533     * Example: pagination(5, 2, 2, 5, 2) will return
1534     *   
1535     * <pre>
1536     *   &lt;gotofirstpage enabled="true"&gt;1&lt;/gotofirstpage&gt;
1537     *   &lt;gotopreviouspage enabled="true"&gt;1&lt;/gotopreviouspage&gt;
1538     *   &lt;page&gt;1&lt;/page&gt;
1539     *   &lt;current&gt;2&lt;/page&gt;
1540     *   &lt;page&gt;3&lt;/page&gt;
1541     *   &lt;page&gt;4&lt;/page&gt;
1542     *   &lt;page&gt;5&lt;/page&gt;
1543     *   &lt;space/&gt;
1544     *   &lt;space/&gt;
1545     *   &lt;space/&gt;
1546     *   &lt;space/&gt;
1547     *   &lt;gotonextpage enabled="true"&gt;3&lt;/gotonextpage&gt;
1548     *   &lt;gotolastpage enabled="true"&gt;5&lt;/gotonextpage&gt;
1549     * </pre>
1550     *   
1551     * @param nbPages The total number of pages
1552     * @param currentPage The currently displayed page (1 based)
1553     * @param nbFirstPages The max number of pages to display before the 1st separator
1554     * @param nbCentralPages The max number of pages to display around the current page
1555     * @param nbLastPages The max number of pages to display after the 2nd separator
1556     * @return The xml described
1557     */
1558    public static AmetysNodeList pagination(int nbPages, int currentPage, int nbFirstPages, int nbCentralPages, int nbLastPages)
1559    {
1560        List<Node> elements = new ArrayList<>();
1561
1562        
1563        elements.add(new StringElement("gotofirstpage", "enabled", Boolean.toString(currentPage > 1), "1"));
1564        elements.add(new StringElement("gotopreviouspage", "enabled", Boolean.toString(currentPage > 1), currentPage > 1 ? currentPage - 1 + "" : ""));
1565
1566        int[] pagination = _pagination(nbPages, currentPage, nbFirstPages, nbCentralPages, nbLastPages);
1567        for (int page : pagination)
1568        {
1569            if (page == _PAGINATION_SEPARATOR)
1570            {
1571                elements.add(new StringElement("separator", ""));
1572            }
1573            else if (page == _PAGINATION_SPACE)
1574            {
1575                elements.add(new StringElement("space", ""));
1576            }
1577            else if (page == _PAGINATION_CURRENT)
1578            {
1579                elements.add(new StringElement("current", Integer.toString(currentPage)));
1580            }
1581            else
1582            {
1583                elements.add(new StringElement("page", Integer.toString(page)));
1584            }
1585        }
1586        
1587        elements.add(new StringElement("gotonextpage", "enabled", Boolean.toString(currentPage < nbPages), currentPage < nbPages ? currentPage + 1 + "" : ""));
1588        elements.add(new StringElement("gotolastpage", "enabled", Boolean.toString(currentPage < nbPages), nbPages + ""));
1589
1590        return new AmetysNodeList(elements);
1591    }
1592    
1593    static int[] _pagination(int nbPages, int currentPage, int nbFirstPages, int nbCentralPages, int nbLastPages)
1594    {
1595        int displayedPages = nbFirstPages + 1 + nbCentralPages + 1 + nbLastPages;  // The +1 are the room for separators
1596        
1597        int[] values = new int[displayedPages];
1598
1599        int centerOfCentralPages = (int) Math.ceil(nbCentralPages / 2.0);
1600        int centralCursor = nbFirstPages + 1 + centerOfCentralPages;
1601        boolean firstSeparator = nbPages > displayedPages && currentPage > centralCursor;
1602        boolean secondSeparator = nbPages > displayedPages && currentPage <= nbPages - centralCursor;
1603        
1604        int cursor = 1;
1605        
1606        // Before first separator
1607        cursor = _paginationFirstPages(nbPages, nbFirstPages, currentPage, values, cursor);
1608        
1609        // First separator
1610        cursor = _paginationFirstSeparator(nbPages, firstSeparator, currentPage, values, cursor);
1611
1612        int offset = _paginationComputeOffsetAfterFirstSeparator(nbPages, currentPage, nbCentralPages, nbLastPages, centerOfCentralPages, firstSeparator, secondSeparator, cursor);
1613        
1614        // Middle part
1615        cursor = _paginationMiddle(nbPages, currentPage, nbFirstPages, nbCentralPages, values, cursor, offset);
1616        
1617        // Second separator
1618        cursor = _paginationSecondSeparator(nbPages, secondSeparator, currentPage, nbLastPages, values, cursor, offset);
1619        
1620        // After second separator
1621        cursor = _paginationLastPages(nbPages, currentPage, displayedPages, values, cursor, offset);
1622        
1623        return values;
1624    }
1625
1626    private static int _paginationMiddle(int nbPages, int currentPage, int nbFirstPages, int nbCentralPages, int[] values, int cursorP, int offset)
1627    {
1628        int cursor = cursorP;
1629        for (; cursor <= nbFirstPages + 1 + nbCentralPages; cursor++)
1630        {
1631            if (cursor + offset > nbPages)
1632            {
1633                values[cursor - 1] = _PAGINATION_SPACE;
1634            }
1635            else if (cursor + offset == currentPage)
1636            {
1637                values[cursor - 1] = _PAGINATION_CURRENT;
1638            }
1639            else
1640            {
1641                values[cursor - 1] = offset + cursor;
1642            }
1643        }
1644        return cursor;
1645    }
1646
1647    private static int _paginationComputeOffsetAfterFirstSeparator(int nbPages, int currentPage, int nbCentralPages, int nbLastPages, int centerOfCentralPages, boolean firstSeparator, boolean secondSeparator, int cursor)
1648    {
1649        if (!firstSeparator)
1650        {
1651            return 0;
1652        }
1653        else if (!secondSeparator)
1654        {
1655            return nbPages - nbLastPages - nbCentralPages - cursor;
1656        }
1657        else
1658        {
1659            return currentPage + 1 - centerOfCentralPages - cursor;
1660        }
1661    }
1662
1663    private static int _paginationLastPages(int nbPages, int currentPage, int displayedPages, int[] values, int cursorP, int offset)
1664    {
1665        int cursor = cursorP;
1666        for (; cursor <= displayedPages; cursor++)
1667        {
1668            if (cursor > nbPages)
1669            {
1670                values[cursor - 1] = _PAGINATION_SPACE;
1671            }
1672            else if (cursor + offset == currentPage)
1673            {
1674                values[cursor - 1] = _PAGINATION_CURRENT;
1675            }
1676            else 
1677            {
1678                values[cursor - 1] = nbPages - (displayedPages - cursor);
1679            }
1680        }
1681        return cursor;
1682    }
1683
1684    private static int _paginationSecondSeparator(int nbPages, boolean secondSeparator, int currentPage, int nbLastPages, int[] values, int cursor, int offset)
1685    {
1686        if (cursor + offset > nbPages)
1687        {
1688            values[cursor - 1] = _PAGINATION_SPACE;
1689        }
1690        else if (currentPage == cursor + offset)
1691        {
1692            values[cursor - 1] = _PAGINATION_CURRENT;
1693        }
1694        else if (secondSeparator)
1695        {
1696            values[cursor - 1] = _PAGINATION_SEPARATOR;
1697        }
1698        else 
1699        {
1700            values[cursor - 1] = nbPages - nbLastPages;
1701        }
1702        return cursor + 1;
1703    }
1704
1705    private static int _paginationFirstSeparator(int nbPages, boolean firstSeparator, int currentPage, int[] values, int cursor)
1706    {
1707        if (cursor > nbPages)
1708        {
1709            values[cursor - 1] = _PAGINATION_SPACE;
1710        }
1711        else if (currentPage == cursor)
1712        {
1713            values[cursor - 1] = _PAGINATION_CURRENT;
1714        }
1715        else if (firstSeparator)
1716        {
1717            values[cursor - 1] = _PAGINATION_SEPARATOR;
1718        }
1719        else 
1720        {
1721            values[cursor - 1] = cursor;
1722        }
1723        return cursor + 1;
1724    }
1725
1726    private static int _paginationFirstPages(int nbPages, int nbFirstPages, int currentPage, int[] values, int cursorP)
1727    {
1728        int cursor = cursorP;
1729        for (; cursor <= nbFirstPages; cursor++)
1730        {
1731            if (cursor > nbPages)
1732            {
1733                values[cursor - 1] = _PAGINATION_SPACE;
1734            }
1735            else if (cursor == currentPage)
1736            {
1737                values[cursor - 1] = _PAGINATION_CURRENT;
1738            }
1739            else 
1740            {
1741                values[cursor - 1] = cursor;
1742            }
1743        }
1744        return cursor;
1745    }
1746}