Five steps to dynamic tables using Struts 2, Dojo and JSON Ajax developers know that tables can do a lot more than display static information in an organized, readable format. By syncing up your table rows with a server-side database, you can create dynamic tables that sort, filter and paginate data on the fly. In this article, Sun Certified Java Developer Oleg Mikheev shows you how to use Struts 2, Dojo and JSON to display server-side data in an Ajax-style table. In the process, he introduces some of the Ajax-friendly features of Struts 2, including its integration with WebWork, Guice and the Dojo toolkit.Almost every Web application incorporates tables, and Ajax-style Web apps are no exception. Tables in Ajax applications can’t just display information like they would in any old Web app, however: Ajax implies dynamic filtering, sorting and pagination. There are many solutions for addressing dynamic, tabular functionality on the client side, but this approach doesn’t satisfy most project requirements. Dealing with hundreds of records on the client is too complicated a task for even a modern processor. More important, in an Ajax application the data is expected to be always in sync with the current database state.Several Ajax solutions simplify Ajax programming by converting server-side Java code into JavaScript, but at the cost of flexibility. Most developers don’t want to be restricted by the dictated programming model and limited number of components these frameworks provide. Given the options, creating a custom Ajax table really is your best choice. In this article I propose a custom solution that works and I show you how to implement it using Struts 2. Along the way, you get a quick tour of some of the frameworks within the framework that is Struts 2 — namely OpenSymphony WebWork/XWork, Guice and Dojo — and find out how Struts 2 integrates these powerful allies to make Ajax development a pleasure.If you need an immediate solution to the problem of displaying server-side data in an Ajax application, cut to the quick list. You’ll get a five-step solution with code samples you can use today. If you’re more generally interested in Ajax development, or want to learn about Ajax support in Struts 2, then read on.Note that this article assumes you are conceptually familiar with Ajax development. Experience with Struts 2, Dojo and JSON is helpful but not required. Creating dynamic tablesAjax support in Struts 2Struts 2 combines the best open source frameworks available to ease Ajax development. To control webflow on the server side it utilizes OpenSymphony WebWork/XWork. For dependency injection without the configuration headache it uses Guice. On the client side it utilizes Dojo, the JavaScript toolkit. Struts 2 also supports JSON, a subset of JavaScript whose abbreviated syntax makes it better suited to data interchange than XML. A typical Ajax table — that is, one whose data is dynamically filtered, sorted and paginated — must receive two pieces of information from the server for every instance of data display: records comprising the single current page of data, and pagination data. The records are taken from the sorted and filtered whole data set. The paging information consists of the current page number and total number of records in the data set after filtering. Let’s call the first one getRows and the second one getRowCount.Passing contextual dataBoth getRows and getRowCount require contextual information, such as what page number is being requested, what column to sort by and how to filter the requested records. The total number of records doesn’t depend on sorting and pagination, so getRowCount only needs to know the filtering criteria. A Map is a simple but sufficient container for this information. Map keys are field names and Map values are criteria field values. getRows additionally needs to know which page number to display and how to sort the data. The Java bean in Listing 1 holds contextual information to be transported from and to getRows and getRowCount.Listing 1. Java bean to transport pagination, sorting, and filtering datapublic class SortedPagedFilter { private byte direction; // direction of sorting private String field; // field to sort by private Map criteria; // filtering criteria private int page; // current page // accessors omitted } Retrieving contextual dataMost of your effort will be spent on coding the logic to return contextual information from the database. You probably are aware of the various techniques for getting a paged record set from a database. If you’re using the Java Persistence API (via a JPA persistence provider like Hibernate 3) you can instruct its Query interface to return a page using setMaxResults and setFirstResult. Your JPA implementation should support a JDBC driver capable of handling your database capabilities. It should also be advanced enough to know how to issue an SQL query that will actually return the exact number of records for a given page. In this case your work is done. Unfortunately, in some cases setMaxResults and setFirstResult will only mimic the expected behavior. These calls will actually transport the entire recordset from the database, cutting off redundant records at the JVM. For applications with many database records, there is a good chance you will need to write an SQL query native to your application database. If you are using ORM (object-relational mapping) you will have to retrieve a list of native object IDs, which you will later use in your ORM-specific query to retrieve the list of entities. For JPA it will look something like:select o from Order where o.id in (3,6,21,30) If you are not using ORM, POJO beans will be populated using the same approach manually. Once you’ve retrieved the actual page data from the server (getRows) getting the pagination data isn’t a big deal: a simple SQL query is enough to retrieve a count of rows with a filter applied.Setting up the JSP pageYour server side is ready to go and it’s time to prepare the display to render all of your gathered data into a table. The first thing you need to do is import the required Dojo components, as shown in Listing 2. Listing 2. Initializing required Dojo components<script type="text/javascript"> dojo.require("dojo.rpc.*"); dojo.require("dojo.widget.*"); dojo.require("dojo.widget.FilteringTable"); dojo.hostenv.writeIncludes(); </script> Dojo’s FilteringTable component is capable of displaying data in JSON (JavaScript Object Notation) format. Of course its look and feel is completely customizable with CSS. Your table definition should include headers only, with the field property set to the property to display, as shown in Listing 3.Listing 3. Table definition<table class="legacyTable" class="filterable" dojoType="filteringTable" id="yourTableId" multiple="false" alternateRows="true" valueField="id"> <thead> <tr> <th field="id" dataType="String">ID</th> <th field="firstName" dataType="String">First Name</th> <th field="secondName" dataType="String">Second Name</th> </tr> </thead> </table> Note that FilteringTable was created to filter and sort data on the client side. You’ll need to disable this functionality in order to proceed with filtering and sorting on the server side, as shown here:var yourTable = dojo.widget.byId('yourTableId'); yourTable.createSorter = function() { return null;}; You now have two pieces of information (first name and second name) on the client side formatted as JSON strings. It’s a good time to think about how you will transport the data between server and client. Using Dojo and JSON for data transportMany JavaScript-to-Java RPC solutions are available, most notably DWR. Struts 2 actually cuts out the need to for third-party libraries because it comes bundled with Dojo, which has RPC built in. What’s more, the JSON plugin for Struts 2 automatically converts JSON-formatted strings to Java objects.Preparing for JSON RPC callsYou need to do two things to expose your server-side Action methods to JavaScript on the client. The first is to edit your Struts 2 configuration XML file, as shown in Listing 4.Listing 4. Struts 2 configuration for RPC calls<package name="RPC" namespace="/nodecorate" extends="json-default"> <action name="SomeListRpc" class="some.ActionClass" method="execute"> <interceptor-ref name="json"> <param name="enableSMD">true</param> </interceptor-ref> <result type="json"> <param name="enableSMD">true</param> </result> </action> </package> The second thing you need to do is mark the ActionClass methods to be exposed as SMD (Standard Method Description format), as shown here: @SMDMethod public List<Some> getRows(SortedPagedFilter filter) {...} @SMDMethod public long getRowCount(SortedPagedFilter filter) {...} Calling a Java method from JavaScriptCalling a Java method from JavaScript is as easy as calling a JavaScript function, but you need to have JsonService defined on the page:<s:url id="smdUrl" namespace="/nodecorate" action="SomeListRpc"/> var service = new dojo.rpc.JsonService("${smdUrl}"); With the JSON plugin, passing a parameter to a Java method is as easy as creating a JSON string and passing it into a JavaScript function. Dojo RPC calls a callback function once the return value is received from the Java method, and the JSON plugin transparently converts the return value of any type (including collections) into a JSON string. Of course, if an object hierarchy exists it converts the whole hierarchy of objects.If you had a currentPage JavaScript variable on a page, the JavaScript code to pass a filter to getRows and get a JSON-formatted response would look like what you see in Listing 5. Listing 5. Preparing contextual info and calling getRows and getRowCount// define a callback function that will be called once a response is //received var rowsCallback = function(bean) { var yourTable = dojo.widget.byId('yourTableId'); yourTable.store.setData(bean); }; // construct a filter based on the current FilteringTable sort information var sortInfo = yourTable.sortInformation[0]; var filter = {field:yourTable .columns[sortInfo.index].field,direction:sortInfo.direction,page:current Page}; // add criteria to the filter, dynamically construct your own one // instead of the hardcoded one below filter['criteria'] = {firstName:'Ivan',secondName:'Smith'}; var deferred = service.getRows(filter); // start the RPC process deferred.addCallback(rowsCallback); var rowNumCallback = function(rowNumber) { totalPages = Math.ceil(rowNumber / 20); // call your fillPagination function rendering pagination info fillPagination(totalPages); } // you don't need sort and page information to count rows, just filtering //criteria var filter = {field:'',direction:1,page:0}; filter['criteria'] = {firstName:'Ivan',secondName:'Smith'}; var deferred = service.getRowCount(filter); // start the RPC process deferred.addCallback(rowNumCallback); In this case getRows returns a list of entities, which are Java beans. Sometimes you don’t want to transfer all of your bean properties to the client side, however. For example, when a table has fewer columns than the bean has, or when you are transferring a Hibernate entity but you don’t want to receive the service fields Hibernate attaches to its entities. You can control all this by adding the next line to the result part of the original action configuration shown in Listing 4. Keep in mind that the comma-separated strings are Java regex patterns.<param name="excludeProperties">prop1,moreProp</param> Let the table live!You’re now ready to put the finishing touches on your dynamic, Ajax-style table and push it out into the world. The remaining steps are to implement UI pagination control and to add sorting and row-select handles to the table.As you will notice, both pagination control and table sorting are enabled by the refreshData function. In both cases refreshData is supposed to refresh the whole table with up-to-date data. The complete refreshData function is shown in Step 4 of the Ajax tables quick list below. Pagination controlGood pagination data should consist of a clickable list of the total pages within the current filter, with the current page emphasized. Optionally, it should also include an indicator of the total rows found. This information is deducted from the data returned by getRowCount combined with the current page number, which should be always available on the screen. Given a span or div devoted to pagination data, whose id was testTablePagination, you would render the pagination data as follows:Listing 6. Rendering pagination datafillPagination = function(totalPages) { var el = document.getElementById('testTablePagination'); el.innerHTML = 'Pages: '; for(var i=0; i<this.totalPages; i++) { if(currentPage==i) { el.innerHTML += '<b>' + (i+1) + '</b> '; } else { el.innerHTML += '<a href="#" onclick="currentPage=' + i + '; refreshData();">' + (i + 1) + '</a> '; } } } Extending FilteringTable with Dojo AOPOnce all the data is displayed you need to extend Dojo’s FilteringTable functionality, adding custom handling for dynamic sort and click events. Internally, Dojo implements an aspect-oriented programming model, which means that it’s possible to extend each aspect of Dojo functionality (such as FilteringTable) without modifying its underlying code. You do this simply by creating a new aspect. In AOP, an aspect is a combination of an advice (piece of code to be executed), and a joinpoint (the point when and where this code is to be executed) described using a particular syntax called a pointcut.Dojo provides helper method to make customizing built-in functions easier. The helper method dojo.event.connect will allow you to execute a function before or after any JavaScript function call, at any given joinpoint. The methods FilteringTable calls on sort and click events are onSort and onSelect. Before weaving an advice into either of these methods, Dojo has to initialize the whole page so that all the Dojo widgets are constructed. To ensure your weaving happens right after page initialization, you should add it as an onLoad function to the Dojo container, as demonstrated by the JavaScript in Listing 7. Listing 7. Weaving in table aspects after initialization_container_.addOnLoad(function() { dojo.event.connect("after", yourTable, "onSelect", function () { onSelectFunc(); }); dojo.event.connect("after", yourTable, "onSort", function () { refreshData(); }); } You’ve now extended FilteringTable to call an onSelectFunc function when the user clicks a table row and refreshData when the user clicks a table heading. Your next step is implement both of these dynamic functions.A note about table definitionIn an Ajax-style table, a user click on a table row usually results in a detailed view of that row. This is possible only if a unique identity for the row has been provided in the table definition (which implies that one of the table row properties should be unique). Looking back to Listing 3, you will notice the valueField table was set with id value. In Listing 8 a unique identifier is looked up from the table row to be further used in detailed view display process.Listing 8. On table row select functiononSelectFunct = function() { var id = dojo.widget.byId(tableId).getSelectedData().id; // code to load row details by its 'id' property } If you’ve followed the discussion so far you’ve learned something about the theoretical basis for creating dynamic tables in Struts 2. Now let’s go through these steps again, but more quickly. You can follow the code samples in the next section to develop your own dynamic, Ajax-style table using Struts 2. See the Resources section to download the source code. Ajax tables quick listIf you’re looking for a quick list and some easy code that you can use to create an Ajax-style table using Struts 2, then here it is: Five steps to develop an Ajax-style table that dynamically sorts, filters, and paginates server-side data.Code a filter and two methods in a Struts 2/WebWork Action, returning a List of rows for the current page and the total row count: public class SortedPagedFilter { private byte direction; // direction of sorting private String field; // field to sort by private Map criteria; // filtering criteria private int page; // current page // accessors omitted } public class SomeAction extends ActionSupport { @SMDMethod public List<Some> getRows(SortedPagedFilter filter) {...} @SMDMethod public long getRowCount(SortedPagedFilter filter) {...} } Configure Struts 2: <package name="RPC" namespace="/nodecorate" extends="json-default"> <action name="SomeListRpc" class="some.ActionClass" method="execute"> <interceptor-ref name="json"> <param name="enableSMD">true</param> </interceptor-ref> <result type="json"> <param name="enableSMD">true</param> </result> </action> </package> Define a service, global currentPage variable, paging area, and table; import the Dojo required components onto a JSP page: <script type="text/javascript"> var djConfig = { isDebug: false }; </script> <script src="/MGA/struts/dojo/dojo.js" type="text/javascript"></script> <script type="text/javascript"> dojo.req uire("dojo.rpc.*"); dojo.req uire("dojo.widget.*"); dojo.req uire("dojo.widget.FilteringTable"); dojo.hostenv.writeIncludes(); </script> <s:url id="smdUrl" namespace="/nodecorate" action="SomeListRpc"/> <script type="text/javascript"> var currentPage = 0; var service = new dojo.rpc.JsonService("${smdUrl}"); </script> First Name: <input type="text" id="filterFirstName"> Second Name: <input type="text" id="filterSecondName"> <button onclick="refreshData()">Search</button><br> <span id="testTablePagination"></span> <table class="legacyTable" class="filterable" dojoType="filteringTable" id="yourTableId" multiple="false" alternateRows="true" valueField="id"> <thead> <tr> <th field="id" dataType="String">ID</th> <th field="firstName" dataType="String">First Name</th> <th field="secondName" dataType="String">Second Name</th> </tr> </thead> </table> Define the methods to handle data and page refreshes: <script type="text/javascript"> refreshData = function() { // define a callback function for row data var rowsCallback = function(bean) { var yourTable = dojo.widget.byId('yourTableId'); yourTable.store.setData(bean); }; var yourTable = dojo.widget.byId('yourTableId'); // construct a filter based on the current FilteringTable sort information var sortInfo = yourTable.sortInformation[0]; var filter = {field:yourTable .columns[sortInfo.index].field,direction:sortInfo.direction,page:current Page}; // add criteria to the filter, dynamically construct your own filter['criteria'] = {firstName:document.getElementById('filterFirstName').value,secondName:document.getElementById('filterSecondName').value}; var deferred = service.getRows(filter); // start the RPC process deferred.addCallback(rowsCallback); var rowNumCallback = function(rowNumber) { var totalPages = Math.ceil(rowNumber / 20); // call your fillPagination function rendering pagination info fillPagination(totalPages); } // you don't need sort and page info to count rows, just filtering //criteria filter = {field:'',direction:1,page:0}; filter['criteria'] = {firstName:document.getElementById('filterFirstName').value, secondName:document.getElementById('filterSecondName').value}; var deferred = service.getRowCount(filter); // start the RPC process deferred.addCallback(rowNumCallback); } fillPagination = function(totalPages) { var el = document.getElementById('testTablePagination'); el.innerHTML = 'Pages: '; for(var i=0; i<totalPages; i++) { if(currentPage==i) { el.innerHTML += '<b>' + (i+1) + '</b> '; } else { el.innerHTML += '<a href="#" onclick="currentPage=' + i + '; refreshData();">' + (i + 1) + '</a> '; el.innerHTML += (i+1) + '</a> '; } } } </script> Use Dojo aspects to replace FilteringTable‘s sort and click functionality: dojo.addOnLoad(function() { var yourTable = dojo.widget.byId('yourTableId'); yourTable.createSorter = function() { return null;}; dojo.event.connect("after", yourTable, "onSelect", function () { onSelectFunc(); }); dojo.event.connect("after", yourTable, "onSort", function () { refreshData(); }); }); In conclusionIn this article you’ve learned how to create dynamic, Ajax-style tables using Struts 2. The five-step solution in the quick list results in a simple table that sorts, filters and paginates server-side data. Coding on the server-side is more scalable (and more authentically dynamic) than a client-side solution, and using Struts 2 keeps you free of the programming constraints of a framework like GWT or Echo. Besides, Struts 2 provides all the Ajax development support you could want, doesn’t it?Oleg Mikheev is a Sun Certified Java Developer and IBM WebSphere Portal Developer with 10 years of experience with Java technologies. Oleg is currently employed as systems analyst by Gemini Systems, and is a postgraduate student at St.-Petersburg State Polytechnical University, Russia. Open SourceWeb DevelopmentJavaDevelopment ToolsTechnology IndustrySoftware DevelopmentDevelopment Approaches