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