Jan 17 2010

Use Generators to create boilerplate code in GWT 2.0

Published by at 7:01 PM under Google Web Toolkit

google web toolkit

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.

Eclipse Project

Open Eclipse Project



Launch the Application

The compiled view is very simple, you can see only the generated Label and TextBox -Fields.

Launched Application in Chrome

Launched Application in Chrome



Inside the Hosted Mode Debug Window, you can see some info dropped from the FormBinderGenerator.

Debug Window

Debug Window



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.

Technorati Tags: , , , , , , , , , , , ,

11 responses so far

11 Responses to “Use Generators to create boilerplate code in GWT 2.0”

  1. J.F. Zarama sagt:

    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;

  2. David sagt:

    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.

    • @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.

  3. Venkatesh Sellappa sagt:

    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.

  4. masch sagt:

    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…

  5. [...] Multiple Presenters series (part 1 and part 2). In this post I will demonstrate how to use the GWT generator to differentiate events that are to be filtered by an event group. This gives you more freedom when [...]

  6. maddy sagt:

    I feel sorry for people who have to deal with this stuff. You have to do all that to make a simple form with a textbox? Are you kidding me?? Microsoft Visual Studio and Microsoft Office VBA is sooooooo much easier. Holy cow!

    Dear Google,
    Most people in the world are not C++ programmers. As technology advances, you’re supposed to be making computers and programming them easier for people, not the other way around, trying to turn people into advanced software developers. What are you thinking?

  7. xjxy sagt:

    Overall this is an excellent tutorial on how to use annotation and GWT generator together. The code works almost out of box, except for east-to-fix compiler errors due to double quotes not escaped (I guess the escape characters are lost after being posted to the blog site).
    The potentials for the generator are huge. For example, this might open the door to do aspect oriented programming using GWT since it provides access to the compiler. Just like the AspectJ compiler does to our java code. Due to the limitation we need all the source code to output javascript, the generator probably will not give us as much as AspectJ, however, well designed generators can still greatly reduce the amount of cross-cutting, boiler plate code.

  8. Thanks for quick reply and the welcome. I’ve tried this type of setup as well, even without the table.column name, I even hardcoded in debud mode the exact field names so there’s no doubt about objects being available. The findcontrol method every single time returns null. I finally got some light at the end of the tunnel using the ASS backward method with the following syntax, but at least it seems I can get to the changed values in the TextBoxes in gridview on the postback: Request.Params[gvData.Rows[0].Cells[0].Parent.ClientID.Replace(“_”,”$”) “$RANK”] gvData is my gridview’s name, RANK is the name of one of the fields, I just wanted to get a value back for anything and this seemed to return it. Now I need to figure out how to identify which rows have changed, and this will be a bit more comlicated because I cannot override the InitializeRow method to add handles to each textbox control as my gridview is located within a User control, so codebehind does not inherit from GridView class, but rather from UserControl. I just want to get a list of rows where the values has changed and only do an update on those rows. I’m using a business object to push changes back, so I’m canceling the SQL default update in the OnRowUpdating method and implementing my own update method… Any ideas in this regard or how to still get the values some better way than what I came up with would be appreciated! Thanks!