Skip to main content

Exploring GWT 5: Generating the Code

Posted by mjeffw on November 7, 2008 at 4:30 AM PST

So at this point, we have an interface, PropertyAdapter, for which we want GWT to generate an implementation at compile time using this call:

PropertyAdapter<Person> adapter = (PropertyAdapter<Person>)GWT.create(Person.class);

GWT must be told how to generate this class, which really means that GWT needs to be told which subclass of com.google.gwt.core.ext.Generator to use. This is accomplished by editing your module's Foo.gwt.xml file and adding the following lines:

<generate-with class="com.mjeffw.properties.rebind.PropertyAdapterGenerator">
  <when-type-assignable class="com.mjeffw.properties.client.properties.HasProperty" />
</generate-with>

This tells GWT that whenever you pass a class to GWT.create() that is assignable to an instance of HasProperty, invoke the PropertyAdapterGenerator class to generate the source code for that call.

The astute reader will notice that my module configuration file specifies using a PropertyAdapterGenerator to generate a class when given a class literal that is assignable to HasProperty. This is a simple marker interface:

public interface HasProperties { }

So I have to pollute the purity of my Person data transfer object a little by adding this interface to it:

class Person implements HasProperties
{
   private String firstName;
   private String middleName;
   private String lastName;

   public String getFirstName() { return firstName; }
   public void setFirstName(String name) { firstName = name; }

   public String getMiddleName() { return middleName; }
   public void setMiddleName(String name) { middleName = name; }

   public String getLastName() { return lastName; }
   public void setLastName(String name) { lastName = name; }
}

Now, I believe there is a way around this limitation, but I'm going to save that for after we complete the PropertyAdapterGenerator.

What's left now is to write an implementation of PropertyAdapterGenerator. You'll notice in the XML above, assuming my project's module is "com.mjeffw.properties", the generator is in a subpackage named "rebind". This is the default location of code generators in a GWT module.

Being Test-Infected, I approached this with a series of unit tests to see what code I had to add to get each part of the code written correctly. Below you will see a unit test followed by the generator code.

public class Person_PropertyAdapterTest extends GWTTestCase
{
   public void testDeclaration() throws Exception
   {
      Object object = GWT.create(Person.class);

      assertEquals("com.mjeffw.properties.client.Person_PropertyAdapter",
         object.getClass().getName());
      assertTrue(object instanceof PropertyAdapter);
   }
}

This test just makes sure the Generator is being called and returning an object of the (new) class, Person_PropertyAdapter. Note that I couldn't do something like this:

   assertEquals(Person_PropertyAdapter.class, o.getClass());

...because there is no such class at this point. My generator will create the source code for that class and compile it during the running of the testcase.

Here's the minimal code needed to get this test to pass:

public class PropertyAdapterGenerator extends Generator
{
   @Override
   public String generate(TreeLogger logger, GeneratorContext context,
      String typeName) throws UnableToCompleteException
   {
      TypeOracle oracle = context.getTypeOracle();
      try
      {
         JClassType type = oracle.getType(typeName);

         String packageName = type.getPackage().getName();
         String simpleName = type.getSimpleSourceName() + "_PropertyAdapter";

         ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(
            packageName, simpleName);

         String intfName = "com.mjeffw.properties.client.PropertyAdapter<" +
            typeName + ">";
         composer.addImplementedInterface(intfName);

         PrintWriter printWriter = context.tryCreate(logger,
            composer.getCreatedPackage(), composer.getCreatedClassShortName());
         SourceWriter writer = composer.createSourceWriter(context,
            printWriter);

         writer.commit(logger);
         return composer.getCreatedClassName();
      }
      catch (Exception e)
      {
         logger.log(TreeLogger.ERROR, "unable to generate code for " + typeName, e);
         throw new UnableToCompleteException();
      }
   }

All code generators must extend com.google.gwt.core.ext.Generator, and supply an implementation of the generate(TreeLogger, GeneratorContext, String) method. That method must create the source code of the class to create and somehow hand it to the GWT compiler runtime. It returns the fully qualified name of the class it creates.

The first thing I do in this method is to get a TypeOracle. This class enables you to query the classes present at compile time in a way that is similar to the Java reflection API. With this TypeOracle in hand, I ask it for a JClassType for the typeName passed in. That type name is the Person class.

From that name ("com.mjeffw.properties.client.Person") I build the fully-qualified name of the new class I am going to return: "com.mjeffw.properties.client.Person_PropertyAdapter", and use that name to get a ClassSourceFileComposerFactory object. Once I have a factory in hand, I tell it I want my new class to implement the PropertyAdapter interface:

