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