Render and capture the output of a JSP as a String

Now we can start looking at the Java classes.  First, let’s knock the common framework classes out of the way.

/src/main/java/com/technologicaloddity/capturejsp/util/TechOddViewResolver.java

package com.technologicaloddity.capturejsp.util;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;

@Component
public class TechOddViewResolver extends UrlBasedViewResolver {

    @Override
    protected Class<JstlView> getViewClass() {
        return JstlView.class;
    }

    @Override
    protected String getSuffix() {
        return ".jsp";
    }

    @Override
    protected String getPrefix() {
        return "/WEB-INF/jsp/";
    }

    public String urlForView(String view) {
        String result = view;
        if(getPrefix() != null) {
            result = getPrefix() + result;
        }
        if(getSuffix() != null) {
            result = result + getSuffix();
        }
        return result;
    }
}

Most Spring MVC projects need a ViewResolver, which maps view names to internal URLs.  This class defines a ViewResolver based on Spring’s standard UrlBasedViewResolver.  Given view name “foo”, it will return URL “/WEB-INF/jsp/foo.jsp” by adding the prefix and suffix.  We’ve also added an additional function, getUrlForView on line 26, which returns the URL to us.

We’ll use TechOddViewResolver both as our default resolver (to resolve index.jsp) and as our capture resolver (to resolve captureme.jsp).  Since it is annotated with @Component, Spring will instantiate this class automatically when it does the component-scan we requested in dispatch-servlet.xml, line 13.

(Note for Spring gurus: The only reason I created this class is because I couldn’t figure out how to make UrlBasedViewResolver give me the URL, since it is private. If you know how to do this, please comment!)

Next, we look at our MessageSource, that provides different messages base on the locale.

/src/main/java/com/technologicaloddity/capturejsp/util/TechOddMessageSource.java

package com.technologicaloddity.capturejsp.util;

import javax.annotation.PostConstruct;

import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.stereotype.Component;

@Component("messageSource")
public class TechOddMessageSource extends ReloadableResourceBundleMessageSource {

    @PostConstruct
    public void init() {
        setBasename("classpath:/messages/messages");
    }

}

Line 9: We’ve defined our TechOddMessageSource as a component here so that Spring will create an instance automatically on the component-scan.  We did, however, give this bean a specific name of “messageSource”.  Spring expects a bean named “messageSource” to be present.  If there isn’t one, Spring will create a default bean called “messageSource”, which won’t point where we want it to, and it won’t find our message files.

Lines 12-15: Here we are using a @PostConstruct to set the basename of the MessageSource.  Basename is just a location that points to the message files that we want to use.  MessageSources automatically look for basename + “.properties”, so we’ll point it at our message files in messages/messages*.properties.  The @PostConstruct annotation tells Spring to call this method after all the bean’s members have been resolved and an object has been created (much like Spring 2.x’s InitializingBean interface).  Thus, we will set the basename when Spring creates this bean in the component-scan.

/src/main/java/com/technologicaloddity/capturejsp/util/JspLocaleResolver.java

package com.technologicaloddity.capturejsp.util;

import java.util.Locale;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.web.servlet.LocaleResolver;

public class JspLocaleResolver implements LocaleResolver {

    private static final String JSP_LOCALE = "com.technologicaloddity.capturejsp.LOCALE";

    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        Locale locale = (Locale)request.getAttribute(JSP_LOCALE);
        if(null==locale) {
            locale = Locale.getDefault();
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
        if(locale == null) {
            locale = Locale.getDefault();
        }
        response.setLocale(locale);
        request.setAttribute(JSP_LOCALE, locale);

    }

}

The JspLocaleResolver is used to replace existing LocaleResolvers in the rendered JSP.  Why?  Spring likes to do a lot of tricky things in the LocaleResolver, so we use our own to keep it simple.  In reality though, you don’t need to think about this class much, as the SwallowingJspRenderer will handle it for you (which we will see a bit later).

Line 13: This defines the name of the request attribute where our locale will be stored on the request.  Feel free to change this to whatever you want.

Line 16: resolveLocale looks for the attribute name we defined on line 13.  If it finds it, that value is our Locale.  If it doesn’t, it uses the system default Locale.

Line 25: setLocale adds the proper request attribute (from line 13) to the request, and sets the Locale of the response.

/src/main/java/com/technologicaloddity/capturejsp/util/MockIncludedHttpServletRequest.java

package com.technologicaloddity.capturejsp.util;

import javax.servlet.DispatcherType;

import org.springframework.mock.web.MockHttpServletRequest;

