Tuesday, February 23, 2010

Pretty Printing JSON in Tapestry 5

One great feature of Tapestry 5 is the ability to construct properly-formed JSON to send to the client side for initializing javascript code. The only problem I've had with this is how the JSON is formatted at the bottom of the generated HTML page. For complex javascript components, it becomes a bit of a jumble of characters that are tricky to decipher at-a-glance during development. Sure, I could probably find a JSON formatter to paste it into, but that adds another step to the process.

As an example, this is the generated JSON for one of our components:


WidenUI.createGridView({"userAjaxError":"There was an error performing this action. Please contact an administrator for assistance.","successTitle":"Success","store":{"mappings":["id","class","objects-in-index","indexable-objects-in-database","difference","actions"],"context":[],"url":"/t5/widendev/searchadmin:indexstatusjson"},"resultsPerPage":25,"cellClickListeners":[],"errorTitle":"Error","isDev":true,"gridPanel":{"id":"grid","title":"Index Status","renderTo":"generated-gridview-grid","columns":[{"sortable":false,"width":1,"dataIndex":"class","hasAction":false,"header":"Class"},{"sortable":false,"width":1,"dataIndex":"objects-in-index","hasAction":false,"header":"Objects in Index"},{"sortable":false,"width":1,"dataIndex":"indexable-objects-in-database","hasAction":false,"header":"Indexable Objects in Database"},{"sortable":false,"width":1,"dataIndex":"difference","hasAction":false,"header":"Difference"}]},"options":[]});


There's a source URL buried in there somewhere, as an example of something I'd like to be able to pick out easily when issues arise.

To get this formatted a little nicer, I have pulled Tapestry's JSONObject class onto my classpath and changed toString() to this:


private static ThreadLocal<Integer> toStringLevel = new ThreadLocal<Integer>();

private int incrementToStringLevel()
{
   if (toStringLevel.get() == null)
   {
      toStringLevel.set(0);
   }

   toStringLevel.set(toStringLevel.get() + 1);

   return toStringLevel.get();
}

private void decrementStringLevel()
{
   toStringLevel.set(toStringLevel.get() - 1);
}

@Override
public String toString()
{
   int level = incrementToStringLevel();

   boolean comma = false;

   StringBuilder buffer = new StringBuilder("{\n");

   for (String key : keys())
   {
      if (comma)
      {
         buffer.append("¥");
      }

      for (int i = 0; i < level; i++)
      {
         buffer.append(" ");
      }

      buffer.append(quote(key));
      buffer.append(':');
      buffer.append(valueToString(properties.get(key)));

      comma = true;
   }

   buffer.append('}');

   decrementStringLevel();

   String result = buffer.toString();

   if (result.length() > 110)
   {
      result = result.replace("¥", ",\n");
   }
   else
   {
      result = result.replace("¥", ",");
   }

   return result;
}


Admittedly, that's a pretty ugly chunk of code, but it gets the job done:


WidenUI.createGridView({
 "userAjaxError":"There was an error performing this action. Please contact an administrator for assistance.",
 "successTitle":"Success",
 "store":{
  "mappings":["id","class","objects-in-index","indexable-objects-in-database","difference","actions"],
  "context":[],
  "url":"/t5/widendev/searchadmin:indexstatusjson"},
 "resultsPerPage":25,
 "cellClickListeners":[],
 "errorTitle":"Error",
 "isDev":true,
 "gridPanel":{
  "id":"grid",
  "title":"Index Status",
  "renderTo":"generated-gridview-grid",
  "columns":[{
   "sortable":false, "width":1, "dataIndex":"class", "hasAction":false, "header":"Class"},{
   "sortable":false,
   "width":1,
   "dataIndex":"objects-in-index",
   "hasAction":false,
   "header":"Objects in Index"},{
   "sortable":false,
   "width":1,
   "dataIndex":"indexable-objects-in-database",
   "hasAction":false,
   "header":"Indexable Objects in Database"},{
   "sortable":false, "width":1, "dataIndex":"difference", "hasAction":false, "header":"Difference"}]},
 "options":[]});


I've debated whether this should go into the actual Tapestry codebase (to be switched on when production mode is off at least), but it is pretty ugly, I'm not sure of a proper way to check the PRODUCTION_MODE flag inside JSONObject, and I'm not sure how many people want it. For now, if you want this, you can pull in the class and modify it like I have.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.