miércoles 21 de noviembre de 2007

Adding pagination to the dojo (Virtual)Grid

dojo 1.0 was finally released a couple of weeks ago and it packs once again (it was missing in v0.9) one of the most useful widgets, the Grid (formerly FilteringTable). This new widget (actually it's a revised version of TurboGrid) features a nice set of options (in a brief summary):
  • Performant and robust
  • Backed by a model (data store)
  • Configurable (even programmatically)
  • Inline edition
Probably the synchronization between a data and view is one of the most appreciated features. The raw performance would be other (it can load thousands of rows). A virtual scrolling mechanism had to be devised to allow them. Unfortunately this solution has some cons as well
  • Not always a huge number of records is available but just little chunks
  • Pagination is required or preferred to scrolling
  • The current data stores are not meant to interact with a server
For a developer used to work with DWR it's really too much. Fortunately, dojo offers a solution in VirtualGrid. The VirtualGrid widget is a Grid that is not backed by a model. This means that it supports everything that a Grid supports but the developer will need to provide the functions that the data store would provide otherwise (fetching, loading, sorting, filtering). It seems complicated but it is not! I will start showing how the final result should look like:


So now, let's review step by step how to do it. The first thing to do is declare the widget:

<script type="text/javascript"
   src='/dojo/dojo.js'
   djConfig="isDebug:false, parseOnLoad: true">
</script>
<script type='text/javascript'>
   dojo.require("dojox.grid.VirtualGrid");
   dojo.require("dojo.parser");
</script>
<div
   id="grid"
   dojoType="dojox.VirtualGrid"
   autoHeight="true"
   autoWidth="false"
   rowCount="10"
   get="getRow">
</div>

Nothing fancy there. Just the usual declaration of a dojo widget (dojox in this case). The HTML div includes just one weird attribute named get. It refers to the javascript function that will be called to populate each cell. The table as is cannot be painted. The information about the layout is missing:

dojo.addOnLoad(init);

function init() {
   var view = {noscroll: true, cells:[]};
   var header = [];
   for (var column in headers) {
      aColumn = new Object();
      aColumn.name = headers[column];
      aColumn.formatter = formatCell;
      aColumn.width = widths[column] + "px";
      header.push(aColumn);
   }
   view.cells.push(header);
   layout.push(view);
}

The code above uses a couple global variables (headers, widths) to create a layout. This layout is used along with the data to create the widget itself. The data will be obtained using the previously declared get function.

function getRow(inRowIndex) {
   var obj = dataArray[inRowIndex];
   var field = fields[this.index];
   return eval("obj." + field)
}

function formatCell(cellData) {
   return cellData;
}

That is enough. Supposing the data is stored as an object array (typical DWR response for example) named dataArray that is globally accesible, the code above retrieves the value of specific field of the object (the field names have to be stored in another array). this.index refers to the current cell, inRowIndex refers to the current row number. The formatCell function is called to format the output, for example to parse date objects. In our case it will rely on the default javascript toString representation. To load and populate the grid call the following function once there's data in the array:

function loadTable() {
   dijit.byId("grid").setStructure(layout);
}

By now, a ten row table should be visible on the screen (quite ugly if you haven't imported the stylesheets by the way). This is the base table to work with. It's very basic and lacks all the features. Let's start with sorting. Add the following lines to the init function

var grid = dijit.byId("grid");
grid.canSort = function(column) { return true; }
grid.sort = function() {...}
grid.setSortIndex(0, false);

I cannot provide the sort function code here as is quite extensive but you can find it in the example WAR below. With sorting working it's time to add filtering. The only real interest is obtaining the cell value. I'll use a context menu to do it:

<div
   dojoType="dijit.Menu"
   id="gridMenu"
   style="display: none;">
   <div
      divdojoType="dijit.MenuItem"
      onClick="applyCellFilter(cellNode)">
         Filter by [<span id="cellFilter"></span>]
   </div>
</div>

<script>
   var selectedCellNode;

   dojo.addOnLoad(function() {
      grid = dijit.byId("grid");
      gridMenu = dijit.byId("gridMenu");
      gridMenu.bindDomNode(grid.domNode);
      grid.onCellContextMenu = function(e) {
         byId('cellFilter').innerHTML = e.cellNode.innerHTML;
         selectedCellNode = e.cellNode;
      };
   });

   function applyCellFilter(id, cell) {...}
</script>


As before, the code to filter the object array is available in the example WAR. The interesting part in the code above is the onCellContextMenu. It iwill save the cell clicked in a javascript variable for later use.

Finally, the widget is in a position to add pagination. This is done externally using a list of page numbers an a couple of buttons to enable navigation. All the work is done against the data array. Basically, you only need to take care of the current offset. There's a little trick, always ask for one record more than the showed ones so you can easily test if there are more records available. The code can get really complex (a lot of functionality is working together by now) so you're better off reading it completely.

And now to the example. Some things to take into account just before:
  • All the styles and stuff that makes everything look pretty was done by Ignacio Gros, a fellow team member. Check his site if interested.
  • The example is provided as a proof of concept, with no warranty of any kind. Don't use it in production!
  • Look here for ideas with pagination
  • The server side code is mocked by a javascript function. It should be pretty straightforward to plug in something like DWR.
  • The widget is wrapped inside a tag file to be easier to use and deploy in a page
  • It downloads dojo.js from AOL CDN, change if needed
  • Look (use Firebug) to the number of requests when trying to improve performance
So, finally, you can download the WAR from Internna's repository

miércoles 7 de noviembre de 2007

Java 5: Retrieving all classes from a package

If you take a peek at the Package class you can easily detect that no getClasses() (or getAllClasses() or whatever) method is available. Quite a shock indeed! Probably if you're dealing with packages in some situation or other you'll need to list the classes inside. Unfortunately the problem is even bigger as that's not possible at all.

The reality is..that information is not available. This is due to the fact that not all classes have to reside in the file system as they can be loaded from a socket or a byte[], for example. That means the number of classes is just indeterminate.

On the other hand, it seems plausible that if you want to get all classes from a package, you are interested in the ones physically available, that is, in your hard disk. Fortunately, this is a problem with solution! Basically it consists in scanning a directory structure. The subtlety resides in the location of the directory itself as in many cases it will be a JAR file packaged inside some other archive (WAR, EAR..) and deployed in an application server.

Let's review step by step how to do it.

Our initial task is to obtain the package location. This is done querying the classloader. I will show one way to obtain the classloader here as well(there are more though). The class loader will return an enumeration of paths (as URLs) that can later be scanned

static Set<Class<?>> getClasses(String packageName) throws Exception {
   ClassLoader loader = Thread.currentThread().getContextClassLoader();
   return getClasses(loader, packageName);
}

static Set<Class<?>> getClasses(ClassLoader loader, String packageName) ...{
   Set<Class<?>> classes = new HashSet<Class<?>>();
   String path = packageName.replace('.', '/');
   Enumeration<URL> resources = loader.getResources(path);
   if (resources != null) {
      while (resources.hasMoreElements()) {
         String filePath = resources.nextElement().getFile();
         // WINDOWS HACK
         if(filePath.indexOf("%20") > 0)
            filePath = filePath.replaceAll("%20", " ");
         if (filePath != null) {
            if ((filePath.indexOf("!") > 0) & (filePath.indexOf(".jar") > 0)) {
               String jarPath = filePath.substring(0, filePath.indexOf("!"))
                  .substring(filePath.indexOf(":") + 1);
               // WINDOWS HACK
               if (jarPath.indexOf(":") >= 0) jarPath = jarPath.substring(1);
               classes.addAll(getFromJARFile(jarPath, path));
            } else {
               classes.addAll(
                  getFromDirectory(new File(filePath), packageName));
            }
         }
      }
   }
   return classes;
}

Reading from a plain directory is quite easy now:

static Set<Class<?>> getFromDirectory(File directory, String packageName) .. {
   Set<Class<?>> classes = new HashSet<Class<?>>();
   if (directory.exists()) {
      for (String file : directory.list()) {
         if (file.endsWith(".class")) {
            String name = packageName + '.' + stripFilenameExtension(file);
            Class<?> clazz = Class.forName(name);
            classes.add(clazz);
         }
      }
   }
   return classes;
}

And finally we have to consider compressed files. We can read them using the java.util.jar utilities

static Set<Class<?>> getFromJARFile(String jar, String packageName) ..{
   Set<Class<?>> classes = new HashSet<Class<?>>();
   JarInputStream jarFile = new JarInputStream(new FileInputStream(jar));
   JarEntry jarEntry;
   do {
      jarEntry = jarFile.getNextJarEntry();
      if (jarEntry != null) {
         String className = jarEntry.getName();
         if (className.endsWith(".class")) {
            className = stripFilenameExtension(className);
            if (className.startsWith(packageName))             classes.add(Class.forName(className.replace('/', '.')));
         }
      }
   } while (jarEntry != null);
   return classes;
}

This is really all but if you're interested here's a very simple unit test

public void testGetClasses() throws Exception {
   Set<Class<?>> list = ClassUtils.getClasses("mock.validation");
   assertTrue("Two classes found", list.size() == 2);
   assertTrue("Contains Mock class", list.contains(Mock.class));
   list.clear();
   ClassLoader loader = getClass().getClassLoader();
   list = ClassUtils.getClasses(loader, "org.hibernate.annotations");
   assertTrue("N classes found", list.size() > 10);
   assertTrue("Contains Cache class", list.contains(Cache.class));
}

By the way...this does not work

list = ClassUtils.getClasses(loader, "java.lang");
assertTrue("Contains String class", list.contains(String.class));