public class MockIncludedHttpServletRequest extends MockHttpServletRequest {

    public MockIncludedHttpServletRequest() {
        super();
    }

    public DispatcherType getDispatcherType() {
        return DispatcherType.INCLUDE;
    }

    public boolean isAsyncSupported() {
        return false;
    }
}

This is an extension of Spring-test’s MockHttpServletRequest.  It is here to provide compatibility with Servlets 3.x, if you are using 3.x (in Tomcat 7 for example).  It adds a couple of new methods that are defined by the Servlets 3.0 interface.  Since they aren’t marked @Override, they also don’t break Servlets 2.x.  Therefore, this Mock request works with both Servlets 3.x and Servlets 2.x.  In theory, though, if you are using Servlets 2.x, you could have just used MockHttpServletRequest instead.

Line 16: For Servlets 3.0, we’ll always use DispatchType.INCLUDE, as that is how we fool the Dispatcher into rendering our caputreme.jsp page (we’ll see how in the next class).

Line 20: For Servlets 3.0, we don’t want to use an asynchronous request/response cycle, so return false.

The next class, SwallowingJspRenderer, is the real guts of this tutorial.  This renderer has the job of taking a view name, a locale, and a model map (objects keyed on a String in a Map) and rendering it as a JSP, returning a String.  There is a bit of magic here, so we’ll discuss this one thoroughly.

/src/main/java/com/technologicaloddity/capturejsp/util/SwallowingJspRenderer.java

package com.technologicaloddity.capturejsp.util;

import java.io.IOException;
import java.io.StringWriter;
import java.util.Locale;
import java.util.Map;

import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.jstl.core.Config;
import javax.servlet.jsp.jstl.fmt.LocalizationContext;

import org.directwebremoting.util.SwallowingHttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.ui.Model;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.LocaleResolver;

@Component
public class SwallowingJspRenderer implements ServletContextAware {

    @Autowired
    private TechOddViewResolver viewResolver;

    private ServletContext servletContext;

    public String render(String viewName, Model model) throws IOException  {
        return render(viewName, model.asMap(), null);
    }

    public String render(String viewName, Model model, Locale locale) throws IOException  {
        return render(viewName, model.asMap(), locale);
    }

    public String render(String viewName, Map<String,Object> modelMap) throws IOException  {
        return render(viewName, modelMap, null);
    }

    public String render(String viewName, Map<String,Object> modelMap, Locale locale) throws IOException {
        String result = null;

        if(locale == null) {
            locale = Locale.getDefault();
        }

        // These String objects are used to capture the output
        // of SwallowingHttpServletResponse
        StringWriter sout = new StringWriter();
        StringBuffer sbuffer = sout.getBuffer();

        // Set up a fake request and response.  We need the mock response
        // so that we can create the Swallowing response
        HttpServletRequest request = new MockIncludedHttpServletRequest();
        HttpServletResponse response = new MockHttpServletResponse();
        HttpServletResponse swallowingResponse = new SwallowingHttpServletResponse(response, sout, "UTF-8");

        // Use our own LocaleResolver here, or Spring will try to meddle with it
        LocaleResolver localeResolver = new JspLocaleResolver();
        localeResolver.setLocale(request, swallowingResponse, locale);

        try {
            //Add the modelMap to the request as attributes
            addModelAsRequestAttributes(request, modelMap);

            // Push our LocaleResolver into the request
            request.setAttribute(DispatcherServlet.LOCALE_RESOLVER_ATTRIBUTE, localeResolver);            

            // Push our Locale into the request
            LocalizationContext localizationContext = new LocalizationContext(null, locale);
            request.setAttribute(Config.FMT_LOCALIZATION_CONTEXT+".request", localizationContext);
            request.setAttribute(Config.FMT_LOCALE, locale);

            // Make sure we are using UTF-8 for the rendered JSP
            swallowingResponse.setContentType("text/html; charset=utf-8");

            // "include" the file (but not really an include) with the dispatcher
            // The resulting rendering will come out in swallowing response,
            // via sbuffer
            RequestDispatcher dispatcher = servletContext.getRequestDispatcher(viewResolver.urlForView(viewName));

            dispatcher.include(request, swallowingResponse);

            result = sbuffer.toString();
        } catch(Exception e) {
            throw new IOException(e);
        }

        return result;
    }

