Saturday, January 30, 2010

Struts2 DisplayTag Paging and Sorting using AJAX

In struts2, DisplayTag is a really good library for table data. It supports server side paging and sorting like the easiest way possible. Just set few properties on the JSP page, get few values in action class from request and wow, you got it working. Now the idea comes along like why not make it AJAXed, wouldn't that be awesome?. Certianly it would be.

AjaxTags and AjaxAnywhere provides support for that. Unfortunately, I wasn't able to make it work in any easy way. I wanted more control on my page, so I searched the web for more. Someone very cool and awesome was generous enough to post a different solution, just the thing I like. "Change in the source code."



By just changing few lines in the source code of DisplayTag, one can get request link(which DisplayTag uses) in a javascript and after that..... its all fun.

http://www.mail-archive.com/displaytag-devel@lists.sourceforge.net/msg03149.html


What DisplayTag actually does is that it creates a link for every page number and every sort header with parameters. When the page number or sort header is clicked, its performs the "get" request by using the url behind that link. So it performs request on its own. Now, what if you could get the link in your page before DisplayTag performs the request operation? You could perform the request operation on your own using javascript and then update the page accordingly. That certainly would do the job.


Once you get the link in javascript, you can easily AJAXify the displayTag. Now let me take you through the steps to make AJAXed DisplayTag Table in Struts2.


First of all, change the following in DisplayTag source code as specified. You can also use the link given above to do this (I added a little bit of my code to make is fully workable according to my requirements).

Changes in class org.displaytag.util.DefaultHref.java



 public void setFullUrl(String baseUrl)
    {
        this.url = null;
        this.anchor = null;
        if (baseUrl.startsWith("javascript:"))
        {
            this.realUrl = baseUrl;
            

    //Start of my code modification, i only added this part.

            String regex = "(action=')(\\w+)(.action')";
            Pattern pattern = Pattern.compile(regex);
            Matcher matcher = pattern.matcher(baseUrl);
            if(matcher.find()) {
                this.realUrl = matcher.group().substring(8, matcher.group().length()-1);
            }


         //end of my code modification  
          
            this.url = baseUrl;
            return;
        }


        String noAnchorUrl;
        int anchorposition = baseUrl.indexOf('#');

        // extract anchor from url
        if (anchorposition != -1)
        {
            noAnchorUrl = baseUrl.substring(0, anchorposition);
            this.anchor = baseUrl.substring(anchorposition + 1);
        }
        else
        {
            noAnchorUrl = baseUrl;
        }

        if (noAnchorUrl.indexOf('?') == -1)
        {
            // simple url, no parameters
            this.url = noAnchorUrl;
            return;
        }

        // the Url already has parameters, put them in the parameter Map
        StringTokenizer tokenizer = new StringTokenizer(noAnchorUrl, "?"); //$NON-NLS-1$

        if (baseUrl.startsWith("?")) //$NON-NLS-1$
        {
            // support fake URI's which are just parameters to use with the current uri
            url = TagConstants.EMPTY_STRING;
        }
        else
        {
            // base url (before "?")
            url = tokenizer.nextToken();
        }

        if (!tokenizer.hasMoreTokens())
        {
            return;
        }

        // process parameters
        StringTokenizer paramTokenizer = new StringTokenizer(tokenizer.nextToken(), "&"); //$NON-NLS-1$

        // split parameters (key=value)
        while (paramTokenizer.hasMoreTokens())
        {
            // split key and value ...
            String[] keyValue = StringUtils.split(paramTokenizer.nextToken(), '=');

            // encode name/value to prevent css
            String escapedKey = StringEscapeUtils.escapeHtml(keyValue[0]);
            String escapedValue = keyValue.length > 1
                ? StringEscapeUtils.escapeHtml(keyValue[1])
                : TagConstants.EMPTY_STRING;

            if (!this.parameters.containsKey(escapedKey))
            {
                // ... and add it to the map
                this.parameters.put(escapedKey, escapedValue);
            }
            else
            {
                // additional value for an existing parameter
                Object previousValue = this.parameters.get(escapedKey);
                if (previousValue != null && previousValue.getClass().isArray())
                {
                    Object[] previousArray = (Object[]) previousValue;
                    Object[] newArray = new Object[previousArray.length + 1];

                    int j;

                    for (j = 0; j < previousArray.length; j++)
                    {
                        newArray[j] = previousArray[j];
                    }

                    newArray[j] = escapedValue;
                    this.parameters.put(escapedKey, newArray);
                }
                else
                {
                    this.parameters.put(escapedKey, new Object[]{previousValue, escapedValue});
                }
            }
        }
    }



    public String toString()
    {
        StringBuffer buffer = new StringBuffer(30);

        //buffer.append(this.url);   //Comment this line
        boolean jsUrl = url.startsWith("javascript:");
       
        if (!jsUrl) {
            buffer.append(this.url);
        }



        if (this.parameters.size() > 0)
        {
            //buffer.append('?'); //Comment this line
            if (!jsUrl) {
                buffer.append('?');
            }


            Set parameterSet = this.parameters.entrySet();

            Iterator iterator = parameterSet.iterator();

            while (iterator.hasNext())
            {
                Map.Entry entry = (Map.Entry) iterator.next();

                Object key = entry.getKey();
                Object value = entry.getValue();

                if (value == null)
                {
                    buffer.append(key).append('='); // no value
                }
                else if (value.getClass().isArray())
                {
                    Object[] values = (Object[]) value;
                    for (int i = 0; i < values.length; i++)
                    {
                        if (i > 0)
                        {
                            buffer.append(TagConstants.AMPERSAND);
                        }

                        buffer.append(key).append('=').append(values[i]);
                    }
                }
                else
                {
                    buffer.append(key).append('=').append(value);
                }

                if (iterator.hasNext())
                {
                    buffer.append(TagConstants.AMPERSAND);
                }
            }
        }
       
       
        if (jsUrl)
        {
            int xoff = this.url.indexOf("{}", 11); // skip "javascript:"
            if (xoff < 0)
            {
                return "javascript:alert('displayTag: fix requestURI!')";
            }
            try
            {
                // assuming the rest of the URL to be already encoded -otherwise we wouldn't have been able to put it in requestURI
                return this.url.substring(0, xoff) + '\'' + URLEncoder.encode(buffer.toString(), "UTF-8") + '\''
                + this.url.substring(xoff + 2);
            }
            catch (UnsupportedEncodingException e)
            {
                throw new RuntimeException(e);
            }
        }



       
       

        if (this.anchor != null)
        {
            buffer.append('#');
            buffer.append(this.anchor);
        }

        return buffer.toString();
    }


