Patterns in Software

How To Create Email From JSF Templates

By , 1 October 2012

How To Create Email From JSF Templates

Hi everybody. In this blog I'm going to share a little trick for creating emails using JSF Facelets templates. The concept is actually independent of JSF and could be used for any servlet-based technology.

Right, so the basic idea is to temporarily trick the ServletResponse object into writing all the content to an in-memory buffer, rather than streaming it to the client's browser. We then manually invoke the JSF render phase for the particular view using the special ServletResponse and voila!, the captured output can be put into an email.

Let's have a look at some of the code.

How To Create Email From JSF Templates

First, the heavy lifting. Here is the method which sets up the context correctly for JSF to create and render a template without messing up the current view. This method also lets you pass in a map of parameters that get put in the request scope while the template is rendered.

    /**
     * Render a template in memory and return the content as a string. The
     * request parameter 'emailClient' is set to true during rendering. This
     * method relies on a FacesContext for Facelets templating so it only
     * works when the app is deployed.
     */
    public static String capture(String template, Map params) {
        
        // setup a response catcher
        FacesContext faces = FacesContext.getCurrentInstance();
        ExternalContext context = faces.getExternalContext();
        ServletRequest request = (ServletRequest) faces.getExternalContext().
                getRequest();
        HttpServletResponse response = (HttpServletResponse) 
                context.getResponse();
        ResponseCatcher catcher = new ResponseCatcher(response);
        
        // hack the request state
        UIViewRoot oldView = faces.getViewRoot();
        Map oldAttributes = null;
        if (params != null) {
            oldAttributes = new HashMap(params.size() * 2); // with buffer
            for (String key : (Set<String>) params.keySet()) {
                oldAttributes.put(key, request.getAttribute(key));
                request.setAttribute(key, params.get(key));
            }
        }
        request.setAttribute("emailClient", true);
        context.setResponse(catcher);
        
        try {
            // build a JSF view for the template and render it
            ViewHandler views = faces.getApplication().getViewHandler();
            UIViewRoot view = views.createView(faces, template);
            faces.setViewRoot(view);
            views.getViewDeclarationLanguage(faces, template).
                    buildView(faces, view);
            views.renderView(faces, view);
        } catch (IOException ioe) {
            String msg = "Failed to render " + template;
            faces.addMessage(null, new FacesMessage(
                    FacesMessage.SEVERITY_ERROR, msg, msg));
            return null;
        } finally {
        
            // restore the request state
            if (oldAttributes != null) {
                for (String key : (Set<String>) oldAttributes.keySet()) {
                    request.setAttribute(key, oldAttributes.get(key));
                }
            }
            request.setAttribute("emailClient", null);
            context.setResponse(response);
            faces.setViewRoot(oldView);
        }
        return catcher.toString();
    }

The important lines are shown in bold above. Now that you have your view rendered as a string you can easily turn it into an email.

The ResponseCatcher which actually captures the output of the rendering is a pretty dumb object. It just wraps the existing response object, but intercepts getWriter() method to return a local in-memory CharArrayWriter:

/**
 * This is a response wrapper which saves all of the output to a char array,
 * so it can be retrieved as a string afterwards with the toString() method.
 * We only support capturing text output currently.
 */
public class ResponseCatcher implements HttpServletResponse {

    /** the backing output stream for text content */
    CharArrayWriter output;

    /** a writer for the servlet to use */
    PrintWriter writer;
    
    /** a real response object to pass tricky methods to */
    HttpServletResponse response;
        
    /** 
     * Create the response wrapper.
     */
    public ResponseCatcher(HttpServletResponse response) {
        this.response = response;
        output = new CharArrayWriter();
        writer = new PrintWriter(output, true);
    }
    
    /**
     * Return a print writer so it can be used by the servlet. The print
     * writer is used for text output.
     */
    public PrintWriter getWriter() {
        return writer;
    }

    public void flushBuffer() throws IOException {
        writer.flush();
    }
    
    public boolean isCommitted() {
        return false;
    }
    
    public boolean containsHeader(String arg0) {
        return false;
    }

    /* wrapped methods */
    public String encodeURL(String arg0) {
        return response.encodeURL(arg0);
    }

    public String encodeRedirectURL(String arg0) {
        return response.encodeRedirectURL(arg0);
    }

    public String encodeUrl(String arg0) {
        return response.encodeUrl(arg0);
    }

    public String encodeRedirectUrl(String arg0) {
        return response.encodeRedirectUrl(arg0);
    }

    public String getCharacterEncoding() {
        return response.getCharacterEncoding();
    }

    public String getContentType() {
        return response.getContentType();
    }
    
    public int getBufferSize() {
        return response.getBufferSize();
    }
    
    public Locale getLocale() {
        return response.getLocale();
    }
    
    public void sendError(int arg0, String arg1) throws IOException {
        response.sendError(arg0, arg1);
    }
    
    public void sendError(int arg0) throws IOException {
        response.sendError(arg0);
    }
    
    public void sendRedirect(String arg0) throws IOException {
        response.sendRedirect(arg0);
    }
    
    @Override
    public void addCookie(Cookie arg0) {
        response.addCookie(arg0);
    }
    
    @Override
    public void setDateHeader(String arg0, long arg1) {
        response.setDateHeader(arg0, arg1);
    }
    
    @Override
    public void addDateHeader(String arg0, long arg1) {
        response.addDateHeader(arg0, arg1);
    }
    
    @Override
    public void setHeader(String arg0, String arg1) {
        response.setHeader(arg0, arg1);
    }
    
    @Override
    public void addHeader(String arg0, String arg1) {
        response.addHeader(arg0, arg1);
    }
    
    @Override
    public void setIntHeader(String arg0, int arg1) {
        response.setIntHeader(arg0, arg1);
    }
    