    /*
     * Moves the items in the map to be request.attributes
     */
    private void addModelAsRequestAttributes(ServletRequest request, Map<String,Object> modelMap) {
        if(modelMap != null && request != null) {
            for (Map.Entry<String, Object> entry : modelMap.entrySet()) {
                String modelName = entry.getKey();
                Object modelValue = entry.getValue();
                if(modelValue != null) {
                    request.setAttribute(modelName, modelValue);
                } else {
                    request.removeAttribute(modelName);
                }
            }
        }
    }

    @Override
    public void setServletContext(ServletContext servletContext) {
        this.servletContext = servletContext;
    }
}

Line 24: Like most of the other utility classes we have seen, this one is annotated with @Component, so that Spring will create an instance during the component-scan.

Lines 25,30,115-117: We implement Spring’s ServletContextAware interface, which will give us a chance to the grab the ServletContext when the renderer is instantiated.  We’ll need the ServletContext on line 85 to get the RequestDispatcher.

Lines 27-28: We’ll need the TechOddViewResolver to render the JSP, so we autowire it in here.  Note that I had to use the full class name (as opposed to “private ViewResolver viewResolver”) so that we have access to getUrlForView in TechOddViewResolver.

Lines 32,36,40: These resolve functions are just helper function that pass through to the real resolve function on line 44.

Line 44: This resolve function is the real workhorse of the whole system.  It’s purpose is to take the name of a view and render it as a JSP, returning the String that is the major goal of this tutorial.

Resolve takes the parameters “viewName” of the JSP to render (before resolution, so “foo” instead of “foo.jsp”), “modelMap”, a Map of Strings and Objects that will become the Model for the rendered JSP, and “locale”, which will be the Locale that we use to render messages, numbers, and Locale-aware things in the JSP.

Lines 47-49: If we don’t pass in a Locale, we use the system default Locale instead.

Lines 53-54: These lines set up a StringWriter and an associated StringBuffer that we will use with the SwallowingHttpServletResponse on line 60.  SwallowingHttpServletResponse will write all the output of a response to this StringWriter.

Line 58: Here we define the c.t.capturejsp.util.MockIncludedHttpServletRequest that we previously discussed.  “Mock” means “fake”, so this isn’t a real request in the normal sense, but it is good enough to render a JSP.  We use Mocks here so that we don’t need the real request and response from an end-user, as we may not have access to it (depending on what your application is doing).  Remember that we needed to this special extension of Spring-test’s normal MockHttpSerlvetRequest due to changes in Servlets 3.x (see discussion above).  If you are sure you will not use Servlets 3.x, you could change this to a normal MockHttpServletRequest.

Line 59: Similar to the request, we define a MockHttpServletResponse to act as our response.  We can use Spring-test’s normal Mock response here, since we don’t have conflicts with Servlets 3.x.  In reality, we only need this request to pass to SwallowingHttpServletResponse, since it requires a backing response of some kind in it’s constructor.

Line 60: We use DWR’s handy SwallowingHttpServletResponse here to set up a response that will capture output to the StringWriter/Buffer we defined on line 53.  We pass in our Mock response and our StringWriter to the constructor, and define the character encoding as UTF8.  We’ll pass this swallowing response to the RequestDisatcher to render our JSP.

Lines 63-64: Here we create an instance of your JspLocaleResolver, and use it to attach the requested Locale to the request and the swallowing response.  This will prevent Spring from trying to use one its own resolvers, which doesn’t work for us in this case.

Line 68: Here we move all the Object in the model map to be attributes of the mock request using the method defined on line 99.  This is how both JSTL and Spring expect to be passed Objects to a page.

Line 71: We are going to push our JspLocaleResolver on the request as the LOCALE_RESOLVER_ATTRIBUTE.  Again, this keeps Spring from try to use its own LocaleResolver, which we may or may not want for the rendered JSP.

Lines 74-76: Here we are creating a “LocalizationContext” onto FMT_LOCALIZATION_CONTEXT, which is what the JSTL fmt taglib package uses to resolve fmt:formatNumber requests.  We also push our Locale onto the request’s FMT_LOCALE for good measure.  Note that the “.request” part on line 75 is the scope of the attribute (as opposed to “.page” or “.application” scope).  This part is a bit kludgy, I admit, but it works for what we need to do.

Line 79: One more place to make sure we are using UTF-8 on the response.

