001/*
002 *  Copyright 2012 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.cms.transformation.xslt;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.ArrayList;
022import java.util.HashMap;
023import java.util.List;
024import java.util.Locale;
025import java.util.Map;
026import java.util.Properties;
027
028import javax.xml.parsers.DocumentBuilder;
029import javax.xml.parsers.DocumentBuilderFactory;
030import javax.xml.transform.OutputKeys;
031import javax.xml.transform.Transformer;
032import javax.xml.transform.TransformerConfigurationException;
033import javax.xml.transform.TransformerException;
034import javax.xml.transform.TransformerFactory;
035import javax.xml.transform.URIResolver;
036import javax.xml.transform.dom.DOMResult;
037import javax.xml.transform.sax.SAXTransformerFactory;
038import javax.xml.transform.sax.TransformerHandler;
039import javax.xml.transform.stream.StreamSource;
040
041import org.apache.avalon.framework.context.Context;
042import org.apache.avalon.framework.context.ContextException;
043import org.apache.avalon.framework.logger.LogEnabled;
044import org.apache.avalon.framework.logger.Logger;
045import org.apache.avalon.framework.service.ServiceException;
046import org.apache.avalon.framework.service.ServiceManager;
047import org.apache.cocoon.components.ContextHelper;
048import org.apache.cocoon.components.source.SourceUtil;
049import org.apache.cocoon.environment.Request;
050import org.apache.cocoon.xml.XMLUtils;
051import org.apache.commons.lang.StringUtils;
052import org.apache.commons.lang3.ArrayUtils;
053import org.apache.excalibur.source.Source;
054import org.apache.excalibur.source.SourceResolver;
055import org.w3c.dom.Document;
056import org.w3c.dom.Element;
057import org.w3c.dom.Node;
058import org.w3c.dom.NodeList;
059import org.xml.sax.SAXException;
060
061import org.ametys.cms.content.ContentHelper;
062import org.ametys.cms.contenttype.ContentType;
063import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
064import org.ametys.cms.data.RichText;
065import org.ametys.cms.data.RichTextHelper;
066import org.ametys.cms.repository.Content;
067import org.ametys.cms.tag.CMSTag;
068import org.ametys.cms.tag.ColorableTag;
069import org.ametys.cms.tag.Tag;
070import org.ametys.cms.tag.TagProviderExtensionPoint;
071import org.ametys.cms.transformation.dom.TagElement;
072import org.ametys.core.util.dom.AmetysNodeList;
073import org.ametys.core.util.dom.EmptyElement;
074import org.ametys.core.util.dom.MapElement;
075import org.ametys.core.util.dom.StringElement;
076import org.ametys.plugins.explorer.resources.Resource;
077import org.ametys.plugins.explorer.resources.ResourceCollection;
078import org.ametys.plugins.explorer.resources.dom.ResourceCollectionElement;
079import org.ametys.plugins.repository.AmetysObjectResolver;
080import org.ametys.plugins.repository.AmetysRepositoryException;
081import org.ametys.plugins.repository.UnknownAmetysObjectException;
082import org.ametys.plugins.repository.data.holder.ModelAwareDataHolder;
083import org.ametys.plugins.repository.metadata.CompositeMetadata;
084import org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType;
085import org.ametys.plugins.repository.metadata.UnknownMetadataException;
086import org.ametys.plugins.repository.version.VersionAwareAmetysObject;
087import org.ametys.runtime.model.exception.BadDataPathCardinalityException;
088import org.ametys.runtime.model.type.DataContext;
089
090/**
091 * Helper component to be used from XSL stylesheets.
092 */
093public class AmetysXSLTHelper extends org.ametys.core.util.AmetysXSLTHelper implements LogEnabled
094{
095    /** The Ametys object resolver */
096    protected static AmetysObjectResolver _ametysObjectResolver;
097    /** The content types extension point */
098    protected static ContentTypeExtensionPoint _cTypeExtensionPoint;
099    /** The tags provider */
100    protected static TagProviderExtensionPoint _tagProviderExtPt;
101    /** Helper for content */
102    protected static ContentHelper _contentHelper;
103    /** The avalon context */
104    protected static Context _context;
105    /** The logger */
106    protected static Logger _logger;
107    /** The source resolver */
108    protected static SourceResolver _sourceResolver;
109    /** The sax parser */
110    protected static RichTextHelper _richTextHelper;
111    
112    @Override
113    public void contextualize(Context context) throws ContextException
114    {
115        super.contextualize(context);
116        _context = context;
117    }
118    
119    @Override
120    public void enableLogging(Logger logger)
121    {
122        _logger = logger;
123    }
124    
125    @Override
126    public void service(ServiceManager manager) throws ServiceException
127    {
128        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
129        _cTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
130        _tagProviderExtPt = (TagProviderExtensionPoint) manager.lookup(TagProviderExtensionPoint.ROLE);
131        _contentHelper = (ContentHelper) manager.lookup(ContentHelper.ROLE);
132        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
133        _richTextHelper = (RichTextHelper) manager.lookup(RichTextHelper.ROLE);
134    }
135    
136    /* ------------------------ */
137    /*      Content methods     */
138    /* ------------------------ */
139    
140    /**
141     * Get the content types of a content
142     * @param contentId The content id
143     * @return The content type or empty if the content does not exist
144     */
145    public static NodeList contentTypes(String contentId)
146    {
147        ArrayList<StringElement> contentTypes = new ArrayList<>();
148        
149        try
150        {
151            Content content = _ametysObjectResolver.resolveById(contentId);
152            
153            try
154            {
155                for (String id : content.getTypes())
156                {
157                    contentTypes.add(new StringElement("content-type", "id", id));
158                }
159            }
160            catch (AmetysRepositoryException e)
161            {
162                _logger.error("Can not get type of content with id '" + contentId + "'", e);
163            }
164        }
165        catch (UnknownAmetysObjectException e)
166        {
167            _logger.error("Can not get type of content with id '" + contentId + "'", e);
168        }
169        
170        return new AmetysNodeList(contentTypes);
171    }
172    
173    /**
174     * Get the mixins of a content
175     * @param contentId The content id
176     * @return The content type or empty if the content does not exist
177     */
178    public static NodeList contentMixinTypes(String contentId)
179    {
180        ArrayList<StringElement> contentTypes = new ArrayList<>();
181        
182        try
183        {
184            Content content = _ametysObjectResolver.resolveById(contentId);
185            
186            try
187            {
188                for (String id : content.getMixinTypes())
189                {
190                    contentTypes.add(new StringElement("mixin", "id", id));
191                }
192            }
193            catch (AmetysRepositoryException e)
194            {
195                _logger.error("Can not get type of content with id '" + contentId + "'", e);
196            }
197        }
198        catch (UnknownAmetysObjectException e)
199        {
200            _logger.error("Can not get type of content with id '" + contentId + "'", e);
201        }
202        
203        return new AmetysNodeList(contentTypes);
204    }
205    
206    /**
207     * Determines if the content of given id is a entry of reference table
208     * @param contentId the content id
209     * @return true if the content type is a reference table
210     */
211    public static boolean isReferenceTableContent(String contentId)
212    {
213        try
214        {
215            Content content = _ametysObjectResolver.resolveById(contentId);
216            return _contentHelper.isReferenceTable(content);
217        }
218        catch (UnknownAmetysObjectException e)
219        {
220            _logger.error("Can not get type of unknown content with id '" + contentId + "'", e);
221            return false;
222        }
223    }
224    
225    /**
226     * Returns the current language for rendering.
227     * @return the current language for rendering.
228     */
229    public static String lang()
230    {
231        Request request = ContextHelper.getRequest(_context);
232        return (String) request.getAttribute("renderingLanguage");
233    }
234    
235    /**
236     * Determines if there is a value for the data at the given path
237     * @param contentId The content id
238     * @param dataPath the path of data
239     * @return true if the data exists
240     */
241    public static boolean hasValue(String contentId, String dataPath)
242    {
243        try
244        {
245            Content content = _ametysObjectResolver.resolveById(contentId);
246            return content.hasValue(dataPath);
247        }
248        catch (UnknownAmetysObjectException e)
249        {
250            if (_logger.isDebugEnabled())
251            {
252                _logger.debug("Can not check if attribute '" + dataPath + "' exists on unknown content with id '" + contentId + "'", e);
253            }
254            return false;
255        }
256    }
257    
258    /**
259     * Get the attribute of a content at the given path
260     * @param contentId The content id
261     * @param dataPath The data path
262     * @return The value into a "value" node or null if an error occurred
263     */
264    public static NodeList contentAttribute(String contentId, String dataPath)
265    {
266        return contentAttribute(contentId, dataPath, null);
267    }
268    
269    /**
270     * Get the attribute of a content at the given path
271     * @param contentId The content id
272     * @param dataPath The data path
273     * @param lang The language for localized attribute. Can be null for non-localized attribute or to get the values for all existing locales. 
274     * @return The value into a "value" node or null if an error occurred
275     */
276    public static NodeList contentAttribute(String contentId, String dataPath, String lang)
277    {
278        try
279        {
280            Content content = _ametysObjectResolver.resolveById(contentId);
281            List<Node> values = _getNodeValues(content.getDataHolder(), dataPath, lang, DataContext.newInstance().withObjectId(contentId));
282            if (values != null)
283            {
284                return new AmetysNodeList(values);
285            }
286        }
287        catch (UnknownAmetysObjectException e)
288        {
289            _logger.error("Can not get attribute at path '" + dataPath + "' on unknown content with id '" + contentId + "'", e);
290        }
291        
292        return null;
293    }
294
295    /**
296     * Get values of an attribute of a model aware data holder at the given path
297     * @param dataHolder the data holder
298     * @param dataPath The data path
299     * @param lang The language for localized attribute. Can be null for non-localized attribute or to get the values for all existing locales. 
300     * @return A Node for each values or null if an error occurred
301     */
302    protected static List<Node> _getNodeValues(ModelAwareDataHolder dataHolder, String dataPath, String lang)
303    {
304        return _getNodeValues(dataHolder, dataPath, lang, DataContext.newInstance());
305    }
306    
307    /**
308     * Get values of an attribute of a model aware data holder at the given path
309     * @param dataHolder the data holder
310     * @param dataPath The data path
311     * @param lang The language for localized attribute. Can be null for non-localized attribute or to get the values for all existing locales.
312     * @param dataContext The data context 
313     * @return A Node for each values or null if an error occurred
314     */
315    protected static List<Node> _getNodeValues(ModelAwareDataHolder dataHolder, String dataPath, String lang, DataContext dataContext)
316    {
317        
318        if (dataHolder == null)
319        {
320            return null;
321        }
322        
323        try
324        {
325            SAXTransformerFactory saxTransformerFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
326            TransformerHandler th = saxTransformerFactory.newTransformerHandler();
327            
328            DOMResult result = new DOMResult();
329            th.setResult(result);
330            
331            th.startDocument();
332            XMLUtils.startElement(th, "value");
333            
334            Locale locale = StringUtils.isEmpty(lang) ? null : new Locale(lang);
335            dataHolder.dataToSAX(th, dataPath, dataContext.cloneContext().withLocale(locale).withEmptyValues(false));
336            
337            XMLUtils.endElement(th, "value");
338            th.endDocument();
339            
340            List<Node> values = new ArrayList<>();
341            
342            // #getChildNodes() returns a NodeList that contains the value(s) saxed 
343            // we cannot returns directly this NodeList because saxed values should be wrapped into a <value> tag.
344            NodeList childNodes = result.getNode().getFirstChild().getChildNodes(); 
345            for (int i = 0; i < childNodes.getLength(); i++)
346            {
347                Node n = childNodes.item(i);
348                values.add(n);
349            }
350            
351            return values;
352        }
353        catch (BadDataPathCardinalityException e)
354        {
355            _logger.error("Unable to get attribute at path '" + dataPath + "'. Path is invalid.", e);
356        }
357        catch (TransformerConfigurationException | SAXException e)
358        {
359            _logger.error("Fail to sax attribute at path '" + dataPath + "'", e);
360        }
361        catch (Exception e) 
362        {
363            _logger.error("An error occurred, impossible to get attribute at path '" + dataPath + "'", e);
364        }
365        
366        return null;
367    }
368    
369    /**
370     * Get the metadata of a content
371     * @param contentId The content id
372     * @param metadataName The metadata name (/ for composite)
373     * @param lang The language for localized metadata. Can be null to get the current language.
374     * @return The name or empty if the metadata or the content does not exist
375     */
376    public static String contentMetadata(String contentId, String metadataName, String lang)
377    {
378        try
379        {
380            Content content = _ametysObjectResolver.resolveById(contentId);
381            try
382            {
383                Locale locale = StringUtils.isEmpty(lang) ? null : new Locale(lang);
384                return _getMetadata(content.getMetadataHolder(), metadataName, locale);
385            }
386            catch (UnknownMetadataException e)
387            {
388                _logger.debug("Can not get metadata '" + metadataName + "' on content with id '" + contentId + "'", e);
389                return "";
390            }
391        }
392        catch (UnknownAmetysObjectException e)
393        {
394            _logger.debug("Can not get metadata '" + metadataName + "' on unknown content with id '" + contentId + "'", e);
395            return "";
396        }
397    }
398    
399    /**
400     * Get the metadata of a content
401     * @param contentId The content id
402     * @param metadataName The metadata name (/ for composite)
403     * @return The name or empty if the metadata or the content does not exist
404     */
405    public static String contentMetadata(String contentId, String metadataName)
406    {
407        return contentMetadata(contentId, metadataName, null);
408    }
409    
410    private static String _getMetadata(CompositeMetadata cm, String metadataName, Locale locale)
411    {
412        int i = metadataName.indexOf("/");
413        if (i == -1)
414        {
415            if (cm.getType(metadataName).equals(MetadataType.MULTILINGUAL_STRING))
416            {
417                if (locale == null)
418                {
419                    String currentLanguage = lang();
420                    if (StringUtils.isEmpty(currentLanguage))
421                    {
422                        _logger.error("Can not get the value of a multilingual metadata " + metadataName + " without a defined locale");
423                        return "";
424                    }
425                    return cm.getLocalizedString(metadataName, new Locale(currentLanguage));
426                }
427                else
428                {
429                    return cm.getLocalizedString(metadataName, locale);
430                }
431            }
432            else
433            {
434                return cm.getString(metadataName);
435            }
436        }
437        else
438        {
439            return _getMetadata(cm.getCompositeMetadata(metadataName.substring(0, i)), metadataName.substring(i + 1), locale);
440        }
441    }
442    
443    /**
444     * Returns a DOM {@link Element} containing {@link Resource}s representing the attachments of the current content.
445     * @return an Element containing the attachments of the current content as {@link Resource}s.
446     */
447    public static Node contentAttachments()
448    {
449        Request request = ContextHelper.getRequest(_context);
450        
451        Content content = (Content) request.getAttribute(Content.class.getName());
452        
453        return contentAttachments(content);
454    }
455    
456    /**
457     * Returns a DOM {@link Element} containing {@link Resource}s representing the attachments of a given content.
458     * @param contentId the content ID.
459     * @return an Element containing the attachments of the given content as {@link Resource}s.
460     */
461    public static Node contentAttachments(String contentId)
462    {
463        Content content = _ametysObjectResolver.resolveById(contentId);
464        
465        return contentAttachments(content);
466    }
467    
468    /**
469     * Returns a DOM {@link Element} containing {@link Resource}s representing the attachments of a given content.
470     * @param content the content.
471     * @return an Element containing the attachments of the given content as {@link Resource}s.
472     */
473    private static Node contentAttachments(Content content)
474    {
475        if (content == null)
476        {
477            return null;
478        }
479        
480        ResourceCollection collection = content.getRootAttachments();
481        
482        return collection != null ? new ResourceCollectionElement(collection) : new EmptyElement("collection");
483    }
484    
485    /**
486     * Set the content of given id in request attribute
487     * @param contentId the id of content
488     */
489    public static void setCurrentContent(String contentId)
490    {
491        setCurrentContent(contentId, null);
492    }
493    
494    /**
495     * Set the content of given id and version in request attribute
496     * @param contentId the id of content
497     * @param versionLabel The version label
498     */
499    public static void setCurrentContent(String contentId, String versionLabel)
500    {
501        Request request = ContextHelper.getRequest(_context);
502        
503        Content content = _ametysObjectResolver.resolveById(contentId);
504        
505        if (StringUtils.isNotEmpty(versionLabel) && content instanceof VersionAwareAmetysObject)
506        {
507            String[] allLabels = ((VersionAwareAmetysObject) content).getAllLabels();
508            if (ArrayUtils.contains(allLabels, versionLabel))
509            {
510                ((VersionAwareAmetysObject) content).switchToLabel(versionLabel);
511            }
512        }
513        
514        request.setAttribute(Content.class.getName(), content);
515        
516    }
517    
518  //*************************
519    // Tag methods
520    //*************************
521    
522    /**
523     * Returns all tags of the current content.
524     * @return a list of tags.
525     */
526    public static NodeList contentTags()
527    {
528        Request request = ContextHelper.getRequest(_context);
529        
530        Content content = (Content) request.getAttribute(Content.class.getName());
531        return _contentTags(content);
532    }
533    
534    /**
535     * Returns all tags of the given content
536     * @param contentId The identifier of the content
537     * @return a list of tags.
538     */
539    public static NodeList contentTags(String contentId)
540    {
541        try
542        {
543            Content content = _ametysObjectResolver.resolveById(contentId);
544            return _contentTags(content);
545        }
546        catch (AmetysRepositoryException e)
547        {
548            _logger.warn("Cannot get tags for content '" + contentId + "'", e);
549        }
550        
551        return null;
552    }
553    
554    /**
555     * Returns all tags of the given content
556     * @param content The content
557     * @return a list of tags.
558     */
559    protected static NodeList _contentTags(Content content)
560    {
561        if (content == null)
562        {
563            return null;
564        }
565        
566        List<TagElement> list = new ArrayList<>();
567        
568        for (String tag : content.getTags())
569        {
570            list.add(new TagElement(tag));
571        }
572        
573        return new AmetysNodeList(list);
574    }
575    
576    /**
577     * Get the name of the parent of a tag.
578     * @param siteName the site name
579     * @param tagName the tag's name
580     * @return The id of parent or empty if not found
581     */
582    public static String tagParent(String siteName, String tagName)
583    {
584        Map<String, Object> contextParameters = new HashMap<>();
585        contextParameters.put("siteName", siteName);
586        
587        Tag tag = _tagProviderExtPt.getTag(tagName, contextParameters);
588        if (tag == null)
589        {
590            return StringUtils.EMPTY;
591        }
592        
593        String parentName = tag.getParentName();
594        return parentName != null ? parentName : StringUtils.EMPTY;
595    }
596    
597    /**
598     * Get the path of a tag. The path contains the tag's parents seprated by '/'.
599     * @param siteName The site name
600     * @param tagName The unique tag's name
601     * @return The tag's path or empty string if tag does not exist
602     */
603    public static String tagPath (String siteName, String tagName)
604    {
605        Map<String, Object> contextParameters = new HashMap<>();
606        contextParameters.put("siteName", siteName);
607        
608        Tag tag = _tagProviderExtPt.getTag(tagName, contextParameters);
609        if (tag == null)
610        {
611            return StringUtils.EMPTY;
612        }
613        
614        String path = tagName;
615        
616        Tag parentTag = tag.getParent();
617        while (parentTag != null)
618        {
619            path = parentTag.getName() + "/" + path;
620            parentTag = parentTag.getParent();
621        }
622
623        return path;
624    }
625    
626    /**
627     * Get the label of a tag
628     * @param siteName the current site
629     * @param tagName the name of the tag
630     * @param lang the lang (if i18n tag)
631     * @return the label of the tag or empty if it cannot be found
632     */
633    public static String tagLabel(String siteName, String tagName, String lang)
634    {
635        Map<String, Object> contextParameters = new HashMap<>();
636        contextParameters.put("siteName", siteName);
637        
638        Tag tag = _tagProviderExtPt.getTag(tagName, contextParameters);
639        return tag == null ? "" : _i18nUtils.translate(tag.getTitle(), lang);
640    }
641    
642    /**
643     * Get the description of a tag
644     * @param siteName the current site
645     * @param tagName the name of the tag
646     * @param lang the lang (if i18n tag)
647     * @return the label of the tag or empty if it cannot be found
648     */
649    public static String tagDescription(String siteName, String tagName, String lang)
650    {
651        Map<String, Object> contextParameters = new HashMap<>();
652        contextParameters.put("siteName", siteName);
653        
654        Tag tag = _tagProviderExtPt.getTag(tagName, contextParameters);
655        return tag == null ? "" : _i18nUtils.translate(tag.getDescription(), lang);
656    }
657    
658    /**
659     * Get the visibility of a tag
660     * @param siteName the current site
661     * @param tagName the name of the tag
662     * @return the lower-cased visibility of the tag ("public" or "private")
663     */
664    public static String tagVisibility(String siteName, String tagName)
665    {
666        Map<String, Object> contextParameters = new HashMap<>();
667        contextParameters.put("siteName", siteName);
668        
669        CMSTag tag = _tagProviderExtPt.getTag(tagName, contextParameters);
670        return tag == null ? "" : tag.getVisibility().toString().toLowerCase();
671    }
672    
673    /**
674     * Get the color (main and text) of a tag
675     * @param siteName the current site
676     * @param tagName the name of the tag
677     * @return the the color (main and text) of a tag
678     */
679    public static MapElement tagColor(String siteName, String tagName)
680    {
681        Map<String, Object> contextParameters = new HashMap<>();
682        contextParameters.put("siteName", siteName);
683        
684        Tag tag = _tagProviderExtPt.getTag(tagName, contextParameters);
685        if (tag instanceof ColorableTag colorTag)
686        {
687            String color = colorTag.getColor(true);
688            Map<String, String> map = colorTag.getColorComponent().getColors().get(color);
689            return map != null ? new MapElement("color", map) : null;
690        }
691        
692        return null;
693    }
694    
695    /* ----------------------------- */
696    /*      Content type methods     */
697    /* ----------------------------- */
698    
699    /**
700     * Returns all tags of a content type
701     * @param contentTypeId The id of the content type
702     * @return a list of tags.
703     */
704    public static NodeList contentTypeTags(String contentTypeId)
705    {
706        ArrayList<TagElement> tags = new ArrayList<>();
707        
708        try
709        {
710            ContentType cType = _cTypeExtensionPoint.getExtension(contentTypeId);
711            if (cType != null)
712            {
713                for (String tag : cType.getTags())
714                {
715                    tags.add(new TagElement(tag));
716                }
717            }
718            else
719            {
720                _logger.error("Can not get tags of unknown content type of id '" + contentTypeId + "'");
721            }
722            
723        }
724        catch (AmetysRepositoryException e)
725        {
726            _logger.error("Can not get tags of content type of id '" + contentTypeId + "'", e);
727        }
728        
729        return new AmetysNodeList(tags);
730    }
731    
732    /**
733     * Get the excerpt of content from the given richtext attribute
734     * @param contentId the id of content
735     * @param attributePath the attribute path of rich text attribute
736     * @param limit the max length for content excerpt 
737     * @return the excerpt
738     */
739    public static String contentExcerpt(String contentId, String attributePath, int limit)
740    {
741        Content content = _ametysObjectResolver.resolveById(contentId);
742        
743        if (content.hasValue(attributePath))
744        {
745            RichText richText = content.getValue(attributePath);
746            return _richTextHelper.richTextToString(richText, limit);
747        }
748     
749        return org.apache.commons.lang3.StringUtils.EMPTY;
750    }
751    
752    /**
753     * Get the HTML view of a content
754     * @param contentId the id of content
755     * @return the content html view wrapped into a &lt;content&gt; tag
756     */
757    public static Node getContentView(String contentId)
758    {
759        return getContentView(contentId, null, 1, null, false);
760    }
761    
762    /**
763     * Get the HTML view of a content with offset on headings
764     * @param contentId the id of content
765     * @param startHeadingsLevel The start level for headings (h1, h2, h3, ...). For example, set to 2 so that the highest level headings are &lt;h2&gt;. Set to 1 for no offset.
766     * @return the content html view wrapped into a &lt;content&gt; tag
767     */
768    public static Node getContentView(String contentId, int startHeadingsLevel)
769    {
770        return getContentView(contentId, null, startHeadingsLevel, null, false);
771    }
772    
773    /**
774     * Get the HTML view of a content
775     * @param contentId the id of content
776     * @param viewName The content view name
777     * @return the content html view wrapped into a &lt;content&gt; tag
778     */
779    public static Node getContentView(String contentId, String viewName)
780    {
781        return getContentView(contentId, viewName, 1, null, false);
782    }
783    
784    /**
785     * Get the HTML view of a content
786     * @param contentId the id of content
787     * @param viewName The content view name
788     * @param startHeadingsLevel The start level for headings (h1, h2, h3, ...). For example, set to 2 so that the highest level headings are &lt;h2&gt;. Set to 1 for no offset.
789     * @return the content html view wrapped into a &lt;content&gt; tag
790     */
791    public static Node getContentView(String contentId, String viewName, int startHeadingsLevel)
792    {
793        return getContentView(contentId, viewName, startHeadingsLevel, null, false);
794    }
795    
796    /**
797     * Get the HTML view of a content with offset on headings
798     * @param contentId the id of content
799     * @param viewName The content view name
800     * @param lang the language. Can be null. Useful only if the content has a multilingual title.
801     * @param startHeadingsLevel The start level for headings (h1, h2, h3, ...). For example, set to 2 so that the highest level headings are &lt;h2&gt;. Set to 1 for no offset.
802     * @param checkReadAccess Set to <code>true</code> to check the read access on content. Be careful, do not use with <code>true</code> on a cacheable element.
803     * @return the content html view wrapped into a &lt;content&gt; tag
804     */
805    public static Node getContentView(String contentId, String viewName, int startHeadingsLevel, String lang, boolean checkReadAccess)
806    {
807        Content content = _ametysObjectResolver.resolveById(contentId);
808        
809        if (checkReadAccess && !_rightManager.currentUserHasReadAccess(content))
810        {
811            _logger.warn("Current user is not authorized to see content of id '" + contentId + "'. AmetysXSLHelper#getContentView will return null element");
812            return null;
813        }
814        
815        Locale requestedLocale = StringUtils.isNotEmpty(lang) ? new Locale(lang) : null;
816        
817        DocumentBuilder builder = null;
818        Source source = null;
819        
820        String uri = _contentHelper.getContentHtmlViewUrl(content, viewName);
821        
822        try
823        {
824            source = _sourceResolver.resolveURI(uri);
825            
826            Source src = null;
827            try
828            {
829                // Wrap HTML view into a <content> tag and move titles hierarchy if needed
830                src = _sourceResolver.resolveURI("plugin:cms://stylesheets/content/content2htmlview.xsl");
831                try (InputStream is = src.getInputStream())
832                {
833                    SAXTransformerFactory tFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
834                    
835                    // Set uri resolver to resolve import
836                    tFactory.setURIResolver(new URIResolver()
837                    {
838                        public javax.xml.transform.Source resolve(String href, String base) throws TransformerException
839                        {
840                            try
841                            {
842                                Source resolvedSource = _sourceResolver.resolveURI(href);
843                                return new StreamSource(resolvedSource.getInputStream());
844                            }
845                            catch (IOException e)
846                            {
847                                throw new TransformerException(e);
848                            }
849                        }
850                    });
851                    
852                    TransformerHandler transformerHandler = tFactory.newTransformerHandler(new StreamSource(is));
853                    Transformer transformer = transformerHandler.getTransformer();
854                    
855                    Properties format = new Properties();
856                    format.put(OutputKeys.METHOD, "xml");
857                    format.put(OutputKeys.ENCODING, "UTF-8");
858                    
859                    transformer.setOutputProperties(format);
860                    
861                    transformer.setParameter("contentId", content.getId());
862                    transformer.setParameter("contentName", content.getName());
863                    transformer.setParameter("contentTitle", content.getTitle(requestedLocale));
864                    if (content.getLanguage() != null)
865                    {
866                        transformer.setParameter("contentLanguage", content.getLanguage());
867                    }
868                    transformer.setParameter("headingLevel", startHeadingsLevel);
869                    
870                    builder  = DocumentBuilderFactory.newInstance().newDocumentBuilder();
871                    Document document = builder.newDocument();
872                    DOMResult result = new DOMResult(document);
873
874                    transformerHandler.setResult(result);
875                    SourceUtil.toSAX(source, transformerHandler);
876                    
877                    return result.getNode();
878                }
879            }
880            finally
881            {
882                _sourceResolver.release(src);
883            }
884        }
885        catch (Exception e)
886        {
887            _logger.error("Fail to get HTML view of content " + contentId, e);
888            return null;
889        }
890        finally
891        {
892            _sourceResolver.release(source);
893        }
894    }
895}