David Sills's muse is fueled by Bach, Beethoven, and Brahms (and Berlioz and Boulez). He has no life, which comports well with the über-geekitude to which he aspires. He reads and writes fitfully in English, Spanish, French, German, Italian (mostly medieval), C, VB, Perl.... Oh, and Java and XSLT, lots and lots of Java and XSLT. Trained somewhere in darkest Central America, he works today at DataSource, Inc., on the product team for the CASE tool Abri. David has posted 9 posts at DZone. View Full User Profile

Running the Table With JMesa

06.18.2008
| 44641 views |
  • submit to reddit

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!:

Figure 5.: A customized search resultFigure 5.: A customized search result

 


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.)

 

Legacy
Article Resources: 
Published at DZone with permission of its author, David Sills.

(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)

Comments

Nicolas Frankel replied on Wed, 2008/06/18 - 10:19am

Hello,

Don't get me wrong, but displaying tabular data in HTML is done since a loooooong time with the DisplayTaglib. There's a short article on my blog if you're interested. They use taglibs but I see it as an advantage.

For more informations, see their site or their live demos which are really bluffing when you see the tiny amount of config needed on the JSP.

Cheers.

Nicolas

 

 

Benoit Guerout replied on Wed, 2008/06/18 - 10:53am

I've a very bad recollection of the last time I've tried to create a table JMesa (old but wonderfull displaytag solved easly the problem).

But, may be, this tutorial will help me to succeed the next time i'll try JMesa.

Jeff Johnston replied on Wed, 2008/06/18 - 7:19pm

What a great article...I was very excited to read this today :)! It was very well thought out and complete. You really tapped into just how customizable JMesa is. By design, just about every aspect of JMesa can be tweaked and modified with very little code. I will for sure link to this article from the home page of JMesa and reference it in the tutorials!

I am going to see about implementing your extended attributes notion. I think that is a nice improvement! I also hope to make some adapters for Spring so that those that choose to use the Spring framework can tie into Spring's notion of messages and preferences. It wouldn't be hard to do and would be a completely optional runtime dependency so non-Spring developers would not be effected.

Nicolas, there is a complete tag library with JMesa. The tag library is just a thin wrapper around the API. In addition JMesa has first class Groovy support and, in the next release, will even have Grails support with GSP Tags. The latter is currently being built by a member of the JMesa community and I am very excited to see that work as well. I have seen other work by this developer and it is always top notch. Also, as the article mentions, JMesa can also be put in an editable mode and will track your changes automatically.

bguerout, sorry to hear your first go around wasn't so great. How long ago was that? Based on how few questions I get nowadays I can say with confidence that your next experience should be really good. It is a very solid API! If you have any problems either post on the groups or email me directly. I try to get back ASAP as I know how frustrating it is to wait for replies when there are questions.

I wanted to mention that JMesa was designed to be an API first and foremost. It also embraces the Model 2 environment. Both of these give it the advantage of being able to use JMesa with just about any framework and regardless of the view technology being used.

David, if you don't mind, I also wanted to mention a few subtle improvements to your example that may make things easier:

  • You can actually turn off filtering at the row level by setting filterable to false. This will cause the filter row to not be rendered and the filter toolbar items to not display.
  • You might want to consider extending AbstractCellEditor as that will give you access to the Column, as well as the CoreContext and WebContext.
  • You do not need to inject your CellEditor with the CoreContext or WebContext. The support interfaces are there so the API can do that automatically for you when you set the editor on the renderer.
  • You can also define bogus column names instead of null for the column properties that do not map to a bean attribute. That enables you to reference the columns by name instead of position. It seems I usually have columns like "chkbox" myself that do not actually map to bean properties.

Once again, congratulations on a great article!

-Jeff Johnston

 

David Sills replied on Sun, 2008/06/22 - 1:59am in response to: Nicolas Frankel

Nicolas:

Thanks for your comment and thanks for reading.

It was, in fact, using Display Taglib for a while that got me to looking around for something easier to customize and with a few more features. I agree completely that if it meets your needs, it's a terrific library. It simply didn't meet mine.

That said, chacun à son goût, as the French say. The beauty of the Java world today is that we have enough open-source code around that everyone can use whatever they feel comfortable with.

Cheers,

David Sills

David Sills replied on Sun, 2008/06/22 - 2:01am in response to: Jeff Johnston

Jeff:

Thanks to you for authoring JMesa! Without it, of course, the article would have been, shall we say, less interesting?

I have incorporated some of your comments into the code and resources. I would be pleased if you wanted to link to the article.

All the best! 

David Sills

Neil Juan replied on Fri, 2008/08/22 - 9:07pm

I guess were on same track, I'm using JMesa for quite a while, in my swf application. If I do have problems, could I ask for help?

David Sills replied on Sat, 2008/08/23 - 1:23pm in response to: Neil Juan

Sure, but your best bet is the Google JMesa group: http://groups.google.com/group/jmesa/topics. Jeff Johnston (the creator of JMesa) moderates, and is extremely friendly and helpful.

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.