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