   String packageName = type.getPackage().getName();
   String simpleName = type.getSimpleSourceName() + "_PropertyAdapter";

   ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(
      packageName, simpleName);

   String intfName = "com.mjeffw.properties.client.PropertyAdapter<" +
      typeName + ">";
   composer.addImplementedInterface(intfName);

The next step is to get a SourceWriter object from the composer factory initialized with a PrintWriter gotten from the Generator context. At this point, we have the shell of a class that probably looks something like this:

package com.mjeffw.properties.client;

public class Person_PropertyAdapter implements com.mjeffw.properties.client.PropertyAdapter
{
}

What's left is to hand this source code back to the GWT compiler to translate to Java byte code (for hosted mode execution) or Javascript for web mode execution.
This is a poorly documented part of GWT, but I'm betting this call:

   writer.commit(logger);

...is how the generator hands the compiler the source code.

If you recall from my previous post, the PropertyAdapter interface implements a couple of methods:

public interface PropertyAdapter<T>
{
   void setPropertySource(T source);
   Property<?> get(String propertyName);
}

To get this test to pass, I commented out the method declarations in this interface.

The next test is to add in the setPropertySource(Person source) method. To do this, all I have to do is uncomment out that method in the interface. This is because the original call to GWT.create(Person.class) will fail if the class doesn't compile.

To provide a do-nothing implementation of this method is really dirt simple (new lines in bold):

   PrintWriter printWriter = context.tryCreate(logger, 
      composer.getCreatedPackage(), composer.getCreatedClassShortName());
   SourceWriter writer = composer.createSourceWriter(context, printWriter);
  
   writer.indent();
   writer.println("public void setPropertySource(" + typeName + " source){}");
   writer.outdent();


   writer.commit(logger);
   return composer.getCreatedClassName();

I'm literally adding the source code to implement that method to the SourceWriter before I commit it.

While this minimal code works to make the test case pass, it doesn't actually do anything. What we want to do when setPropertySource is set is to store a reference to Person as an instance field, and call a method that creates a Map of property names to Property objects, one for each field in the Person class. And seeing that this blog entry is getting pretty long, I'll save that for next time.

Related Topics >>

Comments

Are you are extending GWTTestCase? Is your test classpath set up to include your source directories?

This is the generate-with section... I have replaced brackets with #. Application runs fine, even if I pack it in jar and include in different project. It is just that JUnit reports the given types 'Unknown'. Perhaps I'm not doing something right with configuration of JUnit (I have some expirience with JUnit testing, but not in combination with GWT). I'm using maven 2 to build the app. #generate-with class="eu.execom.gwt.rebind.EventAwareGenerator"# #when-type-assignable class="eu.execom.gwt.client.eventaware.EventAware" /# #/generate-with# #generate-with class="eu.execom.gwt.rebind.EventHandlerGenerator"# #when-type-assignable class="eu.execom.gwt.client.event.EventHandler" /# #/generate-with# #generate-with class="eu.execom.gwt.rebind.EAFGenerator"# #when-type-assignable class="eu.execom.gwt.client.event.EventAwareFactory" /# #/generate-with#

Great article! Still I have a problem running the tests for deferred binding. Project runs well and generates the classes, but when I try to run JUnit tests I get the message: Checking rule [WARN] Unknown type 'eu.execom.gwt.client.event.EventAwareFactory' specified in deferred binding rule . Am I missing something?

Good stuff. Thanks!

It's hard to say from that brief comment. Could you post the 'generate-with' section of your *.gwt.xml module file? You'll have to replace the angle brackets with something as those are not allowed in posted text. What other topics do you want to see covered here?