Changes in class org.displaytag.tags.TableTag.java

protected void initHref(RequestHelper requestHelper)
    {
        // get the href for this request
        this.baseHref = requestHelper.getHref();

        if (this.excludedParams != null)
        {
            String[] splittedExcludedParams = StringUtils.split(this.excludedParams);

            // handle * keyword
            if (splittedExcludedParams.length == 1 && "*".equals(splittedExcludedParams[0]))
            {
                // @todo cleanup: paramEncoder initialization should not be done here
                if (this.paramEncoder == null)
                {
                    this.paramEncoder = new ParamEncoder(getUid());
                }

                Iterator paramsIterator = baseHref.getParameterMap().keySet().iterator();
                while (paramsIterator.hasNext())
                {
                    String key = (String) paramsIterator.next();

                    // don't remove parameters added by the table tag
                    if (!this.paramEncoder.isParameterEncoded(key))
                    {
                        baseHref.removeParameter(key);
                    }
                }
            }
            else
            {
                for (int j = 0; j < splittedExcludedParams.length; j++)
                {
                    baseHref.removeParameter(splittedExcludedParams[j]);
                }
            }
        }

        if (this.requestUri != null)
        {
            // if user has added a requestURI create a new href
            String fullURI = requestUri;
            //if (!this.dontAppendContext)      //Comment this line
            if (!fullURI.startsWith("javascript:"))

            {

           
                //String contextPath = ((HttpServletRequest) this.pageContext.getRequest()).getContextPath();     //Comment this line
                if (!this.dontAppendContext)
                {
                    String contextPath = ((HttpServletRequest) this.pageContext.getRequest()).getContextPath();


                   
                // prepend the context path if any.
                // actually checks if context path is already there for people which manually add it
//                if (!StringUtils.isEmpty(contextPath)         //Comment this line
//                    && requestUri != null    
//Comment this line
//                    && requestUri.startsWith("/")     //Comment this line
//                    && !requestUri.startsWith(contextPath))    //Comment this line
//                {     //Comment this line
//                    fullURI = contextPath + this.requestUri;     //Comment this line
//                }     //Comment this line

                    // prepend the context path if any.
                    // actually checks if context path is already there for people which manually add it
                    if (!StringUtils.isEmpty(contextPath)
                    && requestUri != null
                    && requestUri.startsWith("/")
                    && !requestUri.startsWith(contextPath))
                    {
                    fullURI = contextPath + this.requestUri;
                    }

                }
                    // call encodeURL to preserve session id when cookies are disabled
                    fullURI = ((HttpServletResponse) this.pageContext.getResponse()).encodeURL(fullURI);


            }

            // call encodeURL to preserve session id when cookies are disabled
            //fullURI = ((HttpServletResponse) this.pageContext.getResponse()).encodeURL(fullURI);
  //Comment this line

            baseHref.setFullUrl(fullURI);

            // // ... and copy parameters from the current request
            // Map parameterMap = normalHref.getParameterMap();
            // this.baseHref.addParameterMap(parameterMap);
        }
       
        baseHref.setTableTagAndRequest(this, requestHelper);

    }




