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