Tutorial: Implementing a Servlet Filter for JSONP callback with Spring’s DelegatingFilterProxy

Update: I recommend to anyone wanting to learn more about JSONP to also read this blog post.  The author covers some goods points that ensure better security and prevent the execution of bad code or malicious code.

Introduction

In my previous tutorial, I talked about creating a web application using Spring Roo, Hibernate EntityManager and JSONP for cross-domain AJAX calls.  I also mentioned some trouble I had using @ResponseBody to generate JSON output.  When asked by SpringSource’s Keith Donald what the issue was, I was completely unable to reproduce it, although I remembered it having something to do with Jackson not being found on my classpath and an exception being thrown, even though it was there.  Further feedback from SpringSource’s Jeremy Grelle suggested that I remove the JSONP callback handling from my controller and place it in either a servlet filter or an MVC HandlerInterceptor.  Having zero experience programming servlet filters, I decided now was a good time to learn.  This tutorial is a follow up to my last tutorial and I’ll cover the servlet filter itself and show you the new reduced controller class.

Contents

  1. Implementing the servlet filter and servlet response wrapper
  2. The simplified/reduced @Controller from my previous tutorial
  3. Conclusion

1. Implementing the servlet filter and servlet response wrapper

Before getting right into the servlet filter code itself, I’ll need to add to both the web.xml file and the applicationContext.xml file.  In the web.xml file I’ll add a reference to the new servlet filter, but using Spring’s DelegatingFilterProxy.  I use the DelegatingFilterProxy because that filter will delegate to my own filter which I define in the applicationContext.xml file further allowing me to take full advantage of Spring’s dependency injection if I need to.  In this example, my filter doesn’t make use of any other Spring managed beans, but using this as a starting point will easily allow me to add Spring managed beans to my filter in the future if I see the need.

The following filter and filter-mapping gets added to web.xml:

<filter>
	<filter-name>jsonpCallbackFilter</filter-name>
	<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
	<filter-name>jsonpCallbackFilter</filter-name>
	<url-pattern>*.json</url-pattern>
</filter-mapping>

In my filter-mapping, I’m saying I want all requests ending in .json to be handled by my filter, but nothing else.  By default, the DelegatingFilterProxy will look for a Spring managed bean with the same name I’ve given my filter.  If I wanted to override this behaviour, I could set an init-param named “targetBeanName” with a value of the actual Spring managed bean I want the proxy to delegate to.  The following is that Spring managed bean located in applicationContext.xml:

<bean class="org.mypackage.web.servlet.filter.JsonpCallbackFilter" id="jsonpCallbackFilter" />

With that out of the way, let’s move on to the actual filter class.

package org.mypackage.web.servlet.filter;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class JsonpCallbackFilter implements Filter {

	private static Log log = LogFactory.getLog(JsonpCallbackFilter.class);

	public void init(FilterConfig fConfig) throws ServletException {}

	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
		HttpServletRequest httpRequest = (HttpServletRequest) request;
		HttpServletResponse httpResponse = (HttpServletResponse) response;

		@SuppressWarnings("unchecked")
		Map<String, String[]> parms = httpRequest.getParameterMap();

		if(parms.containsKey("callback")) {
			if(log.isDebugEnabled())
				log.debug("Wrapping response with JSONP callback '" + parms.get("callback")[0] + "'");

			OutputStream out = httpResponse.getOutputStream();

			GenericResponseWrapper wrapper = new GenericResponseWrapper(httpResponse);

			chain.doFilter(request, wrapper);

			out.write(new String(parms.get("callback")[0] + "(").getBytes());
			out.write(wrapper.getData());
			out.write(new String(");").getBytes());

			wrapper.setContentType("text/javascript;charset=UTF-8");

			out.close();
		} else {
			chain.doFilter(request, response);
		}
	}

	public void destroy() {}
}

In this filter, the only method I’m really implementing is the doFilter method.  If I needed to implement the other methods required by the Filter class, I would need to add another init-param to my filter in web.xml called “targetFilterLifecycle” and set it to true.  That will force the init and destroy methods to be called.  Since I don’t need them right now, then I’ve left them blank.

This section of the tutorial was an adaptation of documentation I found on Oracle’s (Sun’s) website.  The job of this filter is to determine if the JSON request being made needs to be wrapped with a callback method and produce a JSONP response instead.  To do that, I get the parameter map from the request.  This results in a map of String keys and String[] values.  So I check to see if the map contains a key named “callback” which will identify that a JSONP request is being made.  If the key doesn’t exist, I don’t bother wrapping the response and I pass the request and response on to the filter chain.  In the event a “callback” key is found, then I can proceed to wrap the JSON output.

