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