Pages

Monday, December 22, 2008

Creating custom result type - struts 2

Creating a custom result type comes to the field when your are trying
to develop a Ajax base development with struts 2. By default, struts 2 uses RequestDispatcher originated from servlets API, as the default result type. That is, after returning the control string from your action, this default result type will show the way for final page rendering.

Suppose you want to have some data response to the client, in stead of despatching to a JSP page after returning from your action class. Ajax request to the server often needs this kind of data.
Here, I am going to explain you, how to make a Ajax request to a struts 2 action class and how to use a custom result type to send some data back to the client, instead of rendering a another JSP page as usual. I am not going to give you a detailed description on Ajax related things. My major concern is how to create a custom result type and use it with Ajax development.

I have a list of User's names in a struts select box component and onchange event will make a request to a action class to get full details about the user to the client and update some segment of current rendered page of the browser as usual Ajax application does. I am passing userId as query string parameter to the action class.
Here is the javascript code the responsible to make the request to the server. I have encpsulated all related functions needed to make a request to the server, into one javascript object.

var PDA = {};
PDA.common = {};
PDA.common.ContentLoader = function(httpMethod,url,params,callBack){
     this.init(httpMethod,url,params,callBack);
}
PDA.common.ContentLoader.prototype = {
READY_STATE_UNINITIALIZED :0,
READY_STATE_LOADING :1,
READY_STATE_LOADED :2,
READY_STATE_INTERACTIVE :3,
READY_STATE_COMPLETE :4,
httpMethod : null,
url : null,
params : null,
callBack : null,
error : null,
request : null,

defaultError:function(){
      alert("error fetching data!"
      +"\n\nreadyState:"+this.request.readyState
      +"\nstatus: "+this.request.status
      +"\nheaders: "+this.request.getAllResponseHeaders());
},

getXMLHttpRequest: function(){
   if(window.XMLHttpRequest){
     this.request = new XMLHttpRequest();
   }else if(typeof ActiveXObject != "undefined"){
       try {
           this.request = new ActiveXObject("Msxml2.XMLHTTP");
       }catch(e) {
           try{
                  this.request = new ActiveXObject("Microsoft.XMLHTTP");
              } catch (e){
                    this.request = null;
              }
        }
   } 
},

onReadyState: function(){
 var req = this.request;
 var ready = req.readyState;
 if (ready == this.READY_STATE_COMPLETE){
     var httpStatus = req.status;
    if (httpStatus == 200 || httpStatus == 0){
         this.callBack.call(this);
     }else{
         this.error.call(this);
     }
  }
},

sendRequest: function(){
   this.getXMLHttpRequest();
      if(this.request){
         try{
             var loader = this;
             this.request.onreadystatechange = function(){
                  loader.onReadyState.call(loader);
             }
             this.request.open(this.httpMethod,this.url,true);
             this.request.send(this.params);
          }catch(e){
                this.error.call(this);
          }
    }else{
         this.error.call(this);
    }
},

init: function(httpMethod,url,params,callBack){
      this.httpMethod = httpMethod;
      this.url = url;
      this.params = params;
      this.callBack = callBack;
      this.error = this.defaultError;
      this.sendRequest();
   }
};
The above object almost contains all the function to make a Ajax request to the server. I will simply explain the constructor parameters need to initiate object from this class.
  • httpMethod : eidther "POST" or "GET"
  • url : request url (in my case, it is action name)
  • params : url parameter needed to pass to the server
  • callBack : javascrip function name which handles the response data and update the client browser content
If you want to make a request to the server, you can simply initiate an object from this class. Easy isn't it? Now the time to call the action.
My user list select box' onchange event will fire the fallowing javascrip function which get the user id form the select box and make a request to the "ViewUsers" class.
function fetchUser(){
    var selectBox = document.getElementById('userId');
    var selectedIndex = selectBox.selectedIndex;
    var selectedValue = selectBox.options[selectedIndex].value;
    new PDA.common.ContentLoader("POST",
                                 "ViewUsers.action?userId=" + selectedValue,
                                  null,
                                  showUsers);
}
"showUsers" function is the callback function in my case which is responsible for client side updates. Let's see the source code for ViewUsers action class next.
package com.axiohelix.pda.actions;
import org.hibernate.Transaction;
import com.opensymphony.xwork2.ActionSupport;