An important part is where you place the wrapper.setContentType call to change the Content-Type header to text/javascript.  At first, I was setting the Content-Type write after creating the GenericResponseWrapper instance, however Chrome kept telling me that the data received from the server had a Content-Type of application/json which was obviously being done because my controller method was annotated with @ResponseBody and with Jackson Mapper on my classpath, it converted the output to JSON.  After running in debug a few times, I determined that I was successfully setting the Content-Type to text/javascript, but the call to chain.doFilter was overwriting that.  So placing that line of code after chain.doFilter resulted in a properly set Content-Type.  I also tried placing that line after out.close() which closes the OutputStream, but that basically didn’t do anything, so once the OutputStream was closed, I could no longer change the Content-Type.

The following two classes are the GenericResponseWrapper and FilterServletOutputStream.  I won’t go into much detail about these classes because I merely copied them from the documentation I mentioned above.  My broad understanding of creating a response wrapper is to add custom functionality to the servlet response giving you the ability to modify or transform that response.

package org.mypackage.web.servlet.filter;

import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;

public class GenericResponseWrapper extends HttpServletResponseWrapper {

	private ByteArrayOutputStream output;
	private int contentLength;
	private String contentType;

	public GenericResponseWrapper(HttpServletResponse response) {
		super(response);

		output = new ByteArrayOutputStream();
	}

	public byte[] getData() {
		return output.toByteArray();
	}

	public ServletOutputStream getOutputStream() {
		return new FilterServletOutputStream(output);
	}

	public PrintWriter getWriter() {
		return new PrintWriter(getOutputStream(), true);
	}

	public void setContentLength(int length) {
		this.contentLength = length;
		super.setContentLength(length);
	}

	public int getContentLength() {
		return contentLength;
	}

	public void setContentType(String type) {
		this.contentType = type;
		super.setContentType(type);
	}

	public String getContentType() {
		return contentType;
	}
}
package org.mypackage.web.servlet.filter;

import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;

import javax.servlet.ServletOutputStream;

public class FilterServletOutputStream extends ServletOutputStream {

	private DataOutputStream stream;

	public FilterServletOutputStream(OutputStream output) {
		stream = new DataOutputStream(output);
	}

	public void write(int b) throws IOException {
		stream.write(b);
	}

	public void write(byte[] b) throws IOException {
		stream.write(b);
	}

	public void write(byte[] b, int off, int len) throws IOException {
		stream.write(b, off, len);
	}

}

That’s all it takes to wrap the JSON output to produce a JSONP response.

2. The simplified/reduced @Controller from my previous tutorial

package org.mypackage.tools.mileage;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import org.mypackage.tools.mileage.domain.Mileage;
import org.mypackage.tools.mileage.domain.ZipSystem;

@RequestMapping("/mileage/**")
@Controller
public class MileageController {

	private final Log log = LogFactory.getLog(MileageController.class);

	@RequestMapping(value = "/mileage/locations", method = RequestMethod.GET)
	public @ResponseBody List<ZipSystem> getLocations(@RequestParam(value = "q", required = true) String cityName, @RequestParam(value="limit", required = false) String limit) {
		int maxResults = 0;

		try {
			maxResults = Integer.parseInt(limit);
		} catch(NumberFormatException e) {
			if(log.isWarnEnabled())
				log.warn("A limit was passed that could not be parsed as an int.  Setting maxResults to default." );

			maxResults = ZipSystem.DEFAULT_MAX_RESULTS;
		}

		return ZipSystem.findZipSystemsByCityName(cityName, true, maxResults);
	}

	@RequestMapping(value = "/mileage/calculator", method = RequestMethod.GET)
	public @ResponseBody Map<String, String> calculateMileage(@RequestParam(value = "origin", required = true) String origin, @RequestParam(value = "destination", required = true) String destination) {
		Map<String, String> map = new HashMap<String, String>();

		map.put("miles", Mileage.calculateMileage(origin, destination));

		return map;
	}
}

The big difference here is I’ve annotated the methods with @ResponseBody which will produce the JSON output automatically since I’m now returning actual domain objects rather than a ResponseEntity.  That being said, the custom HTTP headers weren’t needed either.

3. Conclusion