Line 84: In order to get an application server to process a URL request, you need a RequestDispatcher.  The RequestDispatcher can determine if the URL is bound for a servlet, or an HTML page, or a JSP page, and how to process it.  For example, on my Tomcat 7 system, the RequestDispatcher will send our JSP request off to the Jasper 2 JSP engine (part of Tomcat) to be rendered.  How do we get access to the RequestDispatcher?  That’s what we need the ServletContext for (remember line 25’s “ServletContextAware” interface).  The ServletContext can point us to the RequestDispatcher to use for a given URL.  So, how do we get the URL?  We can pass the view name that was passed to the render method (our view name will be “capture/captureme” as we shall see in a minute) to the getUrlForView method of our TechOddViewResolver.  Bingo!  We have a RequestDispatcher for our view!

Line 86: If you look at the javadocs for RequestDispatcher, you’ll see that it only provides two functions: forward and include.  “Forward” ignores the current response and replaces it with the given URL.  “Include” includes the URL in the current response.  It may seem odd that we are using RequestDispatcher.include here, instead of a forward.  Don’t worry, although this is the method by jsp:include, WE ARE NOT PERFORMING A JSP:INCLUDE.  This “include” simple writes the rendered contents of URL into the response that is passed in to the include method.  In our case, that response is the SwallowingHttpServletReponse, not the end-user response.  So, our swallowing response will ONLY have the rendered JSP in it.

Why not use a “forward” on line 86?  It seems logical, but there is a problem.  If you use forward, the request (or mock request in our case) goes round-trip through the server.  Some modern application servers (notably Tomcat 7) require a request to by “privileged” to go round-trip in such a way.  “Privileged” can mean a few things, but usually it means the request that is passed to forward must have originated from an end-user and gone through Tomcat.  This is obviously not true for our mock request.  What is returned by forward on these application servers is silence… no exception, no rendered JSP, no clue what happened.  Therefore, we use “include” instead for maximum compatibility.

When the include returns, our swallowing response will have the rendered JSP in it.

Line 88:  Since our swallowing response writes the response out to our StringWriter (from line 53), and our StringBuffer (from line 54) is attached to the StringWriter, we can get the contents of the response (that is, the rendered JSP) with buffer.toString().

Line 93: Return the string we got from the response.  That String represents the rendered JSP and can be used in any way you would use any other String.

There is only one more thing left to do: we need a @Controller that will handle a browser request to /index.html and show our test page.

/src/main/java/com/technologicaloddity/capturejsp/controller/IndexController.java

package com.technologicaloddity.capturejsp.controller;

import java.util.*;

import javax.servlet.http.HttpServletResponse;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import com.technologicaloddity.capturejsp.util.SwallowingJspRenderer;

@Controller
public class IndexController {

    @Autowired
    private SwallowingJspRenderer jspRenderer;

    private static final Logger log = Logger.getLogger(IndexController.class);

    @RequestMapping(value="/index.html")
    public String handleRequest(Model model,HttpServletResponse response, @RequestParam(value="locale", required=false) String requestedLocale) {

        // Set the output of index.jsp to UTF-8, so we can see things like the Euro symbol
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");

        // Add a message to index.jsp's model
        model.addAttribute("message", "Hello Web World!");

        try {
            Locale locale = Locale.getDefault();

            // If the user passes in a ?locale=foo line, parse it and
            // use that as a locale
            if(requestedLocale != null && StringUtils.hasText(requestedLocale)) {
                if(requestedLocale.contains("_")) {
                    String[] parts = requestedLocale.split("_",2);
                    locale = new Locale(parts[0], parts[1]);
                } else {
                    locale = new Locale(requestedLocale);
                }
            }

            // build the map for the object that go to the JSP we want to render
            // that is to captureme.jsp
            Map<String,Object> jspMap = new HashMap<String,Object>();
            jspMap.put("localeUsed", locale);
            jspMap.put("jspMessage", "This is the value of jsp message");
            jspMap.put("costMessage", 4567.89);
            String jspOutput = jspRenderer.render("capture/captureme", jspMap, locale);

            // add the String that represents the JSP to the model of the index page
            model.addAttribute("jspoutput", jspOutput);

        } catch(Exception e) {
            log.error(e);
        }
        return "index";
    }

}

Line 17: We’ll be using Spring’s @Controller stereotype so that this class can serve end-user requests.

Lines 20-21: Here we autowire in our SwallowingJspRenderer.

Line 25: We want method “handleRequest” to respond to url “index.html”.

Line 26: In Spring 3 fashion, we ask for the Model (as in ModelAndView) and the response object.  Spring is smart enough to fill these in for us automatically.  We have also defined one optional @RequestParmeter called “locale”.  This allows us to use the same handleRequest method for various Locale settings for our rendered JSP.  We will return a String, which is the name of the view for the request (in this case “index”, which the ViewResolver automatically changes to /WEB-INF/jsp/index.jsp before the response is rendered).