    @Override
    public void addIntHeader(String arg0, int arg1) {
        response.addIntHeader(arg0, arg1);
    }
    
    @Override
    public void setContentLength(int arg0) {
        response.setContentLength(arg0);
    }
    
    @Override
    public void setContentType(String arg0) {
        response.setContentType(arg0);
    }
    
    /* null ops */
    @Override public void setStatus(int arg0) {}
    @Override public void setStatus(int arg0, String arg1) {}
    @Override public void setBufferSize(int arg0) {}
    @Override public void resetBuffer() {}
    @Override public void reset() {}
    @Override public void setLocale(Locale arg0) {}
    
    
    /* unsupported methods */
    public ServletOutputStream getOutputStream() throws IOException {
        throw new UnsupportedOperationException("Not supported yet.");
    }
    
    /** 
     * Return the captured content.
     */
    @Override
    public String toString() {
        return output.toString();
    }
}

What about JSF 1.2?

If you're running JSF 1.2, you can still use the same ResponseCatcher but the capture() method looks a little different. I'm currently only using this with Facelets, but I used to do something similar with the JSP ViewHandler so I'd expect it to work okay with JSP too.

Here is the JSF 1.2 version:

/**
 * Render a view in memory and return the content as a string. The
 * request parameter 'emailClient' is set to true during rendering.
 */
public String capture(String template) {
       
    // initial values
    FacesContext faces = FacesContext.getCurrentInstance();
    ExternalContext context = faces.getExternalContext();
    HttpServletResponse response = (HttpServletResponse) 
            context.getResponse();
    ResponseCatcher catcher = new ResponseCatcher(response);
    ViewHandler views = faces.getApplication().getViewHandler();
        
    // render the message
    try {
        context.setResponse(catcher);
        context.getRequestMap().put("emailClient", true);
        views.renderView(faces, views.createView(faces, template));
        context.getRequestMap().remove("emailClient");
        context.setResponse(response);
    } catch (IOException ioe) {
        String msg = "Failed to render email internally";
        faces.addMessage(null, new FacesMessage(
                FacesMessage.SEVERITY_ERROR, msg, msg));
        return null;
    }

That's it. Unfortunately it doesn't get much simpler (although one developer has had success replacing the JSF ResponseWriter instead of the ServletResponse). If this method doesn't work for you, head on over and try out his code.

About Roger Keays

Roger is an active member of the JSF 2 Expert Group and is happy to be a contributor to the Java Community. He has been writing software since the age of 8 and his other interests include languages, science, travel and running.

Comment posted by: Pete Muir, 6 years ago

Hey Roger,

Did you know we have an email library in Seam which allows you to do this, and much more (has special tags for setting from, to, subject, headers, attachments and body).

Though, we're not using your rendering method - I didn't know you could make JSF do that, currently we do it through Facelets directly, but yours is *much* neater.

Comment posted by: , 6 years ago

Hi Pete,

I forgot to mention the Seam method... sorry! Here is this link to the Seam email docs for other readers:

http://docs.jboss.org/seam/2.1.0.CR1/reference/en-US/html/mail.html

I know Seam has a component set for creating PDFs too. FWIW, you can also use the captureView() method described above in conjunction with the Flying Saucer xhtml renderer to create pdfs from JSF views. There is a pretty detailed tutorial on java.net describing this:

Combine JSF Facelets and the Flying Saucer XHTML Renderer

I'd like to create a JSF component which renders its children to a pdf, but last time I tried Facelets did not support binary output from components (that was probably a year ago now though).

Comment posted by: , 5 years ago

Woah, the captureView() method returns an empty string when you use RichFaces if faces.responseComplete() has been called. Their ViewHandler doesn't do anything in this case, which makes sense I guess, but caught me out.

Comment posted by: Zoltan, Gyurasits, 5 years ago

Hi Roger!

It's wonderful solutions!
I need this, but not work for me :(

I get only content outside of view tag. Can you help a little?

This is my example:

www . gyurasits . hu / GenerateJSFView . rar

I use the SUN reference JSF implementation.

Thanks a lot!

Best Regards!
Zoltan Gyurasits

Comment posted by: SK, 2 years ago

Hi Roger,

I've used this solution in 2 instances. The first works fine, while in the second the content was written into a string as well as displayed onto the client's browser.

In the 1st instance, it was a simple single email. In the 2nd instance, I have a loop that renders and sends the emails.

Any idea what might have gone wrong?

Comment posted by: , 2 years ago

SK, I use this in a loop and it works okay (JSF 2.0). Maybe something wrong with your loop?

Comment posted by: SK, 2 years ago

Thanks for your reply.I found where the real cause of the problem. I'm using seam, and my page.xml instructions was not run.

Thanks, again.

Comment posted by: A Ghost, 2 years ago

This will create the HashMap; but will still cause a resize when the usage reaches 75%...

HashMap(params.size())

Like it seems better to go for params.size + 30%.

Comment posted by: , 2 years ago

Thanks Ghost. I put in * 2 to keep things simple. I'm sure there's a more efficient way to clone a HashMap though.

Comment posted by: Masilakhe, 9 months ago

working

Comment posted by: dfgd, 2 months ago

dfvdfgvdfggdg

Comment posted by: asd, 1 month ago

just call the page via jax-rs, capture output and put it into e-mail....

Comment posted by: Guest, 4 weeks ago
Hi,

"This method relies on a FacesContext for Facelets templating so it only
works when the app is deployed." How can I do without context?

Add a comment

Please visit http://www.NinthAvenue.com.au/how-to-create-email-from-jsf-templates to add your comments.

Join The Mailing List

Subscribe to our mailing list for the latest news and announcements.

Follow Ninth Avenue

Website Updates