Jan 17 2010
Use Generators to create boilerplate code in GWT 2.0

Table of contents
Introduction
A very annoying point in GWT is to write the (same) code to create Widgets and set the Properties. Most lines are filled with these boilerplate code. GWT 2.0 has a solution for this, it is called Generators. Generators are classes that are invoked by the GWT compiler to generate a Java implementation of a class during compilation. Instead of writing:
TextBox myTextBox = new TextBox();
myTextBox.setText("Hello World TextBox");
myTextBox.setName("blablubb");
myTextBox.setStyleName("ihatethis");
...
You can write this:
@FormField(styleName = "GWTstyleTextBox", parentAccessor = "verticalPanel", defaultText= "Hello World TextBox") TextBox nameField;
What has happened? The boilerplate code was replaced by an Annotation. First the Generator translates the Annotation into a Java class and after that the compiler creates the corresponding JavaScript-Code (very crazy i know). You can use Annotations for nearly everything, but this blog shows you how to create TextBox and Label -fields.
A simple Example
First, take a look at MyPanel.java, where you can see the @FormField-Annotation in action, which generates code for a TextBox (nameField) and a Label (labelField). This is only working when the deferred binding mechanism GWT.create() is encountered while compiling.
MyPanel.java
package eu.jdevelop.gwt.anno.client;
import com.google.gwt.core.client.GWT;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import eu.jdevelop.gwt.anno.client.ui.forms.FormBinder;
import eu.jdevelop.gwt.anno.client.ui.forms.FormField;
/**
* Component which contains an annotated TextField and LabelField
*
* @author Siegfried Bolz
* @since 09.01.2010
*/
public class MyPanel {
// Create this in every annotated class to support Field-Annotations
interface InternalFormBinder extends FormBinder<MyPanel> {}
private static final InternalFormBinder formBinder = GWT.create(InternalFormBinder.class);
// Parent (Store) for the annotated Fields
final VerticalPanel verticalPanel = new VerticalPanel();
// Create a LabelField
@FormField(styleName = "GWTstyleLabel", parentAccessor = "verticalPanel", defaultText= "Hello World Label")
Label labelField;
// Create a TextField
@FormField(styleName = "GWTstyleTextBox", parentAccessor = "verticalPanel", defaultText= "Hello World TextBox")
TextBox nameField;
public MyPanel() {
// Bind this instance to the FormBinder
formBinder.bind(this);
}
public VerticalPanel getPanel() {
return verticalPanel;
}
}
The FormField-Annotation has only 3 parameters, which are set on every Field. If you want specific parameters you can create more Annotations or enhance this one with an intelligent system to activate them.
FormField.java
package eu.jdevelop.gwt.anno.client.ui.forms;
import java.lang.annotation.*;
/**
* Annotation for all Field-Types
*
* @author Siegfried Bolz
* @since 09.01.2010
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface FormField {
String styleName() default "";
String parentAccessor();
String defaultText() default "";
}
The FormBinder-Interface is used for the deferred binding operation, the parameter can be accessed in the FormBinderGenerator-class.
FormBinder.java
package eu.jdevelop.gwt.anno.client.ui.forms;
import eu.jdevelop.gwt.anno.client.MyPanel;
/**
* Simple Interface for binding operations
*
* @author Siegfried Bolz
* @since 09.01.2010
*/
public interface FormBinder<T> {
// Make "bind" generic to support more components
void bind(MyPanel component);
}
The Generators are creating the Java-files. I have splitted this operation in three main and two sub files. BaseGenerator is used to create an empty File with imports, FormBinderGenerator adds some default actions like Field instantiation, setStyleName.. and implementations of FieldGenerator adds Field specific operations.
BaseGenerator.java
package eu.jdevelop.gwt.anno.ui;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.uibinder.rebind.IndentedWriter;
import java.io.PrintWriter;
/**
* Create the Implementation-Class and add a basic header data for ALL
* generated Annotations.
*
* @author Siegfried Bolz
* @since 09.01.2010
*/
public abstract class BaseGenerator extends Generator {
protected static final String IMPORT = "import %1$s;";
protected static final String PACKAGE = "package %s;";
protected JClassType interfaceType(TypeOracle oracle, String s, TreeLogger treeLogger) throws UnableToCompleteException {
JClassType interfaceType;
try {
interfaceType = oracle.getType(s);
} catch (NotFoundException e) {
treeLogger.log(TreeLogger.ERROR, String.format("%s: Could not find the interface [%s]. %s", e.getClass().getName(), s, e.getMessage()));
throw new UnableToCompleteException();
}
return interfaceType;
}
@Override
public String generate(TreeLogger treeLogger, GeneratorContext generatorContext, String s) throws UnableToCompleteException {
JClassType interfaceType = interfaceType(generatorContext.getTypeOracle(), s, treeLogger);
String packageName = interfaceType.getPackage().getName();
PrintWriterManager writers = new PrintWriterManager(generatorContext, treeLogger, packageName);
String implName = interfaceType.getName().replace(".", "_") + "Impl";
PrintWriter printWriter = writers.tryToMakePrintWriterFor(implName);
if (printWriter != null) {
IndentedWriter writer = new IndentedWriter(printWriter);
writer.write(String.format(PACKAGE, packageName));
writer.newline();
doGenerate(interfaceType, implName, writer);
writers.commit();
}
return packageName + "." + implName;
}
protected abstract void doGenerate(JClassType interfaceType, String implName, IndentedWriter writer);
protected void writeClassIntro(JClassType interfaceType, String implName, IndentedWriter writer) {
writer.write("public class %1$s implements %2$s {", implName, interfaceType.getName());
writer.indent();
writer.newline();
}
protected JParameter[] extractInterfaceMethodParams(JClassType interfaceType) {
return interfaceType.getImplementedInterfaces()[0].getMethods()[0].getParameters();
}
protected void writeOutro(IndentedWriter writer) {
writer.outdent();
writer.write("}");
writer.outdent();
writer.write("}");
}
}
FormBinderGenerator.java
package eu.jdevelop.gwt.anno.ui.forms.generator;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.uibinder.rebind.IndentedWriter;
import eu.jdevelop.gwt.anno.client.ui.forms.FormField;
import eu.jdevelop.gwt.anno.ui.BaseGenerator;
/**
* Generate the Class-Framework for all Annotations
* which are implementing the FormBinder-interface.
*
* @author Siegfried Bolz
* @since 09.01.2010
*/
public class FormBinderGenerator extends BaseGenerator {
@Override
protected void doGenerate(JClassType interfaceType, String implName, IndentedWriter writer) {
JParameter[] methodParams = extractInterfaceMethodParams(interfaceType);
writeImports(writer, methodParams);
writeClassIntro(interfaceType, implName, writer);
writeFieldsIntro(writer);
writeMethodIntro(writer, methodParams);
writeFieldsBinding(interfaceType, writer);
writeOutro(writer);
}
/**
* Generate default code for all generated Components
*
* @param interfaceType
* @param writer
*/
private void writeFieldsBinding(JClassType interfaceType, IndentedWriter writer) {
for (JField jField : interfaceType.getEnclosingType().getFields()) {
FormField annotation = jField.getAnnotation(FormField.class);
if(annotation != null) {
writer.write("component.%1$s = new %2$s();", jField.getName(), jField.getType().getQualifiedSourceName());
writer.write("component.%1$s.setStyleName("%2$s");", jField.getName(), annotation.styleName());
writer.write("GWT.log("Adding Field: %1$s of Type: %2$s to: %3$s",null);", jField.getName(), jField.getType().getQualifiedSourceName(), annotation.parentAccessor());
writer.write("component.%1$s.add(component.%2$s);", annotation.parentAccessor(), jField.getName());
FieldGeneratorFactory.getInstance().createFor(jField).write(jField, annotation, writer);
}
}
}
private void writeMethodIntro(IndentedWriter writer, JParameter[] parameters) {
writer.write("public void bind(%1$s component) {", parameters[0].getType().getQualifiedSourceName());
writer.indent();
}
private void writeFieldsIntro(IndentedWriter writer) {
// nothing to do now
writer.newline();
}
private void writeImports(IndentedWriter writer, JParameter[] parameters) {
writer.write(IMPORT, GWT.class.getName());
//writer.write(IMPORT, parameters[1].getType().getQualifiedSourceName());
writer.newline();
}
}
FieldGenerator.java
package eu.jdevelop.gwt.anno.ui.forms.generator;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.uibinder.rebind.IndentedWriter;
import eu.jdevelop.gwt.anno.client.ui.forms.FormField;
/**
* Extend this abstract class to create a FieldGenerator.
*
* @author Siegfried Bolz
* @since 09.01.2010
*/
public abstract class FieldGenerator {
public abstract void write(JField jField, FormField annotation, IndentedWriter writer);
}
In this class i am adding a specific operation only for the TextBox.
TextFieldGenerator.java
package eu.jdevelop.gwt.anno.ui.forms.generator;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.uibinder.rebind.IndentedWriter;
import eu.jdevelop.gwt.anno.client.ui.forms.FormField;
/**
* Generates additional Code only for the TextBox
*
* @author Siegfried Bolz
* @since 09.01.2010
*/
public class TextFieldGenerator extends FieldGenerator {
@Override
public void write(JField jField, FormField annotation, IndentedWriter writer) {
writer.write("component.%1$s.setEnabled(true);", jField.getName());
writer.write("component.%1$s.setText("%2$s");", jField.getName(), annotation.defaultText());
writer.write("component.%1$s.setName("%1$s");", jField.getName());
}
private boolean isInstanceOf(JField jField, final Class<?> aClass) {
try {
return aClass.isAssignableFrom(Class.forName(jField.getType().getQualifiedSourceName(), false, this.getClass().getClassLoader()));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
In this class i am adding a specific operation only for the Label.
LabelFieldGenerator.java
package eu.jdevelop.gwt.anno.ui.forms.generator;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.uibinder.rebind.IndentedWriter;
import eu.jdevelop.gwt.anno.client.ui.forms.FormField;
/**
* Generates additional Code only for the Label
*
* @author Siegfried Bolz
* @since 09.01.2010
*/
public class LabelFieldGenerator extends FieldGenerator {
@Override
public void write(JField jField, FormField annotation, IndentedWriter writer) {
writer.write("component.%1$s.setText("%2$s");", jField.getName(), annotation.defaultText());
}
private boolean isInstanceOf(JField jField, final Class<?> aClass) {
try {
return aClass.isAssignableFrom(Class.forName(jField.getType()
.getQualifiedSourceName(), false, this.getClass().getClassLoader()));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
To create a relation between the Fields and the Generators, i am using the FieldGeneratorFactory.
FieldGeneratorFactory.java
package eu.jdevelop.gwt.anno.ui.forms.generator;
import com.google.gwt.core.ext.typeinfo.JField;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.TextBox;
import java.util.HashMap;
import java.util.Map;
/**
* Register all Generators.
*
* @author Siegfried Bolz
* @since 09.01.2010
*/
public class FieldGeneratorFactory {
private static final FieldGeneratorFactory INSTANCE = new FieldGeneratorFactory();
private static final Map<String, FieldGenerator> generators = new HashMap<String, FieldGenerator>();
static {
generators.put(TextBox.class.getName(), new TextFieldGenerator());
generators.put(Label.class.getName(), new LabelFieldGenerator());
}
private FieldGeneratorFactory() {
}
public static FieldGeneratorFactory getInstance() {
return INSTANCE;
}
public FieldGenerator createFor(JField jField) {
String className = jField.getType().getQualifiedSourceName();
FieldGenerator generator = generators.get(className);
if(generator == null) {
throw new IllegalArgumentException("Did not find any generator for the [" + className +"].");
}
return generator;
}
}
The last class is used to write the Java-Files.
PrintWriterManager.java
package eu.jdevelop.gwt.anno.ui;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import java.io.PrintWriter;
import java.util.HashSet;
import java.util.Set;
/**
* Write the generated Java files.
*
* @author Siegfried Bolz
* @since 09.01.2010
*/
public class PrintWriterManager {
private final GeneratorContext genCtx;
private final String packageName;
private final TreeLogger logger;
private final Set<PrintWriter> writers = new HashSet<PrintWriter>();
public PrintWriterManager(GeneratorContext genCtx, TreeLogger logger,
String packageName) {
this.genCtx = genCtx;
this.packageName = packageName;
this.logger = logger;
}
/**
* Commit all writers we have vended.
*/
public void commit() {
for (PrintWriter writer : writers) {
genCtx.commit(logger, writer);
}
}
/**
* @param name classname
* @return the printwriter
* @throws RuntimeException if this class has already been written
*/
public PrintWriter makePrintWriterFor(String name) {
PrintWriter writer = tryToMakePrintWriterFor(name);
if (writer == null) {
throw new RuntimeException(String.format("Tried to write %s.%s twice.", packageName, name));
}
return writer;
}
/**
* @param name classname
* @return the printwriter, or null if this class has already been written
*/
public PrintWriter tryToMakePrintWriterFor(String name) {
PrintWriter writer = genCtx.tryCreate(logger, packageName, name);
if (writer != null) {
writers.add(writer);
}
return writer;
}
}
The last thing to do, is to create the module-file FormBinder.gwt.xml and insert it into the project module file GwtAnnotations.gwt.xml
FormBinder.gwt.xml
<module>
<inherits name="com.google.gwt.resources.Resources" />
<generate-with class="eu.jdevelop.gwt.anno.ui.forms.generator.FormBinderGenerator">
<when-type-assignable class="eu.jdevelop.gwt.anno.client.ui.forms.FormBinder" />
</generate-with>
</module>
Here you can see the whole project opened in Eclipse.
Launch the Application
The compiled view is very simple, you can see only the generated Label and TextBox -Fields.
Inside the Hosted Mode Debug Window, you can see some info dropped from the FormBinderGenerator.
This is the generated Java-file with the boilerplate code. Every line has been moved from the Project into this file. Imagine how many lines this file contains if you create 10, 100 or more Fields?
Generated MyPanel file
package eu.jdevelop.gwt.anno.client;
import com.google.gwt.core.client.GWT;
public class MyPanel_InternalFormBinderImpl implements MyPanel.InternalFormBinder {
public void bind(eu.jdevelop.gwt.anno.client.MyPanel component) {
component.labelField = new com.google.gwt.user.client.ui.Label();
component.labelField.setStyleName("GWTstyleLabel");
GWT.log("Adding Field: labelField of Type: com.google.gwt.user.client.ui.Label to: verticalPanel",null);
component.verticalPanel.add(component.labelField);
component.labelField.setText("Hello World Label");
component.nameField = new com.google.gwt.user.client.ui.TextBox();
component.nameField.setStyleName("GWTstyleTextBox");
GWT.log("Adding Field: nameField of Type: com.google.gwt.user.client.ui.TextBox to: verticalPanel",null);
component.verticalPanel.add(component.nameField);
component.nameField.setEnabled(true);
component.nameField.setText("Hello World TextBox");
component.nameField.setName("nameField");
}
}
Conclusion
Using Generators can help you to clean up your code by getting rid of boilerplate code. Don’t waste time by writing the same stuff, just generate it. One positive aspect is, that you have one point to make changes and don’t have to refactor the whole project. Ok you could use a factory with static methods (like WidgetsCreatorHelper.createLabelField(String name, String styleName) ), but this is old school and not so beautiful like Annotations.







I read with interest your blog re GWT generators.
The Introduction provides a good contrast of the potential benefit and streamlining.
However, the included example is long and I got lost; I see much code, several classes; I look forward to future posts that will help me better understand GWT 2.0
Thanks for the blog entry on a subject I am trying to understand; thanks;
Ugly bugly!
You want to have either factories producing your customized components pre configured, or just extended classes where the configuration is done in the constructor. Specifying attributes in an annotation is just messy, it’s worse than setting them directly.
Any specific reason you don’t want to use UIBinder for the same thing ?
@Venkatesh Sellappa
I am using GXT which is currently not working with the UIBinder. To reduce the amount of code i am using this special Generators.
@David
I don’t think so, in my example i am using annotations to reduce the code. I have created other Annotations to simplify Field-Security and to add Handlers/Listeners on the same way. An other option is to use the Builder-Pattern if you have selfmade Widgets. Ok it looks like the old way to code Servlets, but it is working very fine.
Hello,
First at all, thank you very much for your example, It helped me a lot. There is something that I do not understand:
Why do you have the isInstanceOf function in each FieldGenerator?
What is it the functionality?
The Eclipse IDE marked as a warning like it is never used in the project, Is it?
Thank you
masch…
salu2…