Lines 29-30: We need to make sure that index.jsp is also rendered in UTF-8.  Since our rendered JSP will just be a String, its character encoding is not available to index.jsp, so we define it here specifically.

Line 33: We add a silly message to index.jsp’s model, just to show that there are two separate models: one for index.jsp (that is, “model” in this method) and one for captureme.jsp (that is, “jspMap” later in this function).   This “message” is printed out at index.jsp, line 12.

Lines 36-47: Here we are setting up the Locale we will send to the SwallowingJspRenderer.  We first set it to the default.  Then we look to see if a parameter “?locale=foo” was sent to the page.  If so, we use the value of that parameter as the Locale instead.

Line 51: The HashMap jspMap created here will be passed as the modelMap to the SwallowingJspRenderer.  In other words, this is captureme.jsp’s model.

Lines 52-54: Here we see the objects that we have seen in captureme.jsp.  We push them into the jspMap so we can pass them to the SwallowingJspRenderer.

Line 55: The magic line.  We pass the SwalloingJspRenderer the name of the model (“capture/captureme”, which the ViewResolver will make a URL with getUrlForView, as we have seen in the renderer), the model to pass to the rendered JSP (“jspMap”), and the Locale that we to use on the rendered JSP (“locale”).  We get back a String that is the rendered JSP page.

Line 58: Now that we have a simple String that represents the rendering of captureme.jsp, we need to add it into index.jsp’s model so that we can print it out on index.jsp, line 18.

Line 63: Ask Spring to render index.jsp with the model that we have been working with.

That’s it!  Now we have a working web application that can render and capture an arbitrary JSP file returning a simple String.  You can run it in Eclipse or use Maven to make it a WAR file (“mvn clean package” inside the project root).  The URL will be something like http://localhost:8080/capturejsp/, depending on your application server setup.  Let’s see a few screenshots of how it looks with no parameters:

IndexJsp_English

Note that my system’s default Locale is en_US.  Yours may be different.  Notice the “Welcome!” message in English, and the formatNumber for Euros returned “EUR4,567.89”, using the EUR to indicate Euros and a comma for the thousands separator and a period to separate the whole and decimal part of the number (as is standard in the US).

Now let’s click on “Use Locale de_DE”  (de is the language code for German, and DE the country code for Germany).

IndexJsp_German

Note that the welcome message is now in German.  That is spring:message at work, using our new Locale.  Also note that the formatNumber for Euros has changed quite a bit, using the Euro symbol (at the end), the period for the thousands separator, and the comma between whole and decimal parts of the number (as is the standard in Germany).

I hope you have enjoyed this tutorial on rendering and capturing a JSP page.  Don’t forget that full source code for this project is available on GitHub.

For those of you who don’t use Maven, here as a list of all the dependencies as reported by Eclipse’s webappclassth.  I haven’t checked this out, but it looks correct.  Let me know if is not.

\javax\servlet\jstl\1.2\jstl-1.2.jar

\log4j\log4j\1.2.14\log4j-1.2.14.jar

\org\springframework\spring-webmvc\3.0.5.RELEASE\spring-webmvc-3.0.5.RELEASE.jar

\org\springframework\spring-asm\3.0.5.RELEASE\spring-asm-3.0.5.RELEASE.jar

\org\springframework\spring-beans\3.0.5.RELEASE\spring-beans-3.0.5.RELEASE.jar

\org\springframework\spring-core\3.0.5.RELEASE\spring-core-3.0.5.RELEASE.jar

\commons-logging\commons-logging\1.1.1\commons-logging-1.1.1.jar

\org\springframework\spring-context\3.0.5.RELEASE\spring-context-3.0.5.RELEASE.jar

\org\springframework\spring-aop\3.0.5.RELEASE\spring-aop-3.0.5.RELEASE.jar

\aopalliance\aopalliance\1.0\aopalliance-1.0.jar

\org\springframework\spring-expression\3.0.5.RELEASE\spring-expression-3.0.5.RELEASE.jar

\org\springframework\spring-context-support\3.0.5.RELEASE\spring-context-support-3.0.5.RELEASE.jar

\org\springframework\spring-web\3.0.5.RELEASE\spring-web-3.0.5.RELEASE.jar

\org\springframework\spring-test\3.0.5.RELEASE\spring-test-3.0.5.RELEASE.jar

\org\directwebremoting\dwr\2.0.3\dwr-2.0.3.jar

