Skip to main content

Exploring GWT 6: Detailed implementation

Posted by mjeffw on November 14, 2008 at 3:35 AM PST

In the last entry, I got to the point where I had a functional GWT generator that was creating a do-nothing shell of the Person_PropertyAdapter class. Here, I plan to complete that implementation. At this point, we are generating the following source code:

package com.mjeffw.properties.client;

public class Person_PropertyAdapter implements com.mjeffw.properties.client.PropertyAdapter
{
   public void setPropertySource(com.mjeffw.properties.client.Person source) {}
}

The next step to complete the setPropertySource method is to actually save a reference to the Person argument in an instance field. To make sure this happens in a test-driven way, I decided to add a new method to the interface, getPropertySource():

package com.mjeffw.properties.client;

public interface PropertyAdapter<T>
{
   void setPropertySource(T source);

   T getPropertySource();

   // commented out until later
   // Property<?> get(String propertyName);
}

I refactored my test case to make my call to GWT.create() inside the gwtSetUp() method, and added the test case in bold:

public class Person_PropertyAdapterTest extends GWTTestCase
{
   private Object object;

   @Override
   public String getModuleName()
   {
      return "com.mjeffw.properties.Properties";
   }

   @Override
   protected void gwtSetUp() throws Exception
   {
      super.gwtSetUp();

      object = GWT.create(Person.class);
   }

   public void testDeclaration() throws Exception
   {
      assertEquals("com.mjeffw.properties.client.Person_PropertyAdapter", object.getClass()
         .getName());
      assertTrue(object instanceof PropertyAdapter);
   }

   @SuppressWarnings("unchecked")
   public void testSetPropertySource() throws Exception
   {
      PropertyAdapter"<Person> adapter = (PropertyAdapter<Person>) object;

      Person person = new Person();
      adapter.setPropertySource(person);
      assertSame(person, adapter.getPropertySource());
   }

}

To my surprise, this test case didn't work. The error emitted by GWT was no help ("perhaps you forgot to import a required module?") and it was with some difficulty that I finally figured it out: GWT generators fail if they generate the same class more than once. This is an example of how the current lack of documentation makes it difficult to figure out how these things work.

Eventually, I discovered that if the class to be generated already exists, when you call context.tryCreate(TreeLogger logger, String packageName, String simpleName) it will return null. In that case, your generator code should just immediately return with the name of the generated class and GWT will find that class from the original generation and use it. So I had to refactor the existing generator code to do that, then add the code to make getPropertySource() and setPropertySource(Person source) methods work. The current state of the generator is below, with changes in bold:

public class PropertyAdapterGenerator extends Generator
{
   private ClassSourceFileComposerFactory composer;

