Search |
|||
M. Jeff Wilson's blogExploring GWT 6: Detailed implementationPosted 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.
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 >>
Web Applications Comments
Comments are listed in date ascending order (oldest first)
|
CategoriesArchivesRecent Entries |
||
|
|