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