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