public class ViewUsers extends ActionSupport{
 private static final long serialVersionUID = 1L;
 private int userId;
 private User user;
 private Object jsonModel;
 
 public String execute(){
  String result = null;
  try{
   //do some thing to get the user object from userId
   setUserType(user);
   setJsonModel(user);
   result = SUCCESS;
  }catch(Exception e){
   e.printStackTrace();
   result = ERROR;
  }finally{
  }
  return result;
 }
 
 public int getUserId() {
  return userId;
 }

 public void setUserId(int userId) {
  this.userId = userId;
 }

 public User getUser() {
  return user;
 }

 public void setUser(User user) {
  this.user = user;
 }

 public Object getJsonModel() {
  return jsonModel;
 }

 public void setJsonModel(Object jsonModel) {
  this.jsonModel = jsonModel;
 }
}
As you can see, I have created User object from the userId and exposed it into the ValueStack under the property of "jsonModel". After returning from this action class, it should hit our custom result type and should send the user informations back to the client. Here, I am going to use JSON which provides a succinct text-based serialization of data objects. JSON can be a powerfully succinct and lightweight means of communication between a web application server and an Ajax client.
Next, we will see the core of the post.We are going to create the custom result which sends JSON data string back to the client. The only requirement needs to create a struts2 custom result is, implementing Result interface of struts 2. Here is the source code to create a custom result type and serialize the object in to JSON.

package com.axiohelix.pda.jsonresult;

import java.io.PrintWriter;

import org.apache.struts2.ServletActionContext;

import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.Result;
import com.opensymphony.xwork2.util.ValueStack;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver;

public class JSONResult implements Result{
 private static final long serialVersionUID = 1L;
 public static final String DEFAULT_PARAM = "classAlias";
 String classAlias;

 public String getClassAlias() {
  return classAlias;
 }
 public void setClassAlias(String classAlias) {
  this.classAlias = classAlias;
 }
 @Override
 public void execute(ActionInvocation invocation) throws Exception {
  ServletActionContext.getResponse().setContentType("text/plain"); 
  ServletActionContext.getResponse().addHeader("Cache-Control", "no-cache");
  PrintWriter responseStream = ServletActionContext.getResponse().getWriter();
  ValueStack valueStack = invocation.getStack();
  Object jsonModel = valueStack.findValue("jsonModel");
  XStream xstream = new XStream(new JettisonMappedXmlDriver());
  if ( classAlias == null ){
   classAlias = "object";
  }
  xstream.alias(classAlias, jsonModel.getClass() ); 
  responseStream.println(xstream.toXML( jsonModel ));
 }
}

Here, I have used two open source libraries, XStream and Jettison. XStream helps to serialize java objects into XML format while Jettison helps to create JSON from the XML.As you can see,I am getting "jsonModel" property exposed into the ValueStack from our ViewUsers action calss,progrmmetically and serialize it into JSON.
Now the time to tell ViewUsers action class about our custom result type. We have to add fallowing entries into our declarative architecture, ie struts.xml file. To declare our custom result type, you have to add fallowing into struts.xml file.
<result-types>
<result-type name="customJSON" class="com.axiohelix.pda.jsonresult.JSONResult">
</result-type>
</result-types>
To use our custom result type, we need to add the fallowing to struts.xml file.
<action name="ViewUsers" class="com.axiohelix.pda.actions.ViewUsers">
<result type="customJSON">user</result>
</action&>
Here, you can see, I am passing in a parameter to the custom result type. It will be set to the "classAlias" field and finally, it will be the name of the root JSON object return to the client. Fallowing is the response JSON string representation of JSON

{"user":{"username":"Mary",
"password":"max",
"age":"28",
"firstName":"Mary",
"lastName":"Greene",
"receiveJunkMail":"false"}}

5 comments:

  1. Hi,
    Thanks, but it comes to a cache problem if I pass the map. How to solve that? Thanks in advance.

    ReplyDelete
  2. One simple way for avoiding caching is, passing timestamp with the response data.

    ReplyDelete
  3. Great work. I have a doub. When you go to callback function, showUsers, how do you get jsonObject properties?. instantiate a PDA object? PDA.attribute o PDA.Object.attribute?

    Thank for all.

    ReplyDelete
  4. Hi Semika, thank.... you have a free beer...;-)

    ReplyDelete

Share

Widgets