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     * Get the attribute of a content at the given path
271     * @param contentId The content id
272     * @param dataPath The data path
273     * @return The value into a "value" node or null if an error occurred
274     */
275    public static NodeList contentAttribute(String contentId, String dataPath)
276    {
277        return contentAttribute(contentId, dataPath, null);
278    }
279    
280    /**
281     * Get the attribute of a content at the given path
282     * @param contentId The content id
283     * @param dataPath The data path
284     * @param lang The language for localized attribute. Can be null for non-localized attribute or to get the values for all existing locales.
285     * @return The value into a "value" node or null if an error occurred
286     */
287    public static NodeList contentAttribute(String contentId, String dataPath, String lang)
288    {
289        try
290        {
291            Content content = _ametysObjectResolver.resolveById(contentId);
292            DataContext context = RepositoryDataContext.newInstance()
293                                             .withObject(content);
294            
295            List<Node> values = _getNodeValues(content.getDataHolder(), dataPath, lang, context);
296            if (values != null)
297            {
298                return new AmetysNodeList(values);
299            }
300        }
301        catch (UnknownAmetysObjectException e)
302        {
303            _logger.error("Can not get attribute at path '" + dataPath + "' on unknown content with id '" + contentId + "'", e);
304        }
305        
306        return null;
307    }
308
309    /**
310     * Get values of an attribute of a model aware data holder at the given path
311     * @param dataHolder the data holder
312     * @param dataPath The data path
313     * @param lang The language for localized attribute. Can be null for non-localized attribute or to get the values for all existing locales.
314     * @return A Node for each values or null if an error occurred
315     */
316    protected static List<Node> _getNodeValues(ModelAwareDataHolder dataHolder, String dataPath, String lang)
317    {
318        return _getNodeValues(dataHolder, dataPath, lang, DataContext.newInstance());
319    }
320    
321    /**
322     * Get values of an attribute of a model aware data holder at the given path
323     * @param dataHolder the data holder
324     * @param dataPath The data path
325     * @param lang The language for localized attribute. Can be null for non-localized attribute or to get the values for all existing locales.
326     * @param dataContext The data context
327     * @return A Node for each values or null if an error occurred
328     */
329    protected static List<Node> _getNodeValues(ModelAwareDataHolder dataHolder, String dataPath, String lang, DataContext dataContext)
330    {
331        
332        if (dataHolder == null)
333        {
334            return null;
335        }
336        
337        try
338        {
339            SAXTransformerFactory saxTransformerFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
340            TransformerHandler th = saxTransformerFactory.newTransformerHandler();
341            
342            DOMResult result = new DOMResult();
343            th.setResult(result);
344            
345            th.startDocument();
346            XMLUtils.startElement(th, "value");
347            
348            Locale locale = StringUtils.isEmpty(lang) ? null : LocaleUtils.toLocale(lang);
349            dataHolder.dataToSAX(th, dataPath, dataContext.cloneContext().withLocale(locale).withEmptyValues(false));
350            
351            XMLUtils.endElement(th, "value");
352            th.endDocument();
353            
354            List<Node> values = new ArrayList<>();
355            
356            // #getChildNodes() returns a NodeList that contains the value(s) saxed
357            // we cannot returns directly this NodeList because saxed values should be wrapped into a <value> tag.
358            NodeList childNodes = result.getNode().getFirstChild().getChildNodes();
359            for (int i = 0; i < childNodes.getLength(); i++)
360            {
361                Node n = childNodes.item(i);
362                values.add(n);
363            }
364            
365            return values;
366        }
367        catch (BadDataPathCardinalityException e)
368        {
369            _logger.error("Unable to get attribute at path '" + dataPath + "'. Path is invalid.", e);
370        }
371        catch (TransformerConfigurationException | SAXException e)
372        {
373            _logger.error("Fail to sax attribute at path '" + dataPath + "'", e);
374        }
375        catch (Exception e)
376        {
377            _logger.error("An error occurred, impossible to get attribute at path '" + dataPath + "'", e);
378        }
379        
380        return null;
381    }
382    
383    /**
384     * Returns a DOM {@link Element} containing {@link Resource}s representing the attachments of the current content.
385     * @return an Element containing the attachments of the current content as {@link Resource}s.
386     */
387    public static Node contentAttachments()
388    {
389        Request request = ContextHelper.getRequest(_context);
390        
391        Content content = (Content) request.getAttribute(Content.class.getName());
392        
393        return contentAttachments(content);
394    }
395    
396    /**
397     * Returns a DOM {@link Element} containing {@link Resource}s representing the attachments of a given content.
398     * @param contentId the content ID.
399     * @return an Element containing the attachments of the given content as {@link Resource}s.
400     */
401    public static Node contentAttachments(String contentId)
402    {
403        Content content = _ametysObjectResolver.resolveById(contentId);
404        
405        return contentAttachments(content);
406    }
407    
408    /**
409     * Returns a DOM {@link Element} containing {@link Resource}s representing the attachments of a given content.
410     * @param content the content.
411     * @return an Element containing the attachments of the given content as {@link Resource}s.
412     */
413    private static Node contentAttachments(Content content)
414    {
415        if (content == null)
416        {
417            return null;
418        }
419        
420        ResourceCollection collection = content.getRootAttachments();
421        
422        return collection != null ? new ResourceCollectionElement(collection) : new EmptyElement("collection");
423    }
424    
425    /**
426     * Set the content of given id in request attribute
427     * @param contentId the id of content
428     */
429    public static void setCurrentContent(String contentId)
430    {
431        setCurrentContent(contentId, null);
432    }
433    
434    /**
435     * Set the content of given id and version in request attribute
436     * @param contentId the id of content
437     * @param versionLabel The version label
438     */
439    public static void setCurrentContent(String contentId, String versionLabel)
440    {
441        Request request = ContextHelper.getRequest(_context);
442        
443        Content content = _ametysObjectResolver.resolveById(contentId);
444        
445        if (StringUtils.isNotEmpty(versionLabel) && content instanceof VersionAwareAmetysObject)
446        {
447            String[] allLabels = ((VersionAwareAmetysObject) content).getAllLabels();
448            if (ArrayUtils.contains(allLabels, versionLabel))
449            {
450                ((VersionAwareAmetysObject) content).switchToLabel(versionLabel);
451            }
452        }
453        
454        request.setAttribute(Content.class.getName(), content);
455        
456    }
457    
458  //*************************
459    // Tag methods
460    //*************************
461    
462    /**
463     * Returns all tags of the current content.
464     * @return a list of tags.
465     */
466    public static NodeList contentTags()
467    {
468        Request request = ContextHelper.getRequest(_context);
469        
470        Content content = (Content) request.getAttribute(Content.class.getName());
471        return _contentTags(content);
472    }
473    
474    /**
475     * Returns all tags of the given content
476     * @param contentId The identifier of the content
477     * @return a list of tags.
478     */
479    public static NodeList contentTags(String contentId)
480    {
481        try
482        {
483            Content content = _ametysObjectResolver.resolveById(contentId);
484            return _contentTags(content);
485        }
486        catch (AmetysRepositoryException e)
487        {
488            _logger.warn("Cannot get tags for content '" + contentId + "'", e);
489        }
490        
491        return null;
492    }
493    
494    /**
495     * Returns all tags of the given content
496     * @param content The content
497     * @return a list of tags.
498     */
499    protected static NodeList _contentTags(Content content)
500    {
501        if (content == null)
502        {
503            return null;
504        }
505        
506        List<TagElement> list = new ArrayList<>();
507        
508        for (String tag : content.getTags())
509        {
510            list.add(new TagElement(tag));
511        }
512        
513        return new AmetysNodeList(list);
514    }
515    
516    /**
517     * Get the name of the parent of a tag.
518     * @param siteName the site name
519     * @param tagName the tag's name
520     * @return The id of parent or empty if not found
521     */
522    public static String tagParent(String siteName, String tagName)
523    {
524        Map<String, Object> contextParameters = new HashMap<>();
525        contextParameters.put("siteName", siteName);
526        
527        Tag tag = _tagProviderExtPt.getTag(tagName, contextParameters);
528        if (tag == null)
529        {
530            return StringUtils.EMPTY;
531        }
532        
533        String parentName = tag.getParentName();
534        return parentName != null ? parentName : StringUtils.EMPTY;
535    }
536    
537    /**
538     * Get the path of a tag. The path contains the tag's parents seprated by '/'.
539     * @param siteName The site name
540     * @param tagName The unique tag's name
541     * @return The tag's path or empty string if tag does not exist
542     */
543    public static String tagPath (String siteName, String tagName)
544    {
545        Map<String, Object> contextParameters = new HashMap<>();
546        contextParameters.put("siteName", siteName);
547        
548        Tag tag = _tagProviderExtPt.getTag(tagName, contextParameters);
549        if (tag == null)
550        {
551            return StringUtils.EMPTY;
552        }
553        
554        String path = tagName;
555        
556        Tag parentTag = tag.getParent();
557        while (parentTag != null)
558        {
559            path = parentTag.getName() + "/" + path;
560            parentTag = parentTag.getParent();
561        }
562
563        return path;
564    }
565    
566    /**
567     * Get the label of a tag
568     * @param siteName the current site
569     * @param tagName the name of the tag
570     * @param lang the lang (if i18n tag)
571     * @return the label of the tag or empty if it cannot be found
572     */
573    public static String tagLabel(String siteName, String tagName, String lang)
574    {
575        Map<String, Object> contextParameters = new HashMap<>();
576        contextParameters.put("siteName", siteName);
577        
578        Tag tag = _tagProviderExtPt.getTag(tagName, contextParameters);
579        return tag == null ? "" : _i18nUtils.translate(tag.getTitle(), lang);
580    }
581    
582    /**
583     * Get the description of a tag
584     * @param siteName the current site
585     * @param tagName the name of the tag
586     * @param lang the lang (if i18n tag)
587     * @return the label of the tag or empty if it cannot be found
588     */
589    public static String tagDescription(String siteName, String tagName, String lang)
590    {
591        Map<String, Object> contextParameters = new HashMap<>();
592        contextParameters.put("siteName", siteName);
593        
594        Tag tag = _tagProviderExtPt.getTag(tagName, contextParameters);
595        return tag == null ? "" : _i18nUtils.translate(tag.getDescription(), lang);
596    }
597    
598    /**
599     * Get the visibility of a tag
600     * @param siteName the current site
601     * @param tagName the name of the tag
602     * @return the lower-cased visibility of the tag ("public" or "private")
603     */
604    public static String tagVisibility(String siteName, String tagName)
605    {
606        Map<String, Object> contextParameters = new HashMap<>();
607        contextParameters.put("siteName", siteName);
608        
609        CMSTag tag = _tagProviderExtPt.getTag(tagName, contextParameters);
610        return tag == null ? "" : tag.getVisibility().toString().toLowerCase();
611    }
612    
613    /**
614     * Get the color (main and text) of a tag
615     * @param siteName the current site
616     * @param tagName the name of the tag
617     * @return the the color (main and text) of a tag
618     */
619    public static MapElement tagColor(String siteName, String tagName)
620    {
621        Map<String, Object> contextParameters = new HashMap<>();
622        contextParameters.put("siteName", siteName);
623        
624        Tag tag = _tagProviderExtPt.getTag(tagName, contextParameters);
625        if (tag != null && tag instanceof ColorableTag colorTag)
626        {
627            String color = colorTag.getColor(true);
628            Map<String, String> map = colorTag.getColorComponent().getColors().get(color);
629            return map != null ? new MapElement("color", map) : null;
630        }
631        
632        return null;
633    }
634    
635    /* ----------------------------- */
636    /*      Content type methods     */
637    /* ----------------------------- */
638    
639    /**
640     * Returns all tags of a content type
641     * @param contentTypeId The id of the content type
642     * @return a list of tags.
643     */
644    public static NodeList contentTypeTags(String contentTypeId)
645    {
646        ArrayList<TagElement> tags = new ArrayList<>();
647        
648        try
649        {
650            ContentType cType = _cTypeExtensionPoint.getExtension(contentTypeId);
651            if (cType != null)
652            {
653                for (String tag : cType.getTags())
654                {
655                    tags.add(new TagElement(tag));
656                }
657            }
658            else
659            {
660                _logger.error("Can not get tags of unknown content type of id '" + contentTypeId + "'");
661            }
662            
663        }
664        catch (AmetysRepositoryException e)
665        {
666            _logger.error("Can not get tags of content type of id '" + contentTypeId + "'", e);
667        }
668        
669        return new AmetysNodeList(tags);
670    }
671    
672    /**
673     * Get the excerpt of content from the given richtext attribute
674     * @param contentId the id of content
675     * @param attributePath the attribute path of rich text attribute
676     * @param limit the max length for content excerpt
677     * @return the excerpt
678     */
679    public static String contentExcerpt(String contentId, String attributePath, int limit)
680    {
681        Content content = _ametysObjectResolver.resolveById(contentId);
682        
683        if (content.hasValue(attributePath))
684        {
685            RichText richText = content.getValue(attributePath);
686            return _richTextHelper.richTextToString(richText, limit);
687        }
688     
689        return org.apache.commons.lang3.StringUtils.EMPTY;
690    }
691    
692    /**
693     * Get the HTML view of a content
694     * @param contentId the id of content
695     * @return the content html view wrapped into a &lt;content&gt; tag
696     */
697    public static Node getContentView(String contentId)
698    {
699        return getContentView(contentId, null, 1, null, false);
700    }
701    
702    /**
703     * Get the HTML view of a content with offset on headings
704     * @param contentId the id of content
705     * @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.
706     * @return the content html view wrapped into a &lt;content&gt; tag
707     */
708    public static Node getContentView(String contentId, int startHeadingsLevel)
709    {
710        return getContentView(contentId, null, startHeadingsLevel, null, false);
711    }
712    
713    /**
714     * Get the HTML view of a content
715     * @param contentId the id of content
716     * @param viewName The content view name
717     * @return the content html view wrapped into a &lt;content&gt; tag
718     */
719    public static Node getContentView(String contentId, String viewName)
720    {
721        return getContentView(contentId, viewName, 1, null, false);
722    }
723    
724    /**
725     * Get the HTML view of a content
726     * @param contentId the id of content
727     * @param viewName The content view name
728     * @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.
729     * @return the content html view wrapped into a &lt;content&gt; tag
730     */
731    public static Node getContentView(String contentId, String viewName, int startHeadingsLevel)
732    {
733        return getContentView(contentId, viewName, startHeadingsLevel, null, false);
734    }
735    
736    /**
737     * Get the HTML view of a content with offset on headings
738     * @param contentId the id of content
739     * @param viewName The content view name
740     * @param lang the language. Can be null. Useful only if the content has a multilingual title.
741     * @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.
742     * @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.
743     * @return the content html view wrapped into a &lt;content&gt; tag
744     */
745    public static Node getContentView(String contentId, String viewName, int startHeadingsLevel, String lang, boolean checkReadAccess)
746    {
747        Content content = _ametysObjectResolver.resolveById(contentId);
748        
749        if (checkReadAccess && !_rightManager.currentUserHasReadAccess(content))
750        {
751            _logger.warn("Current user is not authorized to see content of id '" + contentId + "'. AmetysXSLHelper#getContentView will return null element");
752            return null;
753        }
754        
755        Locale requestedLocale = StringUtils.isNotEmpty(lang) ? LocaleUtils.toLocale(lang) : null;
756        
757        DocumentBuilder builder = null;
758        Source source = null;
759        
760        String uri = _contentHelper.getContentHtmlViewUrl(content, viewName);
761        
762        try
763        {
764            source = _sourceResolver.resolveURI(uri);
765            
766            Source src = null;
767            try
768            {
769                // Wrap HTML view into a <content> tag and move titles hierarchy if needed
770                src = _sourceResolver.resolveURI("plugin:cms://stylesheets/content/content2htmlview.xsl");
771                try (InputStream is = src.getInputStream())
772                {
773                    SAXTransformerFactory tFactory = (SAXTransformerFactory) TransformerFactory.newInstance();
774                    
775                    // Set uri resolver to resolve import
776                    tFactory.setURIResolver(new URIResolver()
777                    {
778                        public javax.xml.transform.Source resolve(String href, String base) throws TransformerException
779                        {
780                            try
781                            {
782                                Source resolvedSource = _sourceResolver.resolveURI(href);
783                                return new StreamSource(resolvedSource.getInputStream());
784                            }
785                            catch (IOException e)
786                            {
787                                throw new TransformerException(e);
788                            }
789                        }
790                    });
791                    
792                    TransformerHandler transformerHandler = tFactory.newTransformerHandler(new StreamSource(is));
793                    Transformer transformer = transformerHandler.getTransformer();
794                    
795                    Properties format = new Properties();
796                    format.put(OutputKeys.METHOD, "xml");
797                    format.put(OutputKeys.ENCODING, "UTF-8");
798                    
799                    transformer.setOutputProperties(format);
800                    
801                    transformer.setParameter("contentId", content.getId());
802                    transformer.setParameter("contentName", content.getName());
803                    transformer.setParameter("contentTitle", content.getTitle(requestedLocale));
804                    if (content.getLanguage() != null)
805                    {
806                        transformer.setParameter("contentLanguage", content.getLanguage());
807                    }
808                    transformer.setParameter("headingLevel", startHeadingsLevel);
809                    
810                    builder  = DocumentBuilderFactory.newInstance().newDocumentBuilder();
811                    Document document = builder.newDocument();
812                    DOMResult result = new DOMResult(document);
813
814                    transformerHandler.setResult(result);
815                    SourceUtil.toSAX(source, transformerHandler);
816                    
817                    return result.getNode();
818                }
819            }
820            finally
821            {
822                _sourceResolver.release(src);
823            }
824        }
825        catch (Exception e)
826        {
827            _logger.error("Fail to get HTML view of content " + contentId, e);
828            return null;
829        }
830        finally
831        {
832            _sourceResolver.release(source);
833        }
834    }
835}