There are two JSPs
-    Page1.jsp will have the necessary code to perform the request and update the div.
-    Page1Table.jsp will return the DisplayTag table.


Page1.jsp

I used dojo for request/response operation and to update the div.

<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ taglib uri="http://displaytag.sf.net" prefix="display" %>
<%@ taglib prefix="sx" uri="/struts-dojo-tags" %>

<html>
    <head>
        <title>DisplayTag with ajax</title>
        <sx:head />
        <link rel="stylesheet" type="text/css" href="displaytag.css" />

<SCRIPT>   
  
function submitAction(listener) {
    dojo.event.topic.publish(listener);
}

function updateDisplayTable(parm, actionName, divName) {
   
    requestURL = (actionName + "?" + parm).replace(new RegExp('&amp;', 'g'), '&');
      var id = 1;
      var kw = {
           preventCache: true,
         url:   requestURL,
         handler:   function(type, data, evt) {
                 var displayDiv = dojo.byId(divName);
             displayDiv.innerHTML = data;
              },
         mimeType: "text/html"
      };
      dojo.io.bind(kw);
}

</SCRIPT>
     </head>
    
    <body bgcolor="#FFFFFF" leftmargin="0" topmargin="0" marginwidth="0" marginheight="0" >
    <s:form id="frm" name="frm">
    <table class="DisplayTag">
        <tr>
            <td>
                <table class="DisplayTag" >
                    <tr>
                        <td><s:label value="Some Criteria Value:" /></td>
                        <td><s:textfield key="someCriteria" /></td>
                        <td ><input type="button" value="Submit" Class="Button" onclick="submitAction('listen_TableDetails');" /></td>
                    </tr>
                </table>
            </td>
        </tr>
        <tr>       
            <td>
                <div id="tableDiv"></div>
       
                <s:url id="urlTableDetails" value="Page1Table.action" />
                <sx:bind formId="frm" targets="tableDiv" listenTopics="listen_TableDetails" href="%{#urlTableDetails}"  />   
               
            </td>
      </tr>
    </table>
    </s:form>
    </body>
</html>

Page1Table.jsp

<%@ taglib prefix="s" uri="/struts-tags" %>
<%@ taglib uri="http://displaytag.sf.net" prefix="display" %>

            <display:table name="result" uid="table1" class="displayTag" sort="external" id="displayTagID" pagesize="10" partialList="true" size="${listSize}" excludedParams="*" requestURI="javascript:action='Page1Table.action';updateDisplayTable({}, 'Page1Table.action', 'tableDiv');">
                <display:column title="Value" escapeXml="true" sortable="false"  >${displayTagID[0]}</display:column>
                <display:column title="Result" escapeXml="true" sortable="true" sortName="result" headerClass="sortable" >${displayTagID[1]}</display:column>
            </display:table>


