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>.

19 comments:

Unknown said...

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

ahriman 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

Anonymous 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.

Anonymous said...

good article tnx...

Unknown 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.

Unknown 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!

Anonymous 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?

Unknown 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

BalusC 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.*).

Unknown said...

Could somebody tell me how to import com.sun.faces.renderkit.html_basic.MessagesRenderer? I use Websphere Application Server.

Uplift said...

Hi, I know this is an old article, but I am still using JSF 1.2 (can't upgrade to 2.0 just yet). I followed the instructions to the letter, also removing the annotation, and I get the following server error :

Unable to locate tag attribute info for tag attribute escape.

Is there something else I need to set up?

Thanks in advance

Osama Al-Haj Hassan Blog said...

Excellent Job. What if I want the message text, icon to appear from right to left. What changes are needed on the code? Thanks a lot.

Amila Silva said...

Thank you, you save my day on this legacy issue with JSF, Why people still using JSF

Rodrigo Lagos said...

Thanks, I worked without problems with the following JAR:
jsf-api-1.2_12.jar
jsf_facelets-1.1.15.B1.jar
jsf_impl-1.2_12.jar

My faces-config:




com.sun.facelets.FaceletViewHandler

es
es



cl.gov.subdere.seam.CustomPhaseListener



javax.faces.Messages
javax.faces.Messages
cl.gov.subdere.util.EscapableMessagesRenderer




My File Path the class in EJB:

project\trunk\ejb\src\main\java\cl\gov\subdere\util\EscapableMessagesRenderer.java


Ernandes Mourão Júnior said...

My hero!!! Worked like a charm!!!! Thanks a lot!