   @Override
   public String generate(TreeLogger logger, GeneratorContext context, String typeName)
      throws UnableToCompleteException
   {
      TypeOracle oracle = context.getTypeOracle();
      try
      {
         JClassType type = oracle.getType(typeName);
         SourceWriter writer = getSourceWriter(type, context, logger);
         if (writer == null)
         {
            return type.getParameterizedQualifiedSourceName() + "_PropertyAdapter";
         }


         writer.indent();
         writer.println("private" + typeName + " source;");
         writer.println();
         writer.println("public void setPropertySource(" + typeName
            + " source) { this.source = source; }");
         writer.println("public " + typeName + " getPropertySource() { return source; }");

         writer.outdent();

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

   private SourceWriter getSourceWriter(JClassType type, GeneratorContext context, TreeLogger logger)
   {
      String packageName = type.getPackage().getName();
      String simpleName = type.getSimpleSourceName() + "_PropertyAdapter";

      composer = new ClassSourceFileComposerFactory(packageName,
         simpleName);

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

      PrintWriter printWriter = context.tryCreate(logger, packageName, simpleName);
      if (printWriter == null)
      {
         // means that the generated type already exists
         return null;
      }
      else
      {
         SourceWriter sw = composer.createSourceWriter(context, printWriter);
         return sw;
      }
   }

}

I made the ClassSourceFileComposerFactory an instance field, which is instantiated in the new method, getSourceWriter(). That method now contains all the logic to create a Composer factory, and use that factory to produce the shell of the class we are generating, and finally, to create and return a SourceWriter from that factory. If the class has already been created by a previous call to the generator, the following line from that method returns null:

   PrintWriter printWriter = context.tryCreate(logger, packageName, simpleName);

In that case, getSourceWriter() returns null as well, which is interpreted by the generator() method to mean that the class has already been generated, so it simply returns the name of the generated class.

The generate() method also contains the code to generate the instance field in the Person_PropertyAdapter class we're building to hold an instance of Person, and the getPropertySource() and setPropertySource() methods.

Implementing PropertyAdapter.get(String propertyName);

Here comes the tough part: implementing the PropertyAdapter.get(String) method. If you recall, way back in Part 4, I show what the Person_PropertyAdapter class should look like. Basically, when the Person_PropertyAdapter.setPropertySource(Person) method is called, I need to generate a Property object per property in the Person class and store it in the map, and also add a Property get(String) method to allow these properties to be fetched by name. Here I'm defining "property" in a similar way to a standard Java Bean property -- as defined by getter and setter methods. For our Person object, for instance, we have a pair of methods like "public void setFirstName(String)" and "public String getFirstName()" -- this defines a property of type String and with the name, "firstName".

The test case for this looks like this:

   @SuppressWarnings("unchecked")
   public void testGetProperty() throws Exception
   {
      PropertyAdapter<Person> adapter = (PropertyAdapter<Person>) object;

      Person person = new Person();
      adapter.setPropertySource(person);

      Property<String> firstName = (Property<String>) adapter.get("firstName");
      assertNotNull(firstName);
   }

There's a lot of changes I had to make to the Generator code to make this compile and run. First, let's look at the changes for the "generate" method.

   @Override
   public String generate(TreeLogger logger, GeneratorContext context, String typeName)
      throws UnableToCompleteException
   {
      TypeOracle oracle = context.getTypeOracle();
      try
      {
         JClassType type = oracle.getType(typeName);
         SourceWriter writer = getSourceWriter(type, context, logger);
         if (writer == null)
         {
            return type.getParameterizedQualifiedSourceName() + "_PropertyAdapter";
         }

         // define our instance variables
         writer.println("private " + typeName + " source;");
         writer.println("private Map<String, Property<?>> map = "
            + "new HashMap<String, Property<?>>();");

         writer.println();

         // define our methods
        
         // first, the setPropertySource method
         writer.println("public void setPropertySource(" + typeName + " source) {");
         writer.indentln("this.source = source;");
         writer.indentln("createProperties();");

         writer.println("}");

         // the getPropertySource method
         writer.println("public " + typeName + " getPropertySource() { return source; }");

         // the get method
         writer.println("public Property<?> get(String propertyName){ "
            + "return map.get(propertyName); }");

         // the (private) createProperties method
         writer.println("private void createProperties(){");
         writer.indent();
         writer.println("map.clear();");
         List<PropertyInfo> propertyInfos = findPropertyInfo(type.getMethods());
         for (PropertyInfo info : propertyInfos)
         {
            writer.println("map.put("" + info.name + "", new Property<" + info.type + ">(""
               + info.name + "", ");
            writer.indent();
            writer.println("new Accessor<" + info.type + ">() {");
            writer.indent();
            writer.println("public " + info.type + " get() { return source." + info.getter
               + "(); }");
            writer.println("public void set(" + info.type + " newValue) {  source." + info.setter
               + "(newValue); }");
            writer.outdent();
            writer.outdent();
            writer.println("}));");
         }

         writer.outdent();
         writer.println("}");

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

As you can see there is a lot of new code here, but its not that hard to follow.

  • I've added a Map to allow our get method to lookup each property by its name.
  • I've added a call to "createProperties" inside the setPropertySource method. That's where most of the work is done by the PropertyAdapter class to examine the source class and create Property objects for each of that class's properties.
  • I've implemented the Property get(String propertyName) method to return the Property stored in the Map by the propertyName key.
  • I've added the createProperties() method. This method clears the Map of all Property objects, then calls a generator method called findProperty() which returns a List of PropertyInfo objects, one per property found in the source object. It then iterates over the list and for each PropertyInfo object, it writes the source code to create a new Property object and store it in the Map.

The PropertyInfo class is a simple data object I used to store the information I needed to generate the Property objects:

package com.mjeffw.properties.rebind;

class PropertyInfo
{
   public String name;
   public String type;
   public String getter;
   public String setter;
}

The new generator method, findPropertyInfo(), uses more of GWT's typeinfo package to do Reflection-like introspection of the source class to discover its properties. It is invoked by passing in an array of JMethod objects, which I get from the JClassType I got at the top of the method.

   JClassType type = oracle.getType(typeName);
   ...
   List<PropertyInfo> propertyInfos = findPropertyInfo(type.getMethods());

JMethod is a class provided by GWT to represent a method of a class being introspected. It is analogous to java.lang.reflect.Method. Here is the implementation of the findPropertyInfo method:

   private List<PropertyInfo> findPropertyInfo(JMethod[] methods)
   {
      ArrayList<PropertyInfo> results = new ArrayList<PropertyInfo>();
      for (JMethod method : methods)
      {
         if (method.getName().startsWith("get")
            && Character.isUpperCase(method.getName().charAt(3)))
         {
            String name = method.getName().substring(3);
            JType returnType = method.getReturnType();
            JMethod setter = findSetter(methods, name, returnType);
            if (setter != null)
            {
               PropertyInfo info = new PropertyInfo();
               info.name = "" + Character.toLowerCase(name.charAt(0)) + name.substring(1);
               info.getter = method.getName();
               info.setter = setter.getName();
               info.type = returnType.getParameterizedQualifiedSourceName();
               results.add(info);
            }
         }
      }
      return results;
   }

It iterates over the array of JMethod objects looking for methods that are likely to be getters -- they start with the word "get" and the next character is uppercased. If found, it makes a call to another method, findSetter(), which returns a JMethod reference to the setter method that matches the getter, or null. If a setter method was found, it creates and populates one of my PropertyInfo objects and stores it in the results. This method illustrates some of the usage of the JMethod class API, as well as the JType class. (JType represents either primitive or class type; JClassType, which you can see used at the top of the generate() method, represents class types only.) The findSetter() method appears below:

   private JMethod findSetter(JMethod[] methods, String name, JType returnType)
   {
      String setterName = "set" + name;
      for (JMethod method : methods)
      {
         if (method.getName().equals(setterName) && method.getParameters().length == 1)
         {
            if (method.getParameters()[0].getType().equals(returnType))
            {
               return method;
            }
         }
      }
      return null;
   }

Its pretty simple -- it just looks for another method in the JMethod array that has a name that is equal to "set" + the name of the original getter method, minus the initial "get". The match is confirmed if the setter method also has a single parameter that is of the same JType as the return type of the getter method.

At this point, my tests were all passing, and all I needed to do was to prove to myself that the Properties being returned by the PropertyAdapter.get(name) method still worked the way I expected them to. (See Part 3 for properties and binding.) Here is that final test case:

   @SuppressWarnings("unchecked")
   public void testPropertyBinding() throws Exception
   {
      name = "Bob";
      Property<String> middleName = new Property<String>("name", new Accessor<String>() {
         public String get()
         {
            return Person_PropertyAdapterTest.this.name;
         }

         public void set(String newValue)
         {
            Person_PropertyAdapterTest.this.name = newValue;
         }
      });

      PropertyAdapter<Person> adapter = (PropertyAdapter<Person>) object;

      Person person = new Person();
      adapter.setPropertySource(person);

      assertEquals("Bob", name);
      Property<String> source = (Property<String>) adapter.get("middleName");
      middleName.bind(source);
      source.set("Robbie");
      assertEquals("Robbie", name);
   }

This test handily passed with no further coding! As you can see, this test creates a local Property named middleName and binds it to the middleName property returned from our PropertyAdapter. I then test it by changing the PropertyAdapter property's value and assert that the local property's value also changed.

Well, this is a really long post. I'm going to save my comments about GWT's approach to code generation, and the APIs, for next time.

Sources

I don't want to forget my primary sources for information on GWT's deferred binding mechanism and generators:

Related Topics >>

Comments

Exploring GWT 6: Detailed

Rather than using reflection directly, it would be a better idea to use java.beans.Introspector to determine the properties and method names. Sometimes bean authors might hide a property (if you're not supposed to call it, but it had to be public for implementation reasons) or call its methods different names (e.g. if they prefer the naming to be property()/setProperty() instead of getProperty()/setProperty()) so this is really the only way to get precisely the properties the bean has declared.