Feel free to ask questions, point out errors (there are always a couple), or just leave a comment.

20 thoughts on “Render and capture the output of a JSP as a String”

  1. Hi, i like the tutorial. I almost gave up and tried an other way.
    With this i can use the same jsp for the “real” page by including it and i can return the page-content encoded as a part of an json object called by ajax.
    I would like to use it and create a kind of library to have a tested maven artifact. That artifact i would like to use as a dependency for (an) other project(s).
    The only thing is, in the git-reposetory is no License included. Would you add a apache, mit or lgpl license?

  2. Bob, first of all, thanks for writing this tutorial. Bonus points for being so thorough. I came here looking to do the exact same thing as your first poster, David. Anyway, I’ve encountered one big problem. The render(…) method kicks off the servlet container’s own include(…) method which ultimately causes a MalformedURLException. Here’s a snippet of the stack trace:

    ——————————————————-
    java.net.MalformedURLException: Path does not start with a “/” character
    com.zeroturnaround.javarebel.vS.a(JRebel:50)
    com.zeroturnaround.javarebel.pY.getResource(JRebel:108)
    org.eclipse.jetty.webapp.WebAppContext$Context.getResource(WebAppContext.java)
    org.apache.jasper.servlet.JspServlet._serviceJspFile(JspServlet.java:447)
    org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java)
    org.apache.jasper.servlet.JspServlet.service(JspServlet.java:380)
    javax.servlet.http.HttpServlet.service(HttpServlet.java:820)
    org.eclipse.jetty.servlet.ServletHolder.handle(ServletHolder.java:538)
    org.eclipse.jetty.servlet.ServletHandler.doHandle(ServletHandler.java:478)
    org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:119)
    org.eclipse.jetty.security.SecurityHandler.handle(SecurityHandler.java:517)
    org.eclipse.jetty.server.session.SessionHandler.doHandle(SessionHandler.java:225)
    org.eclipse.jetty.server.handler.ContextHandler.__doHandle(ContextHandler.java:937)
    org.eclipse.jetty.server.handler.ContextHandler.doHandle(ContextHandler.java)
    org.eclipse.jetty.servlet.ServletHandler.doScope(ServletHandler.java:406)
    org.eclipse.jetty.server.session.SessionHandler.doScope(SessionHandler.java:183)
    org.eclipse.jetty.server.handler.ContextHandler.doScope(ContextHandler.java:871)
    org.eclipse.jetty.server.handler.ScopedHandler.handle(ScopedHandler.java:117)
    org.eclipse.jetty.server.Dispatcher.include(Dispatcher.java:195)
    … [the rest omitted] …
    ——————————————————-

    (FYI: JRebel isn’t at fault; I get the same whether it’s enabled or not)

    I’ve debugged my custom ViewResolver’s getUrlForView(…) method, and it always returns a path starting with a “/” character (i.e.: starts with “/WEB-INF/”). So I have no idea how to go forward from here. Any idea what the problem might be?

    Michael

    1. Hmm.. that’s a tough one. My first guess would be that the context in the web.xml file doesn’t start (or possibly end) with a “/”, so you may want to look there.

      I also seem to remember that in some early versions of Tomcat that there was a bug getting to internal resources unless they originated within the container itself (as a security measure). Not sure about Jetty though, as I don’t normally use it.

      Check out the web.xml file’s context setting and see if that works. In the meantime, I’ll try to set up a Jetty instance (I don’t have JRebel, but you said it happened even without, so that’s obviously not it).

      Are you using Eclipse’s WTP there? Or just building a war and running it outside?

  3. Hello bob, i’ve use your SwallowingJspRenderer in Tomcat 7.0 and it’s work great. But, when i try to using JBOSS EAP 6.0 or JBOSS AS 7.x.x as a web server, i’ve got error below :

    javax.servlet.ServletException: Original SevletRequest or wrapped original ServletRequest not passed to RequestDispatcher in violation of SRV.8.2 and SRV.14.2.5.1

    What should i do to solve this problem? Because i’ll use JBOSS EAP 6.0 on production’s environment. Thanks 🙂

  4. Hi Apri,

    I haven’t used JBoss in a while, so I haven’t seen this error. One of the JBoss forums suggests this can fixed by turning off strict server compliance with “-Dorg.apache.catalina.STRICT_SERVLET_COMPLIANCE=false” in the startup script though.

    (Yeah, I know, looks weird with org.apache.catalina in it)

    Bob

    1. Thank you for your response bob, i’ve add -Dorg.apache.catalina.STRICT_SERVLET_COMPLIANCE=false in standalone.conf but i’ve got another error :

      java.lang.ClassCastException: com.bankbjb.itcore.cashportal.common.MockIncludedHttpServletRequest cannot be cast to javax.servlet.ServletRequestWrapper

      Any idea to solve this error? thanks again bob 🙂

      1. @Apri,

        I’m not sure why it would be casting MockIncludedHttpServletRequest (MIHSR for short) to ServletRequestWrapper. MIHSR doesn’t implement wrapper, but is (ultimately) derived directly from SerlvetRequest.

        In theory, you could modify MIHSR to implement ServletWrapper as well, though. Can you tell where this is happening? Is there more to the stack trace?

        Bob

        1. helo rob,
          sorry for late response, i’ve got stack trace from jboss log :

          09:56:33,696 INFO [stdout] (http-/127.0.0.1:8089-5) SwallowingJspRenderer: com.bankbjb.itcore.cashportal.common.MockIncludedHttpServletRequest cannot be cast to javax.servlet.ServletRequestWrapper
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) java.lang.ClassCastException: com.bankbjb.itcore.cashportal.common.MockIncludedHttpServletRequest cannot be cast to javax.servlet.ServletRequestWrapper
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationFilterFactory.createFilterChain(ApplicationFilterFactory.java:164)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:827)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationDispatcher.doInclude(ApplicationDispatcher.java:720)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationDispatcher.include(ApplicationDispatcher.java:657)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at com.bankbjb.itcore.cashportal.common.SwallowingJspRenderer.render(SwallowingJspRenderer.java:91)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at com.bankbjb.itcore.cashportal.controller.CredentialControler.renderView(CredentialControler.java:341)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at com.bankbjb.itcore.cashportal.controller.CredentialControler.showView(CredentialControler.java:296)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at com.bankbjb.itcore.cashportal.controller.CredentialControler.showView(CredentialControler.java:290)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at com.bankbjb.itcore.cashportal.controller.CredentialControler.add(CredentialControler.java:133)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at java.lang.reflect.Method.invoke(Method.java:601)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:212)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:126)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:96)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:617)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:578)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:80)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:900)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:827)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:882)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:789)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at javax.servlet.http.HttpServlet.service(HttpServlet.java:754)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at javax.servlet.http.HttpServlet.service(HttpServlet.java:847)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:329)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:248)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:76)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:280)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:248)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:275)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:161)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.jboss.as.web.security.SecurityContextAssociationValve.invoke(SecurityContextAssociationValve.java:153)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:155)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:368)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:877)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:679)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:931)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at java.lang.Thread.run(Thread.java:722)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) java.io.IOException: java.lang.ClassCastException: com.bankbjb.itcore.cashportal.common.MockIncludedHttpServletRequest cannot be cast to javax.servlet.ServletRequestWrapper
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at com.bankbjb.itcore.cashportal.common.SwallowingJspRenderer.render(SwallowingJspRenderer.java:98)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at com.bankbjb.itcore.cashportal.controller.CredentialControler.renderView(CredentialControler.java:341)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at com.bankbjb.itcore.cashportal.controller.CredentialControler.showView(CredentialControler.java:296)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at com.bankbjb.itcore.cashportal.controller.CredentialControler.showView(CredentialControler.java:290)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at com.bankbjb.itcore.cashportal.controller.CredentialControler.add(CredentialControler.java:133)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at java.lang.reflect.Method.invoke(Method.java:601)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.method.support.InvocableHandlerMethod.invoke(InvocableHandlerMethod.java:212)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:126)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:96)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:617)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:578)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:80)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:900)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:827)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:882)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:789)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at javax.servlet.http.HttpServlet.service(HttpServlet.java:754)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at javax.servlet.http.HttpServlet.service(HttpServlet.java:847)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:329)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:248)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:76)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:280)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:248)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:275)
          09:56:33,696 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:161)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.jboss.as.web.security.SecurityContextAssociationValve.invoke(SecurityContextAssociationValve.java:153)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:155)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:368)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:877)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:679)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:931)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) at java.lang.Thread.run(Thread.java:722)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) Caused by: java.lang.ClassCastException: com.bankbjb.itcore.cashportal.common.MockIncludedHttpServletRequest cannot be cast to javax.servlet.ServletRequestWrapper
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationFilterFactory.createFilterChain(ApplicationFilterFactory.java:164)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:827)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationDispatcher.doInclude(ApplicationDispatcher.java:720)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) at org.apache.catalina.core.ApplicationDispatcher.include(ApplicationDispatcher.java:657)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) at com.bankbjb.itcore.cashportal.common.SwallowingJspRenderer.render(SwallowingJspRenderer.java:91)
          09:56:33,711 ERROR [stderr] (http-/127.0.0.1:8089-5) … 37 more

          thanks again rob 🙂

          1. @Apri,

            I’ve been looking around, and to be honest I am stumped. RequestDispatcher.include (which is where the exception starts) should accept any ServletRequest, without it being a wrapper. I’m not sure why JBoss would require such. Maybe somebody on here that uses JBoss can answer this?

            Bob

  5. I had the following problem:
    In a Spring MVC app I had some SVG-s generated by JSP-s. Now I was asked to add the SVG-s to a PDF, which was generated in a controller.

    So I needed exactly the solution. But I tried a simpler one, and it seems to work. Here is my solution:

    1. I implemented javax.servlet.http.HttpServletResponse wrapping PrintWriter, and created content only for getWriter() method:

    public class HttpServletResponseOutput implements HttpServletResponse {

    private PrintWriter printWriter;

    public HttpServletResponseOutput(StringWriter stringWriter){
    this.printWriter = new PrintWriter(stringWriter);
    }

    @Override
    public PrintWriter getWriter() throws IOException {
    return this.printWriter;
    }

    }

    2. I autowired another controller to the main controller:

    @Controller(value = “anotherController”)
    public class AnotherController {…

    @Controller
    public class MainController {
    @Autowired
    private VastController vastController;

    3. Inside the controller method:

    RequestDispatcher dispatcher = request.getRequestDispatcher(“/WEB-INF/views/svg/graph.jsp”); // Absolute path here. It’s not very nice, but it works.
    StringWriter stringWriter = new StringWriter();
    HttpServletResponse intermediateResponse = new HttpServletResponseOutput(stringWriter);
    anotherController.getGraph(model, request, intermediateResponse);
    try{
    dispatcher.include(request, intermediateResponse);
    } catch(Exception e){
    log.error(e.getMessage());
    }
    String graph = stringWriter.toString();
    // This String contains the SVG now

  6. Hi Bob,

    My project wants the static html rendered out of jsp. Will this source code helps in capturing HTML.

    Thanks in advance.
    Sudha.

    1. It should. The “string” that comes out of the JSP renderer is simply the HTML that the JSP would have produced. You could leave out the model stuff if you don’t need it.

  7. Thanks for the tutorial, but messageSource doesn’t work for me (configured it as bean in my java config).

    It works when i use the jsps like usual, but it does not when i want to render jsps to a String.

    Any ideas?

  8. Hi Bob,

    I deployed your code and when I request http://localhost:8080/capturejsp/
    Browser only shows content of index.jsp. The text below line “The capture JSP page follows:” is empty.

    I checked Console and saw this warning:
    |WARN |oting.util.SwallowingHttpServletResponse||Ignoring call to sendError(405, JSPs only permit GET POST or HEAD)

    I think that the mock request object does not have any HTTP method. Then, I change file MockIncludedHttpServletRequest.java as follows:

    public class MockIncludedHttpServletRequest extends MockHttpServletRequest {

    public MockIncludedHttpServletRequest() {
    super();
    this.setMethod(“GET”);
    }

    Now, JSP files are renderred.

  9. Hi, thanks for this tutorial, it works like a charm. I have one issue I could use your help with please. I am using this a secondary way of rendering content in a Spring App. Basically in some cases we use the regular Spring renderer and in others we have to use this.

    What I’d like to know is if its possible that the bean annotation I use for formatting can be applied to this as well?

    I have a custom formatter and annotation for phone number. The formatter is added to Springs registry like this:

    @Override
    public void addFormatters(FormatterRegistry registry)
    {
    registry.addFormatterForFieldAnnotation(new PhoneNumberFormatAnnotationFormatterFactory());
    registry.addFormatterForFieldAnnotation(new BinaryIntFormatterAnnotationFormatterFactory());
    }

    How do I accomplish this in the context of the swallowing renderer?

    Thanks for any advice you can offer.

    1. You know, I’ve looked at this several times over the last couple of months, and I can’t find where the FormatterRegistry is attached. There must be one somewhere that could be modified for the Swallowing renderer, I just can’t find it very easily. If you breakpoint your normal addFormatterFor… calls, you might be able to find it. Sorry I couldn’t be more helpful there.

Leave a Reply

Your email address will not be published. Required fields are marked *