Thursday, July 29, 2010

Using HTML in JSF messages

A common question which keeps returning is "How to display HTML in <h:messages>?". One would logically think to add an escape="false" attribute to the component, like as you would do in a <h:outputText>. Unfortunately, this is not possible in the standard JSF implementation. The component and the renderer does officially not support this attribute. The <h:outputText> and <f:selectItem> are as far the only which supports the escape attribute. Your best bet is to homegrow a renderer which handles this.

First some background information: JSF by default uses ResponseWriter#writeText() to write the tag body, which escapes HTML by default. We'd like to let it use ResponseWriter#write() instead like as with <h:outputText escape="false" />.

So, we'd like to extend the MessageRenderer of the standard JSF implementation and override the encodeEnd() method accordingly. But since the MessageRenderer#encodeEnd() contains pretty a lot of code (~180 lines) which we prefer not to copypaste to just change one or two lines after all, it's a better idea to replace the ResponseWriter with a custom implementation with help of ResponseWriterWrapper wherein the writeText() method is been overriden to handle the escaping.

So, I ended up with this:

package com.example;

import java.io.IOException;

import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.context.ResponseWriterWrapper;
import javax.faces.render.FacesRenderer;

import com.sun.faces.renderkit.html_basic.MessagesRenderer;

@FacesRenderer(componentFamily="javax.faces.Messages", rendererType="javax.faces.Messages")
public class EscapableMessagesRenderer extends MessagesRenderer {

    @Override
    public void encodeEnd(FacesContext context, UIComponent component) throws IOException {
        final ResponseWriter originalResponseWriter = context.getResponseWriter();
        context.setResponseWriter(new ResponseWriterWrapper() {

            @Override
            public ResponseWriter getWrapped() {
                return originalResponseWriter;
            }

            @Override
            public void writeText(Object text, UIComponent component, String property)
                throws IOException
            {
                String string = String.valueOf(text);
                String escape = (String) component.getAttributes().get("escape");
                if (escape != null && !Boolean.valueOf(escape)) {
                    super.write(string);
                } else {
                    super.writeText(string, component, property);
                }
            }
        });

        super.encodeEnd(context, component);
        context.setResponseWriter(originalResponseWriter); // Restore original writer.
    }
}

But, in spite of the @FacesRenderer annotation, it get overriden by the default MessagesRenderer implementation. Since I suspect a bug here, I reported issue 1748. To get it to work anyway, we have to fall back to the faces-config.xml:


    <render-kit>
        <renderer>
            <component-family>javax.faces.Messages</component-family>
            <renderer-type>javax.faces.Messages</renderer-type>
            <renderer-class>com.example.EscapableMessagesRenderer</renderer-class>
        </renderer>
    </render-kit>

And it works! :) Use it as follows:


<h:messages escape="false" />

To do the same for <h:message>, just copy the above and replace anywhere "Messages" appears in the code (component family, renderer type and class names) by "Message".

The above is written with JSF 2.0 in mind, but it should also just work in JSF 1.2, you only have to remove the @FacesRenderer annotation. It will not work in JSF 1.1 or older since there's no ResponseWriter#writeText() method which takes an UIComponent as argument.

Update: a ready to use solution is available in OmniFaces as <o:messages>.

14 comments:

karazy said...

Indeed. I also stumbled upon this question. Thx for this solution!

genocyber said...

Hello!
Can i translate some of your posts into Russian language? Sure with all needed copyrights. Please email me about this:
ahriman@tpu.ru

McDowell said...

Doing this is probably unwise because of HTML/code injection issues - unless you exercise a strong degree of control over your application and its dependencies (including all 3rd party libraries and the JEE platform implementation). You don't know what data will be added to the message queue.

If you replaced the h:message (no S) renderer instead, you'd be on safer ground because each component targets something you've specified in the view.

I'm not saying "don't do this", but I think the post is incomplete without a follow-up describing all the bad things you could do with it.

BalusC said...

Right, as long as you don't redisplay user-controlled input in messages, this is fine.

velmurugan-pousel said...

good article tnx...

Bill Korb said...

I tried this, too, but the JSP compiler then fails saying that the "escape" attribute is not valid for the h:messages tag. I suppose I need to hack the TLD, too? That seems a bit kludgy.

BalusC said...

@Bill: I used Tomcat's one. You can also use <f:attribute name="escape" value="true"/>

Majid said...

thanks for saving me a lot of time with this neat solution.

Pablo Baron said...

what problems can cuase this i read mcdowells post and i got confused if its a good solution to apply or not, BalusC, can you please explain what might cause in my app if i use this class, cause i tried it for H:message, as u said i replaced it and it works perfectly but i want to know waht can happen..what messages can not be redisplayed???... thx

Pablo Baron said...

can i do the same for the rich:message? which package is it??

cogaritis said...

Just what I needed... Thanks man!

Bernhard said...

This is not working for me! I have added the class, and changed faces-config.xml to look as follows:

<?xml version="1.0" encoding="UTF-8"?>

<faces-config
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facesconfig_2_0.xsd"
version="2.0">

<render-kit>
<renderer>
<component-family>javax.faces.Messages</component-family>
<renderer-type>javax.faces.Messages</renderer-type>
<renderer-class>microworks.voyagernetz.bazaar.EscapableMessagesRenderer</renderer-class>
</renderer>
</render-kit>

</faces-config>


The contents of the <h:messages escape="false"> tag is still escaped.

I use Glassfish 3.1.2. Any ideas?

Neil Stevens said...

Myfaces 2.1, HtmlMessageRendererBase, calls writer.writeText(detail, null);

so you need to override writeText(text, property)

not writeText(text, component, property)

Regards,

Neil

Bauke Scholtz said...

@Neil: indeed, that depends on the JSF implementation you're currently using. The article also explicitly mentions "the standard JSF implementation", referring to Mojarra (com.sun.faces.*).