To conclude, after having gotten this working, I can completely agree with Jeremy Grelle’s suggestion to remove the JSONP handling from the controller.  It just makes so much more sense having it in a servlet filter that I can reuse with any @Controller.  Thanks Jeremy for the suggestion.  Also thanks to Keith Donald who got me back on track using @ResponseBody.  One thing I didn’t cover was securing cross-domain requests, I may attempt that in a later tutorial.

About these ads

18 thoughts on “Tutorial: Implementing a Servlet Filter for JSONP callback with Spring’s DelegatingFilterProxy

  1. Cool post! Very informative! I was wondering how to create JSONP from my @Controller and this sounds useful and non-invasive.
    I’ll implement it this way and then it should be easy to remove once (and if) Spring provides an out-of-the-box way to generate JSONP/

  2. Thanks interesting post. Out of interest why use org.springframework.web.filter.DelegatingFilterProxy rather than just implementing a servlet filter and by pass spring and using that filter in web.xml

    • By using DelegatingFilterProxy, I’m able to take advantage of Spring dependency injection within my filter. I didn’t in this case but starting this way allows me that flexibility in the future.

  3. In the filter class JsonpCallbackFilter ,there is a variable httpRequest that does not seem to be initiated anywhere. I think this should this be the input param request? Ditto for httpResponse -> response.

    • I believe I had code in there at some point that cast the input parameters into HttpServletRequest and HttpServletResponse objects. I must have taken out those lines while cleaning up the code and forgot to fix the rest. Good catch, thanks! :-)

  4. Thank you Patrick. Your code and tutorial is very useful. I
    found that I should a line to do jsonp wrap only for the ‘GET’
    case. Without it, getParameterMap() will causes the content in the
    input stream to be consumed. It is a problem if you servlet serves
    ‘POST’ request (doesn’t affect ‘PUT’). I saw this behaviour in
    tomcat 6. public void doFilter(ServletRequest request,
    ServletResponse response, FilterChain chain) throws IOException,
    ServletException { HttpServletRequest httpRequest =
    (HttpServletRequest) request; HttpServletResponse httpResponse =
    (HttpServletResponse) response; // *** add this line to avoid
    POST’s content to be consumed unexpectedly *** //
    if(“GET”.equals(httpRequest.getMethod().toUpperCase())) { Map parms
    = httpRequest.getParameterMap(); // …

  5. Hi,

    I’ve just run into a major issue where a jsp page is over 8KB. This causes the JSPWriterImpl to call getOutputStream() or getWriter() method multiple times to print out the next amount of the buffer.

    This line in your tutorial is where the error would present itself

    public ServletOutputStream getOutputStream() {
    return new FilterServletOutputStream(output);
    }

    What you need to do to be sure that the same output stream is sent back to the calling jsp code is to instantiate your wrapped ServletOutputStream and PrintWriter in your GenericResponseWrapper constructor. So the code would be

    private ByteArrayOutputStream output;
    private FilterServletOutputStream filterStream;
    private PrintWriter printWriter;

    public GenericResponseWrapper(HttpServletResponse response) {
    super(response);
    output = new ByteArrayOutputStream();
    filterStream = new FilterServletOutputStream(output);
    printWriter = new PrintWriter(output, true);
    }

    public ServletOutputStream getOutputStream() {
    return filterStream;
    }

    public PrintWriter getWriter() {
    return printWriter;
    }

  6. Hi Patrick,

    Great tutorial! I have found it very useful and easy to implement.

    While I was doing my tests, I encountered a very strange bug:

    – Using GraphicalHTTPClient on my Mac, I tested the JSONP filter using a “GET” message to a .json endpoint on my server with “accept:application/json” in the header, and callback as a parameter. The server’s response was correctly wrapped in JSONP.
    – Sending the exact same GET, but without “accept:applicaiton/json” in the header, resulted in a truncated response from the server. The server’s response was cut-off by a number of characters equal to the extra characters added by the jsonpCallbackFilter!

    Strange, no? In order to solve the problem, I have refactored the JsonpCallbackFilter to exlicitly reset the size of the response:

    chain.doFilter(request, wrapper);

    byte[] jsonpResponse = Bytes.concat(new String(parms.get(“callback”)[0] + “(“).getBytes(), wrapper.getData(), new String(“);”).getBytes());

    wrapper.setContentType(“text/javascript;charset=UTF-8″);
    wrapper.setContentLength(jsonpResponse.length);

    out.write(jsonpResponse);

    out.close();

    Have you also encountered this phenomenon?

Comments are closed.