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.

Monday, February 22, 2010

Running Tapestry 5.2 Snapshot on Google App Engine

Google App Engine represents a great hosting option these days; it is free up to a fairly generous amount of server resources, after which billing kicks in at reasonable rate up to a budget you specify. Between that and the power and scalability of Google's infrastructure, it is seems like a great option for new projects (or existing projects if you happen to be using the right mix of technologies).

The only catch is that you're limited to a pretty specific set of technologies, and this does NOT include a relational database, or Hibernate! Hence, great for new projects, not so great if you have a significant app that relies on Hibernate or even a relational database paradigm.

My web framework of choice, Tapestry, is sortof supported, but it takes a little work to get it running on the local development server provided by Google.

As of this post, you'll want to be running on the Tapestry 5.2 snapshot. Tapestry 5.1 uses a stax parser that is incompatible with GAE. I'm currently running this on GAE 1.3.1.

Dmitry Gusev has a great post on how to fix a strange Javassist-related error "java.lang.ClassFormatError: Invalid length ... in LocalVariableTable". Unfortunately for me, it took a little extra work to get that fix incorporated into the development server. This thread from the Tapestry user list got me the rest of the way. You'll want to check out this post for some additional gotchas.

These instructions essentially explain the Tapestry list thread in a little more detail since I struggled a little to get it working. I am assuming you already have a working app running on Tapestry 5.2 snapshot, that you have an output "war" folder in the proper format for GAE to run against, and that you have GAE installed, with the /bin folder in your system path.

I'm using IntelliJ CE on Windows, so I am forced to use the command line or an ant script to run the server. These instructions show how to setup both.

1. Create and compile a class com.google.appengine.tools.development.agent.AppEngineDevAgent with the following source:

package com.google.appengine.tools.development.agent;

import java.lang.instrument.Instrumentation;

public class AppEngineDevAgent
{
public static Object getAgent()
{
return null;
}

public static void premain(String foo, Instrumentation bar) {}
}

2. In <sdk root>/lib/agent, extract appengine-agent.jar

3. Go into the extracted files to com/google/appengine/tools/development/agent and copy in the .class file created in step 2, overwriting the existing file

4. Re-jar the extracted files and call it patched-appengine.jar, putting it in <sdk root>/lib/agent

5. Backup <sdk root>/bin/dev_appserver.cmd and replace the contents of the original with this (replacing <sdk root>):

@java -javaagent:<sdk root>/lib/agent/patched-appengine.jar -cp "%~dp0\..\lib\appengine-tools-api.jar" ^
com.google.appengine.tools.development.DevAppServerMain %*

Alternatively, if you want to run the ant script provided by GAE, backup <sdk root>/config/user/ant-macros.xml and find the dev_appserver macrodef. Replace the <sequential> section with this (replacing <sdk root>):

<sequential>
<java classname="com.google.appengine.tools.development.DevAppServerMain"
classpath="${appengine.tools.classpath}"
fork="true" failonerror="true">
<jvmarg value="-javaagent:<sdk root>/lib/agent/patchedappengine.jar" />
<arg value="@{war}"/>
</java>
</sequential>

The nice thing about this is that the production deployment process does not require any extra work (I tried it on a page that gave me the LocalVariableTable error, and it worked without issue).

Hopefully there won't be a need for workarounds with Tapestry soon, Howard Lewis Ship has been working on abstracting Javaassist out of Tapestry recently, and I've seen some GAE compatibility commits from him as well.

Next up, try to figure out how to get JDO working with Tapestry!