Running the Table With JMesa
Join the DZone community and get the full member experience.
Join For FreeShhhh. I’ll tell you a secret. I don’t like tables.
I know. Shocking, isn't it?
Don't get me wrong: I don't dislike tables per se. They're great for displaying tabular material. (For page organization, not so much.) But I so dislike the code needed to build a table within a JSP. It usually comes down to something like this:
<c:forEach items="rows" var="row" varStatus="status" begin="${begin}"
end="${end}" step="${step}">
<c:if test="${status.first}">
<table border="0" cellspacing="0" cellpadding="0">
<tr>
<th>User ID</th>
<th>Name</th>
<th>Email</th>
</tr>
</c:if>
<c:choose>
<c:when test="${status.count % 2 == 0}">
<tr class="even">
</c:when>
<c:otherwise>
<tr class="odd">
</c:otherwise>
</c:choose>
<td>${row.userID}</td>
<td>${row.name}</td>
<td>${row.email}</td>
</tr>
<c:if test="${status.last}">
</table>
</c:if>
</c:forEach>
All that iterative logic simply looks incomprehensible to me. It's still better than scriptlets or custom tag libraries (both of which were, to be sure, phenomenal in their time), but it's an undigestible mass, and even if I do step through it line by line and understand what it does, I'm still left with just a table. Users accustomed to active, Javascript-assisted widgets don't respond to tables that just lie there. Many more lines of code will be needed to enable them to do useful things like paginating through long lists of items, sorting by column values, and the like. It'll be an unholy mix of HTML, JSP directives, JSP tags, EL, Javascript, Java, XML, properties files, and so forth. The whole thing seems so error-prone (note to self: more code + more languages = more "opportunities" for bugs).
But recently I discovered an open-source Java library called JMesa that provides another way. I'm going to share with you some of the things I've found in JMesa, building up an HTML page containing a table from nothing to, well, considerably more than nothing.
There's a good deal of code here, to give you a sense of the JMesa API; hopefully. you'll come away with some ideas about how you can use JMesa in your own projects. I won't bother with package declarations, imports, or code not relevant to the point at hand; the complete code is available for download in the form of an Eclipse project. Installation instructions will be found at the end of this article.
Join me in exploring JMesa!
Preparation
A Page to Show
Before we can get to JMesa, though, we'll need a few things: a page within which to display our table, for instance. In fact, we'll learn even more if we put this page in a context. I have recently fallen in like with Spring MVC and so will use that to build a simple site with a few pages. Just to be clear, while Spring dependency injection and utilities are woven into the code below, JMesa does not depend upon Spring. The pages are not fancy, and I am going to skip most of the setup. Everything is included in the download, of course.
One thing I shouldn't skip is the controller for the search results page, the page within which we will build our table. We'll start with pretty much the simplest functionality we can:
public class SimpleSearchController extends AbstractController
{
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest
request, HttpServletResponse response) throws Exception
{
return new ModelAndView("simple-results", "results",
"Here we will display search results");
}
}
For those not familiar with Spring MVC, the ModelAndView return value contains a string that will be resolved to a view (in this project, it is resolved to "/WEB-INF/jsp/simple-results.jsp"), and a key-value pair (the second and third constructor arguments) that can be accessed using EL on the JSP page:
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>
<%@ taglib prefix="tags" tagdir="/WEB-INF/tags" %>
<tags:layout-top>
<jsp:attribute name="titleBar"><spring:message
code="title.bar.results"/></jsp:attribute>
<jsp:attribute name="titleInternal"><spring:message
code="internal.title.results"/></jsp:attribute>
</tags:layout-top>
${results}
<tags:layout-bottom/>
Finally, we use the Spring jmesa-servlet.xml configuration file to create and associate a URL with our controller:
<bean id="simpleSearchController"
class="com.javalobby.article.jmesa.web.SimpleSearchController"/>
<bean id="urlMapping"
class="org.springframework.web.servlet.handler.SimpleUrlHandlerMapping">
<property name="mappings">
<props>
<prop key="/welcome.html">welcomeController</prop>
<prop key="/search.html">simpleSearchController</prop>
</props>
</property>
</bean>
Clicking on the "Search" link in the menu now produces:
[img_assist|nid=3678|title=Figure 1.|desc=A simple page for our table|link=none|align=left|width=757|height=240]
All right, not much. But it's the page we need.
Something to Display
Another thing we need before we can build a table is something to show in it. This "domain" object should be pretty easy to display:
public class HelloWorld implements Comparable<HelloWorld>
{
private int pk;
private String hello = "Hello";
private String world = "world";
private String from = "from";
private String firstName;
private String lastName;
private String format = "{0}, {1}! {2} {3} {4}";
// ... accessors and mutators
public String toString()
{
return MessageFormat.format(getFormat(), hello, world,
from, getFirstName(), getLastName());
}
// ... implementations of equals, hashCode, and compareTo
}
Persistence Service
Of course, we need instances of this domain object. Normally, we'd get them from a persistence service; for now, we'll just create them in memory:
public class HelloWorldService
{
private int nextId;
private Set<HelloWorld> helloWorlds = new TreeSet<HelloWorld>();
public HelloWorldService()
{
nextId = 1;
helloWorlds.add(newInstance("Albert", "Einstein"));
helloWorlds.add(newInstance("Grazia", "Deledda"));
helloWorlds.add(newInstance("Francis", "Crick"));
helloWorlds.add(newInstance("Linus", "Pauling"));
helloWorlds.add(newInstance("Theodore", "Roosevelt"));
helloWorlds.add(newInstance("Hideki", "Yukawa"));
helloWorlds.add(newInstance("Harold", "Urey"));
helloWorlds.add(newInstance("Barbara", "McClintock"));
helloWorlds.add(newInstance("Hermann", "Hesse"));
helloWorlds.add(newInstance("Mikhail", "Gorbachev"));
helloWorlds.add(newInstance("Amartya", "Sen"));
helloWorlds.add(newInstance("Albert", "Gore"));
helloWorlds.add(newInstance("Amnesty", "International"));
helloWorlds.add(newInstance("Daniel", "Bovet"));
helloWorlds.add(newInstance("William", "Faulkner"));
helloWorlds.add(newInstance("Otto", "Diels"));
helloWorlds.add(newInstance("Marie", "Curie"));
}
public Set<HelloWorld> findAll()
{
return helloWorlds;
}
private HelloWorld newInstance(String firstName, String lastName)
{
HelloWorld hw = new HelloWorld();
hw.setPk(nextId++);
hw.setFirstName(firstName);
hw.setLastName(lastName);
return hw;
}
}
That's that. Now we're ready to focus on JMesa.
JMesa
Let's start with something extremely simple. On the very first page of the JMesa web site we find four lines of code that we can appropriate and refashion for a Spring controller:
public class BasicJMesaSearchController extends AbstractController
{
private HelloWorldService helloWorldService;
public void setHelloWorldService(HelloWorldService helloWorldService)
{
this.helloWorldService = helloWorldService;
}
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest
request, HttpServletResponse response) throws Exception
{
Set<HelloWorld> results = helloWorldService.findAll();
TableFacade tableFacade = new TableFacadeImpl("results",request);
tableFacade.setItems(results);
tableFacade.setColumnProperties("pk", "firstName", "lastName", "format");
return new ModelAndView("results", "results", tableFacade.render());
}
}
We let Spring inject the HelloWorldService, which we use to retrieve a set of items to display. Then we create and configure the JMesa TableFacade class. This class takes an HTTP request in its constructor: TableFacade is going to send itself messages passed as parameters in the request (more on this in a moment). We supply it with the set of items and with which JavaBean property of those items we want displayed in each column.
We'll also need a bit of new code in the search results page (in the project, this is actually a different search results page, as you, oh sharp-eyed reader, have already noticed):
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="tags" tagdir="/WEB-INF/tags"%>
<tags:layout-top>
<jsp:attribute name="titleBar"><spring:message
code="title.bar.results"/></jsp:attribute>
<jsp:attribute name="titleInternal"><spring:message
code="internal.title.results"/></jsp:attribute>
</tags:layout-top>
<form name="resultsForm" action="search.html">
${results}
</form>
<script type="text/javascript">
function onInvokeAction(id, action)
{
setExportToLimit(id, '');
createHiddenInputFieldsForLimitAndSubmit(id);
}
function onInvokeExportAction(id, action)
{
location.href="${pageContext.request.contextPath}/search.html?"
+ createParameterStringForLimit(id);
}
</script>
<tags:layout-bottom/>
And we'll need to create and point to the new controller in jmesa-servlet.xml:
<bean id="helloWorldService"
class="com.javalobby.article.jmesa.entity.HelloWorldService"/>
<bean id="basicSearchController"
class="com.javalobby.article.jmesa.web.BasicJMesaSearchController">
<property name="helloWorldService" ref="helloWorldService"/>
</bean>
...
<prop key="/search.html">basicSearchController</prop>
Redeploy, and the results look like magic. How did we get them?
[img_assist|nid=3679|title=Figure 2.|desc=Using JMesa "out of the box"|link=none|align=left|width=757|height=405]
The key is in the variable results, which now holds the entire text of the table generated by the JMesa TableFacade when we called its render method. We also put a self-submitting HTML form around the JMesa table that it will use to send itself messages about how to alter itself.
This makes possible many amazing features. The table automagically paginates itself. It allows the user to change the number of rows displayed. It allows sorting on any column or combination of columns. It provides color striping of table rows and onMouseOver row highlighting. And every bit of this came for free: we did nothing to enable it but what you have already seen. (OK, we played around with some of JMesa's images and CSS style sheets to make it fit in with our color scheme, but that really shouldn't count.)
To demonstrate, we'll use the select at the top of the form to change the number of rows displayed to 16, sort by first name ascending and last name descending (by clicking on the first column header once and the second twice), and mouse over the third row to see the highlighting:
[img_assist|nid=3681|title=Figure 3.|desc=JMesa search results sorted and highlighted|link=none|align=left|width=757|height=565]
Now Al Gore and Einstein appear in the order we asked for.
You will have noticed the images in the table toolbar. Those on the left are standard first, previous, next, and last navigation icons. The select we've already mentioned. But there are two other images as well: these turn filtering, another amazing feature of JMesa that is active by default, on and off.
Filtering allows the user to apply expressions to a column in order to display only rows having matching values in that column. While filtering can take setup beyond the scope of this article, even by default it's astonishing. Try typing "Einstein" in the text field that appears above the last-name column header and clicking on the filter icon (the magnifying glass). The results show only the row containing Einstein's name in the last name column. And we didn't have to do a thing!
[img_assist|nid=3680|title=Figure 4.|desc=JMesa search results filtered|link=none|align=left|width=757|height=280]
See the JMesa web site for details about filtering, editable tables that keep track of your changes for you, and much, much more: it's impressive stuff.
Customizing
And now, to business. The JMesa default is astounding, but no default is ever exactly like you want it. The ability to customize is critical. Also, defaults rarely exercise every feature, and this one is no exception.
Let's start with some requirements:
- We will display the value of each HelloWorld item's toString method in an additional column
- We will display more user-friendly values in the format column
- We will ensure that columns that cannot be reasonably sorted are made unsortable
- We will add columns containing links to edit and delete pages for the HelloWorld items
- We will display images in the edit and delete columns
- We will not display the Pk property of each item, but will pass its value to edit and delete pages as needed
- We will enable the user to retrieve a comma-separated-values (CSV) copy of the table contents
- We will enable the user to retrieve an Excel spreadsheet copy of the table contents
- We will disable filtering and highlighting
- We will reorganize the toolbar items in a different order
Believe it or not, implementing each of these features will be quite easy! and you'll begin to get a sense for the possibilities of JMesa.
ToString Column
Each HelloWorld item produces a formatted string within its toString method. This is not a JavaBean property method, so we cannot directly point the TableFacade at it. We want this value to be rendered (to use JMesa terminology) as the contents of a <td> (a cell) in each HTML row.
Cell contents are produced by implementations of the CellEditor interface. Its getValue method is passed the item to be displayed, the property to be called, and the current row count. Since only the item itself is actually needed for our purpose, the implementation is simple:
public class ToStringCellEditor implements CellEditor
{
@Override
public Object getValue(Object item, String property, int rowcount)
{
if (item == null)
{
return "";
}
return item.toString();
}
}
Of course, we'll need a column into which to put the results. All we need do is add an arbitrary value to the column properties list:
tableFacade.setColumnProperties("firstName", "lastName", "format", "toString");
This value is used to retrieve the column:
Row row = tableFacade.getTable().getRow();
Column column = row.getColumn("toString");
column.getCellRenderer().setCellEditor(new ToStringCellEditor());
Of course, this means that the getValue method of the ToStringCellEditor will always be passed a bogus property value, but since the editor doesn't use it, that's no problem. (Note that we've also left off the pk column as per requirements.)
User-Friendly Format Column
We continue by introducing a more user-friendly value into the format column. The format string "{0}, {1}! {2} {3} {4}" looks ugly and most likely won't be understood by an end user. The only real information it conveys is that it is the default value. We'll use a Spring MessageSource to supply something a little easier on the eyes at runtime.
First, we'll add a property to the messages.properties file loaded by Spring at application startup:
format.{0},\ {1}!\ {2}\ {3}\ {4}=Default format
(The backslashes are needed to escape the white space in the key.)
As we have already seen, a CellEditor is needed to change a cell's displayed value. Using MessageSource to produce the display value at runtime requires a few more lines than the ToStringCellEditor:
public class SpringMessageCellEditor implements CellEditor
{
MessageSource source;
String prefix;
Locale locale;
public SpringMessageCellEditor(MessageSource source, String prefix,
Locale locale)
{
this.source = source;
this.prefix = prefix;
this.locale = locale;
}
public Object getValue(Object item, String property, int rowcount)
{
if (item != null)
{
try
{
return source.getMessage(prefix + "." +
PropertyUtils.getProperty(item,
property), null, locale);
}
catch (IllegalAccessException ignore) { }
catch (InvocationTargetException ignore) { }
catch (NoSuchMethodException ignore) { }
}
return null;
}
}
We still have to add this editor to the column displaying the format property:
Column column = row.getColumn("format");
column.getCellRenderer().setCellEditor(new
SpringMessageCellEditor(messageSource, "format", locale);
Unsortable Columns
Next, we want the table to know that some columns are unsortable. Columns are typically sorted by property value, but we just added a column that corresponds to no property, that displays the output of the toString method. If the user clicked on the header of that column, he or she would wind up with a very ugly NullPointerException message.
Making a column (actually, we need to have an HtmlColumn, but most columns qualify) unsortable is very simple:
htmlColumn.setSortable(false);
With this, no onClick method will be generated for the column header, preventing users from accidentally causing a mess.
Edit and Delete Columns
Now we'll add columns containing links to edit and delete pages for HelloWorld items. I prefer using icons to buttons saying "Edit" and "Delete", as it reduces the amount of textual information the user must process. Tables typically present a lot of information in a compact space, making user overload a problem worthy of attention.
To do this, we'll need a CellEditor (by now, you knew that was coming!). Since this is functionality I use a lot, let's design it for reuse, refactoring out reusable code into one class, and code tailored to this project into another.
ImageCellEditor encapsulates the general process of setting up an image with a link, and includes a method that will let subclasses override the default processing of the link:
public class ImageCellEditor extends AbstractContextSupport
implements CellEditor
{
private String image;
private String alt;
private String link;
public ImageCellEditor(String image, String alt, String link)
{
this.image = image;
this.alt = alt;
this.link = link;
}
public Object getValue(Object item, String property, int rowcount)
{
CoreContext context = getCoreContext();
String imagePath =
context.getPreference("html.imagesPath");
StringBuilder img = new StringBuilder();
if (link != null && link.trim().length() != 0)
{
img.append("<a href=\"")
.append(processLink(item, property, rowcount, link))
.append("\"/>");
}
img.append("<img src=\"")
.append(getWebContext().getContextPath())
.append(imagePath)
.append(image)
.append("\" title=\"")
.append(alt)
.append("\" alt=\"")
.append(alt)
.append("\"/>");
if (link != null && link.trim().length() != 0)
{
img.append("</a>");
}
return img.toString();
}
/**
* This method can be overridden by subclasses to handle specific
* HTML link needs.
*/
public String processLink(Object item, String property,
int rowcount, String link)
{
return link;
}
}
This is our opportunity to introduce CoreContext and WebContext, two important classes that plug our code into the JMesa infrastructure. Extending AbstractContextSupport gets us JavaBean property methods for these objects (just a convenience; I could have implemented the interface ContextSupport, but then I would have had to write the property methods myself).
The CoreContext has many uses; our immediate purpose for it is to retrieve a value configured in the jmesa.properties file. This was pointed to in web.xml:
<context-param>
<param-name>jmesaPreferencesLocation</param-name>
<param-value>WEB-INF/jmesa.properties</param-value>
</context-param>
It contains a preference called "html.imagesPath" that replaces the default path from which JMesa retrieves images:
html.imagesPath=/images/
This means we won't have to hard-code a part of the image URL. (There are a lot more configurable preferences: for details, see the JMesa web site.)
The WebContext provides us with the servlet context path, again letting us avoid hard-coding the image URL:
getWebContext().getContextPath()
Getting back to the two image columns, we have a requirement to pass the Pk property of the appropriate HelloWorld to the edit or delete pages when the images are clicked. Adding this property to the link is easy, using the MessageFormat class to process the link argument of the application-specific subclass:
public class HelloWorldImageCellEditor extends ImageCellEditor
{
public String processLink(Object item, String property,
int rowcount, String link)
{
return MessageFormat.format(link, ((HelloWorld) item).getPk());
}
}
After creating the editor, we can retrieve the context objects for it from the TableFacade:
ImageCellEditor editor = new HelloWorldImageCellEditor("edit.gif",
messageSource.getMessage("image.edit.alt", null, locale),
"edit.html?pk={0,number,integer}");
editor.setWebContext(tableFacade.getWebContext());
editor.setCoreContext(tableFacade.getCoreContext());
Now we have the images and the links. But it would be awfully nice if the images could be centered within the column, something notoriously difficult to achieve with CSS style sheets. What would work would be to use the align and valign attributes of the cell. How can we do that?
The cell itself, as opposed to its contents, is rendered by the interface CellRenderer. Unfortunately, the HtmlCellRenderer sub-interface that comes with JMesa has no method for adding attributes. The Decorator and Template patterns, however, come to the rescue. Again, we implement the functionality for reuse as two classes, the first a generic decorator with an additional template method:
public abstract class AttributedHtmlCellRendererDecorator implements
HtmlCellRenderer
{
// all other methods will be delegated to this renderer
protected HtmlCellRenderer renderer;
public AttributedHtmlCellRendererDecorator(HtmlCellRenderer renderer)
{
this.renderer = renderer;
}
public Object render(Object item, int rowcount)
{
HtmlBuilder html = new HtmlBuilder();
html.td(2);
html.width(getColumn().getWidth());
addAttributes(html);
html.style(getStyle());
html.styleClass(getStyleClass());
html.close();
String property = getColumn().getProperty();
Object value = getCellEditor().getValue(item, property, rowcount);
if (value != null)
{
html.append(value.toString());
}
html.tdEnd();
return html.toString();
}
/**
* Subclasses will add attributes.
*/
public abstract void addAttributes(HtmlBuilder html);
}
The second will be a subclass that adds the specific attributes we need:
public class AlignedHtmlCellRendererDecorator extends AttributedHtmlCellRendererDecorator
{
private String align;
private String valign;
public AlignedHtmlCellRendererDecorator(HtmlCellRenderer renderer,
String align, String valign)
{
super(renderer);
this.align = align;
this.valign = valign;
}
@Override
public void addAttributes(HtmlBuilder html)
{
html.align(align);
html.valign(valign);
}
}
Whew, that was a mouthful! However, our images will come out nicely centered in the column, and we've learned a good deal more about how the JMesa API works.
There will be edit and delete pages to link to, of course, but these are not of interest here and are completely trivial in the Eclipse project.
CSV and Excel Output
In JMesa terminology, output other than HTML is called exporting the table. As complex as it might seem, it's actually the easiest part of the process. Again, a single line of code will do all we need:
tableFacade.setExportTypes(response, org.jmesa.limit.ExportType.CSV,
org.jmesa.limit.ExportType.EXCEL);
That's really all there is to it! (OK, you have to include some JAR files in the library, but what did you expect, magic?)
Filtering and Highlighting
Making a row (we need an HtmlRow) unfilterable and unhighlighted is just as simple as making a column unsortable:
htmlRow.setFilterable(false);
htmlRow.setHighlighter(false);
With this, no filtering row or icons will be generated above the column header and the highlighting feature will be turned off.
Toolbar
The code to reorganize the toolbar is quite straightforward; while we're at it, we need to include icons for the various output formats:
public class ReorderedToolbar extends AbstractToolbar
{
@Override
public String render()
{
if (ViewUtils.isExportable(getExportTypes()))
{
addExportToolbarItems(getExportTypes());
addToolbarItem(ToolbarItemType.SEPARATOR);
}
MaxRowsItem maxRowsItem = (MaxRowsItem)
addToolbarItem(ToolbarItemType.MAX_ROWS_ITEM);
if (getMaxRowsIncrements() != null)
{
maxRowsItem.setIncrements(getMaxRowsIncrements());
}
addToolbarItem(ToolbarItemType.SEPARATOR);
addToolbarItem(ToolbarItemType.FIRST_PAGE_ITEM);
addToolbarItem(ToolbarItemType.PREV_PAGE_ITEM);
addToolbarItem(ToolbarItemType.NEXT_PAGE_ITEM);
addToolbarItem(ToolbarItemType.LAST_PAGE_ITEM);
return super.render();
}
}
I arranged the icons by simply specifying the order in which they are added to the toolbar. They look more natural to me this way; your mileage may vary. Note that we delegate the messy work of actually rendering the toolbar to the JMesa superclass.
Putting It All Together
We'll refactor out reusable code once more in writing a Factory to encapsulate building our customized table, starting with an abstract class:
public abstract class AbstractTableFactory
{
protected abstract String getTableName();
protected abstract void configureColumns(TableFacade
tableFacade, Locale locale);
protected abstract void configureUnexportedTable(TableFacade
tableFacade, Locale locale);
protected abstract ImageCellEditor getEditImageCellEditor(Locale
locale);
protected abstract ImageCellEditor getDeleteImageCellEditor(
Locale locale);
public TableFacade createTable(HttpServletRequest request,
HttpServletResponse response, Collection items)
{
TableFacade tableFacade = new TableFacadeImpl(getTableName(),
request);
tableFacade.setItems(items);
tableFacade.setStateAttr("return");
configureTableFacade(response, tableFacade);
Locale locale = request.getLocale();
configureColumns(tableFacade, locale);
if (! tableFacade.getLimit().isExported())
{
configureUnexportedTable(tableFacade, locale);
}
return tableFacade;
}
public void configureTableFacade(HttpServletResponse response,
TableFacade tableFacade)
{
tableFacade.setExportTypes(response, getExportTypes());
tableFacade.setToolbar(new ReorderedToolbar());
Row row = tableFacade.getTable().getRow();
if (row instanceof HtmlRow)
{
HtmlRow htmlRow = (HtmlRow) row;
htmlRow.setFilterable(false);
htmlRow.setHighlighter(false);
}
}
protected ExportType[] getExportTypes()
{
return null;
}
protected void configureColumn(Column column, String title,
CellEditor editor)
{
configureColumn(column, title, editor, false, true);
}
protected void configureColumn(Column column, String title,
CellEditor editor, boolean filterable, boolean sortable)
{
column.setTitle(title);
if (editor != null)
{
column.getCellRenderer().setCellEditor(editor);
}
if (column instanceof HtmlColumn)
{
HtmlColumn htmlColumn = (HtmlColumn) column;
htmlColumn.setFilterable(filterable);
htmlColumn.setSortable(sortable);
}
}
protected void configureEditAndDelete(Row row, WebContext webContext,
CoreContext coreContext, Locale locale)
{
HtmlComponentFactory factory = new
HtmlComponentFactory(webContext, coreContext);
HtmlColumn col = factory.createColumn((String) null);
col.setFilterable(false);
col.setSortable(false);
CellRenderer renderer = col.getCellRenderer();
ImageCellEditor editor = getEditImageCellEditor(locale);
editor.setWebContext(webContext);
editor.setCoreContext(coreContext);
renderer.setCellEditor(editor);
col.setCellRenderer(new
AlignedHtmlCellRendererDecorator((HtmlCellRenderer)
renderer, "center", "middle"));
row.addColumn(col);
col = factory.createColumn((String) null);
col.setFilterable(false);
col.setSortable(false);
renderer = col.getCellRenderer();
editor = getDeleteImageCellEditor(locale);
editor.setWebContext(webContext);
editor.setCoreContext(coreContext);
renderer.setCellEditor(editor);
col.setCellRenderer(new
AlignedHtmlCellRendererDecorator((HtmlCellRenderer)
renderer, "center", "middle"));
row.addColumn(col);
}
}
This has a lot of code (note the abstract methods ), in part because I know I usually want edit and delete columns. One line that might pass by unnoticed in all this, however, is really quite something:
tableFacade.setStateAttr("return");
When this attribute is set, JMesa uses the Memento design pattern to save the state of its tables. When you return to a table page and include the attribute you specify here in the URL, you return to the exact place you left: the page number to which you had moved before leaving the table, the number of values displayed per page, and so forth.
The application-specific concrete class, after all this, can be pretty simple:
public class HelloWorldTableFactory extends AbstractTableFactory
{
protected MessageSource messageSource;
public void setMessageSource(MessageSource messageSource)
{
this.messageSource = messageSource;
}
@Override
protected String getTableName()
{
return "results";
}
@Override
protected ExportType[] getExportTypes()
{
return new ExportType[] { CSV, EXCEL };
}
@Override
protected void configureColumns(TableFacade tableFacade, Locale locale)
{
tableFacade.setColumnProperties("firstName", "lastName",
"format", "toString");
Row row = tableFacade.getTable().getRow();
configureColumn(row.getColumn("firstName"),
messageSource.getMessage("column.firstName", null,
locale), null);
configureColumn(row.getColumn("lastName"),
messageSource.getMessage("column.lastName", null,
locale), null);
configureColumn(row.getColumn("format"),
messageSource.getMessage("column.format", null, locale),
new SpringMessageCellEditor(messageSource, "format",
locale), false, false);
configureColumn(row.getColumn("toString"),
messageSource.getMessage("column.toString", null, locale),
new ToStringCellEditor(), false, false);
}
@Override
protected void configureUnexportedTable(TableFacade tableFacade,
Locale locale)
{
HtmlTable table = (HtmlTable) tableFacade.getTable();
table.setCaption(messageSource.getMessage("table.caption", null,
locale));
configureEditAndDelete(table.getRow(),
tableFacade.getWebContext(),
tableFacade.getCoreContext(), locale);
}
@Override
protected ImageCellEditor getEditImageCellEditor(Locale locale)
{
return new HelloWorldImageCellEditor("edit.gif",
messageSource.getMessage("image.edit.alt", null, locale),
"edit.html?pk={0,number,integer}");
}
@Override
protected ImageCellEditor getDeleteImageCellEditor(Locale locale)
{
return new HelloWorldImageCellEditor("delete.gif",
messageSource.getMessage("image.delete.alt", null, locale),
"delete.html?pk={0,number,integer}");
}
}
Controller
We end as we began, with a Spring MVC Controller to launch all this infrastructure. Since the details of table creation are encapulated in a factory, this is uncluttered: the only decision to be made is whether or not the table is to be exported. If it is exported, the results will be written directly to the output stream of the response; if not, they'll be rendered as a string containing our HTML table:
public class CustomJMesaSearchController extends AbstractController
{
private HelloWorldService helloWorldService;
private HelloWorldTableFactory tableFactory;
public void setHelloWorldService(HelloWorldService helloWorldService)
{
this.helloWorldService = helloWorldService;
}
public void setTableFactory(HelloWorldTableFactory tableFactory)
{
this.tableFactory = tableFactory;
}
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest
request, HttpServletResponse response) throws Exception
{
Set<HelloWorld> results = helloWorldService.findAll();
TableFacade tableFacade =
tableFactory.createTable(request, response, results);
if (tableFacade.getLimit().isExported())
{
tableFacade.render();
return null;
}
return new ModelAndView("results", "results",
tableFacade.render());
}
}
We are actually reusing the same JSP page as in the basic JMesa setup: the only difference is in the Java code that generates the table. One more change in jmesa-servlet.xml to create everything and tie it all together:
<bean id="tableFactory"
class="com.javalobby.article.jmesa.util.HelloWorldTableFactory">
<property name="messageSource" ref="messageSource"/>
</bean>
<bean id="customSearchController"
class="com.javalobby.article.jmesa.web.CustomJMesaSearchController">
<property name="helloWorldService" ref="helloWorldService"/>
<property name="tableFactory" ref="tableFactory"/>
</bean>
...
<prop key="/search.html">customSearchController</prop>
And how different the display looks!:
[img_assist|nid=3682|title=Figure 5.|desc=A customized search result|link=none|align=left|width=757|height=465]
Ajax
Finally, the table looks like we want it to, but it's irritating having to resubmit the form each time we want to make a change. Isn't that the sort of thing Ajax is supposed to help us avoid?
The answer is, of course, yes! So how do we leverage Ajax to help us? Fortunately, the JMesa folks have already worked that out. There are two parts to the solution: changes to the controller and changes to the JSP page.
<form name="resultsForm" action="search.html"><b>
<div id="results-div"></b>
${results}<b>
</div></b>
</form>
<script type="text/javascript">
function onInvokeAction(id, action)
{
setExportToLimit(id, '');<b>
var parameterString = createParameterStringForLimit(id);
$.get('${pageContext.request.contextPath}/search.html?ajax=true&'
+ parameterString,
function(data)
{
$("#results-div").html(data);
}
);
}
function onInvokeExportAction(id, action)
{
location.href="${pageContext.request.contextPath}/search.html?" +
createParameterStringForLimit(id);
}
</script>
In our previous solution, the onInvokeAction Javascript method called createHiddenInputFieldsForLimitAndSubmit, which submitted the form. In the Ajax solution, it assembles parameters for the TableFacade class and sends a request for the HTML for table display, adding a parameter to indicate that it's an Ajax request. Then a callback Javascript function substitutes the returned HTML for the contents of the <div> that now holds the table.
The simplicity and unusual syntax of the latter code come courtesy of the jQuery Ajax library, which is thoughtfully used by JMesa:
<script type="text/javascript" src="js/jquery-1.2.3.min.js"></script>
The controller, of course, needs to interpret this new request correctly. This is just one more branch on the decision tree we saw in the previous controller:
public class AjaxJMesaSearchController extends AbstractController
{
@Override
protected ModelAndView handleRequestInternal(HttpServletRequest
request, HttpServletResponse response) throws Exception
{
Set<HelloWorld> results = helloWorldService.findAll();
TableFacade tableFacade = tableFactory.createTable(request,
response, results);
if (tableFacade.getLimit().isExported())
{
tableFacade.render();
return null;
}
else if ("true".equals(request.getParameter("ajax")))
{
String encoding = response.getCharacterEncoding();
byte[] contents = tableFacade.render()
.getBytes(encoding);
response.getOutputStream().write(contents);
return null;
}
return new ModelAndView("ajax-results", "results",
tableFacade.render());
}
}
Of course, we have to make Spring aware of the controller change in jmesa-servlet.xml:
<bean id="ajaxSearchController" class="com.javalobby.article.jmesa.web.AjaxJMesaSearchController">
<property name="helloWorldService" ref="helloWorldService"/>
<property name="tableFactory" ref="tableFactory"/>
</bean>
...
<prop key="/search.html">ajaxSearchController</prop>
That's all there is to it! The table looks and acts just as it did, except now it refreshes without resubmitting the form each time.
Conclusion
Now I don't have to like tables: I can program them in Java and not worry about them on a display JSP. This makes the page cleaner, gives me more functionality out-of-box, and enables me to nix at least some of the languages I'd otherwise have to fuss with. What's not to like?
I hope you'll take a good look at JMesa and see if it can make your life easier, and that this article helps you decide. Good luck!
Installation of the Eclipse Project
Installing the Eclipse project is not difficult; the included Ant build file and these instructions assume Tomcat as the deployment target (I'm using version 6.0.14 with JDK 6.0_03). If you want to use another servlet container, though, feel free to modify the instructions and the Ant file as needed:
- download the ZIP archive
- unzip the archive to any directory; it will create its own top-level subdirectory
- open the project as a Java project in Eclipse
- the project must use the Java 6 compiler (available from "http://java.sun.com/javase/6/")
- the Tomcat installation must be version 6 (available from "http://tomcat.apache.org/download-60.cgi")
- open the build file and modify the path to the Tomcat root
- add an external JAR file to the Eclipse project build path from the Tomcat installation: lib/servlet-api.jar
- run the Ant "deploy" target, which will build automatically
- open a browser and point it to "http://localhost:8080/running-jmesa-examples/" or to an equivalent URL for your setup
(N.B. Some code in the project has been refactored from the way it appears in the article.)
Opinions expressed by DZone contributors are their own.
Comments