Monday, May 19, 2008

Example of Spring Based Ajax Servlet

     A lot people are into GWT when it comes to AJAX. No offense, but I found it disturbing to create a servlet for each AJAX action. So for my projects, I wrote one universal servlet for all my AJAX uses.

     I know there are all kinds of AJAX resources out there. But I started out with prototype and JSON's Json-lib. So, my servlet is also based on these two.

     Let's first take a look at the Servlet:

SpringBasedAjaxServlet.java

package com.mycompany.ajax.servlet;

import java.io.PrintWriter;
import java.lang.reflect.Method;

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

import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.web.servlet.FrameworkServlet;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;

/**
 * @author KWang
 *
 */
public class SpringBasedAjaxServlet extends FrameworkServlet {
    
    private static final long serialVersionUID = 1L;
    protected static Logger log = Logger.getLogger(SpringBasedAjaxServlet.class);

    /* (non-Javadoc)
     * @see org.springframework.web.servlet.FrameworkServlet#doService(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)
     */
    @Override
    protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {

        String json = null;

        try {
            json = getJSONContent(request, response);
        } catch (Exception ex) {
            // Send back a 500 error code.
            log.debug(ex);
            response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Can not create response");
            return;
        }

        response.setContentType("text/json; charset=UTF-8");
        response.setHeader("Cache-Control", "no-cache");
        // this will be the second parameter in onXxxxxx function, already
        // parsed as object
        // unfortunately this header won't work with i18n
        // if(StringUtils.isNotBlank(json))
        // response.setHeader("X-JSON", json);
        PrintWriter pw = response.getWriter();
        // has to set json here if it's not null
        // add security comment delimiters defined by prototype 1.5.1
        pw.write("/*-secure-\n" + json + "\n*/");
        pw.close();
    }

    /**
     * Each child class should override this method to generate the specific
     * JSON content necessary for each AJAX action.
     * 
     * @param request
     *            the {@javax.servlet.http.HttpServletRequest} object
     * @return a {@java.lang.String} representation of the JSON response/content
     */
    private String getJSONContent(HttpServletRequest request, HttpServletResponse response) throws Exception {
        String action = request.getParameter("$action");
        String m = request.getParameter("$method");
        log.debug("calling method name: " + m);
        if (StringUtils.isNotBlank(action)) {
            Object actionClass = this.getWebApplicationContext().getBean(action);
            Method method = actionClass.getClass().getMethod(m, HttpServletRequest.class);
            if (method == null) 
                throw new NoSuchRequestHandlingMethodException(m, getClass());
            return (String) method.invoke(actionClass, new Object[] { request });
        } else {
            //for use with servlets that extend this class
            Method method = this.getClass().getMethod(m, HttpServletRequest.class);
            if (method == null)
                throw new NoSuchRequestHandlingMethodException(m, getClass());
            customSetting();
            return (String) method.invoke(this, new Object[] { request });
        }
    }


    protected void customSetting(){
        //if you extend this class, wire your beans here
    }
}


web.xml


    ...

    <servlet>
        <servlet-name>ajax</servlet-name>
        <servlet-class>com.mycompany.ajax.servlet.SpringBasedAjaxServlet</servlet-class>
        <load-on-startup>2</load-on-startup>
    </servlet>

    ...

    <servlet-mapping>
        <servlet-name>ajax</servlet-name>
        <url-pattern>/AjaxServlet</url-pattern>
    </servlet-mapping>

    ...


     This servlet can be used in two ways. First, the servlet will get an action parameter from the request. If the parameter exists, then the servlet will retrieve the action class bean, invoke the specified method, and return the result as a String. Note that the method's name should also be passed in as a request parameter. And, the method should take in an HttpServletRequest (or any kind of request wrapper as you need) as argument. This should be used when there are a lot AJAX actions that cannot be hold in one class. One good reason for me to use the servlet this way is that I can use one action for both AJAX and non-AJAX calls. All I need to do is pass in a request parameter indicating whether it's an AJAX call or not and return different form of object, or return nothing at all, accordingly.

     The second way is extending this SpringBasedAjaxServlet and write all your AJAX calls inside the servlet. The customSetting method needs to be overwritten if you need to wire additional spring beans. It's a great way to use this servlet if you only have a few AJAX calls which are used widely across the application.

     That's a wrap for my 2 cents.

3 comments:

Anonymous said...

Hi Katie,

I found some reasons not to use the X-JSON header for the JSON response (e.g. max length in IE), but your comment about i18n makes me wonder what your problems were. How can your JSON response interfere with i18n?

Bye, Marc

Katie said...

Hi Marc,
It's true that IE has max length limitation on X-JSON header which in my case, I exceeds most of the time. I'm not sure if it's because of this or prototype's way of parsing the response, returned i18n characters are somehow messed up. It doesn't appear to be a problem after I stop using X-JSON header. Well, it's been a while since I wrote these code. My memory on this issue starts to fade as I move on to new projects. That's why I'm trying to write it down in this blog as a reminder. My comments were written when I was using prototype 1.4. If it is a problem with prototype, it might not be a problem with later version.

Anonymous said...

Katie,
Thank you very much for this post. I've been working with JSF/Facelets for the first time on a recent project. We've found the ajax4jsf too slow for many operations. I'm investing in some ways to make ajax calls outside of the JSF lifecycle and was curious about the X-JSON response header.
Our app needs to be i18n, so I am leary of using the X-JSON header. We will also be breaking any length limitation *grin*. Without the header, I assume I will need to manually eval() the response to get my object?
As a side note, I appreciated your post on the difference between American and Chinese cultures. Having lived in Germany, I remember this phrase about the difference between the U.S. and Germany: "Americans boil their bodies and put them in dirty clothes; Germans boil their clothes and put them on dirty bodies." This is in reference to the temperature of our washing machines and the frequency with which we bathe. Your post brought back many memories of cultural clashes which both challenged my sensibilities and made me more aware of my culture and provincialism.
Again, many thanks,
Tom