Notice {} in requestURI . Because of the changes in source code, DisplayTag will now replace {} with the request link. Clicking the page number or sort header will now run a javascript fuction updateDisplayTable() which will perform the request operation and then update the tableDiv.



The Action Class
Page1.java



package project.action;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import javax.servlet.http.HttpServletRequest;

import org.apache.struts2.interceptor.ServletRequestAware;
import org.displaytag.tags.TableTagParameters;
import org.displaytag.util.ParamEncoder;

import com.opensymphony.xwork2.ActionSupport;

public class Page1 extends ActionSupport implements ServletRequestAware {
   
    private String listSize = "50";
    private List result;

    private String someCriteria;
   
    public String execute() throws Exception {
       
        System.out.println("method executed");
        return SUCCESS;
    }
   
    public String loadTableData() throws Exception {

        String pageNumber = getParameterFromRequest("displayTagID", TableTagParameters.PARAMETER_PAGE);
        String columnName = getParameterFromRequest("displayTagID", TableTagParameters.PARAMETER_SORT);
        String orderBy = getParameterFromRequest("displayTagID", TableTagParameters.PARAMETER_ORDER);
        if(pageNumber == null)
            pageNumber = "1";
        if(columnName == null)
            columnName = "result";
        if(orderBy == null)
            orderBy = "1";
       
        result = new ArrayList();
        for(int i=((Integer.parseInt(pageNumber)-1)*10) + 1;
                i<(Integer.parseInt(pageNumber)*10) + 1;
                i++) {
            List c = new ArrayList();
            c.add(i);
            c.add(pageNumber);
            result.add(c);
        }
               
        return SUCCESS;
    }
   
    public String getListSize() {
        return listSize;
    }

    public void setListSize(String listSize) {
        this.listSize = listSize;
    }

    public String getSomeCriteria() {
        return someCriteria;
    }

    public void setSomeCriteria(String someCriteria) {
        this.someCriteria = someCriteria;
    }

    public List getResult() {
        return result;
    }

    public void setResult(List result) {
        this.result = result;
    }

    private HttpServletRequest request;
    public void setServletRequest(HttpServletRequest request) {
        this.request = request;
    }

    public String getParameterFromRequest(String displayTagID, String paramenterType) {
        return request.getParameter((new ParamEncoder(displayTagID).encodeParameterName(paramenterType)));
    }
   
}

Mapping
In struts.xml, I mapped Page1.action with execute() method of Action class and Page1Table.action with loadTableData() method of action class.




Thats it........ now DisplayTag is fully AJAXed.

To get full working displayTag library with AJAX support. Click Here - DisplayTagAJAXed.rar - 216.4 KB


Happy Programming!

6 comments:

  1. Can we have the zip source code please ?
    Thank a lot.

    ReplyDelete
  2. for (j = 0; j < previousArray.length; j++)
    {
    newArray[j] = previousArray[j];
    }

    should be written as

    System.arraycopy(previousArray, 0, newArray, 0, previousArray.length);

    ReplyDelete
  3. great!! i want to probe, do you have the new library (jar) to download ?

    ReplyDelete
  4. please provide me with the code without the jars.
    THansk A Lot!

    ReplyDelete
  5. sorry guys for replying this late.....
    its been a long time since I posted this....

    I'll look into my code repository for the sample. If I find it, I'll upload it.

    but you can always download the source and the binary files for displayTag from
    http://sourceforge.net/projects/displaytag/files/

    sorry again for replying this late. I'm really lazy :(

    ReplyDelete
  6. Gr8 JOB,i am highly impressed,just copied from your code seems to work flawlessly in my project.
    i searched everywhere & tried everything but yours solution is only worked without a hitch.

    thx for the valuable blog.
    Keep up the marvelous work.

    ReplyDelete