Where’s my Java reflection?
Some people like it, others don’t. I like it, it’s so cool to make generic code, but as I ‘ve been experimenting with GTW to know it’s capabilities I found that GWT does not support Reflection. It uses instead a mechanism called Deffered Binding to overcome the lack of reflection.
When doing some kind of framework code with GWT, soon you’ll realize that you need reflection, as I did. Bad luck, you’ll have to dig into the underground features of GWT, namely deferred bindings and generators. You can learn the theoretical traits of Deferred Bindings in the documentation: http://code.google.com/webtoolkit/doc/latest/DevGuideCodingBasicsDeferred.html
Let me explain my scenario.
I do all client/server communication using JSON, and the form field errors are returned from the server in JSON like this:
{"errors": {"name": ["Error 1 on field name", "Error 1 on field name"], "email: ["Error 1 on field email", "Error 1 on field email"]}, "_errorMessage": "Could not save the record due validation errors"}
My form editor at the GWT side, has some kind of Editors for each field of the form that implements an interface like this:
public interface Errorable {
public void setError();
public void setError(String message);
public void setErrors(List<String> messages);
public void clearErrors();
public boolean hasErrors();
}
My form component has something like this:
public class UserEditor extends Composite implements CompositeEditor<User> {
@UiField
TextBoxEditor name;
@UiField
TextBoxEditor email;
//Rest of the code
public void setFieldErrors(Map<String, List<String>> fieldErrors) {
//Bind error fields
}
}
TextEditor class implements the interface Errorable and the implementation of setErrors(List messages) simply shows the validation errors underneath the text box, the usual stuff…
The UserEditor::setFieldErrors(Map> fieldErrors) get a Map produced from the JSON response, with the name of the field as key and a list of Strings representing each error in the field, and show the errors for the corresponding field in the UserEditor
My problem is with the implementation of UserEditor::setFieldErrors(Map> fieldErrors) , I did’n want to do something like this:
public void setFieldErrors(Map<String, List<String>> fieldErrors) {
//Bind error fields
if ( fieldErrors.containsKey("name")) {
name.setErrors(fieldErrors.get("name"));
}
if ( fieldErrors.containsKey("email")) {
name.setErrors(fieldErrors.get("email"));
}
}
This could be a small amount of code now, but for a bigger application this is too much code I have to write by hand. So, here was where I needed the Java reflection. I needed to go through each field of FormEditor and find the members that implements the Erroable interface and check if the Map has a key with the same name and the call the setErros(List) on that members… you get it, right?
This could be done in runtime if I have access to Java reflection features in GWT, but I don’t and what I can have in GWT is code generation at compile time, so the solution to the problem can be slightly different than doing it with Java Reflection, but If I can generate that boilerplate code it would be great.
From here I digged into GWT code generation, and borrow some ideas from the UIBinder.
My UserEditor.java has now something like this:
public class UserEditor extends Composite implements CompositeEditor<User> {
private static UserEditorErrorBinder errorBinder = GWT
.create(UserEditorErrorBinder.class);
interface UserEditorErrorBinder extends ErrorBinder<UserEditor> {
}
}
The ErrorBinder interface is defined as:
public interface ErrorBinder<T> {
public void bindErrors(T editor, Map<String, List<String>> errors);
}
Now I have to implement one generator that generate the code that implements the method bindErrors with my desired logic.
After solving many puzzles, I came out with the this generator code that you can borrow to do your own generators in GWT:
public class ErrorBinderGenerator extends Generator {
private String implPackageName;
private String implTypeName;
private JClassType parameterizedType;
public String generate(TreeLogger logger, GeneratorContext context,
String requestedClass) throws UnableToCompleteException {
TypeOracle typeOracle = context.getTypeOracle();
JClassType objectType = typeOracle.findType(requestedClass);
if (objectType == null) {
logger.log(TreeLogger.ERROR, "Could not find type: "
+ requestedClass);
throw new UnableToCompleteException();
}
implTypeName = objectType.getSimpleSourceName() + "Impl";
implPackageName = objectType.getPackage().getName();
JClassType[] implementedTypes = objectType.getImplementedInterfaces();
// Can only implement one interface
if (implementedTypes == null
|| implementedTypes.length != 1
|| !implementedTypes[0].getQualifiedSourceName().equals(
ErrorBinder.class.getName())) {
logger.log(TreeLogger.ERROR, "The type: " + requestedClass
+ " Must implement only one interface: "
+ ErrorBinder.class.getName());
throw new UnableToCompleteException();
}
// Get parameterized type
JParameterizedType parameterType = implementedTypes[0]
.isParameterized();
if (parameterType == null) {
logger.log(TreeLogger.ERROR, "The type: " + requestedClass
+ " Must implement only one parameterized interface: "
+ ErrorBinder.class.getName());
throw new UnableToCompleteException();
}
if (parameterType.getTypeArgs() == null
|| parameterType.getTypeArgs().length != 1) {
logger.log(TreeLogger.ERROR, "The type: " + requestedClass
+ " Must implement only one parameterized interface: "
+ ErrorBinder.class.getName() + " with only onde Parameter");
throw new UnableToCompleteException();
}
parameterizedType = parameterType.getTypeArgs()[0];
ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(
implPackageName, implTypeName);
composerFactory.addImport(Map.class.getCanonicalName());
composerFactory.addImport(List.class.getCanonicalName());
composerFactory.addImplementedInterface(objectType
.getQualifiedSourceName());
PrintWriter printWriter = context.tryCreate(logger, implPackageName,
implTypeName);
if (printWriter != null) {
SourceWriter sourceWriter = composerFactory.createSourceWriter(
context, printWriter);
composeBindErrorsMethod(sourceWriter);
sourceWriter.commit(logger);
}
return implPackageName + "." + implTypeName;
}
private void composeBindErrorsMethod(SourceWriter sourceWriter) {
sourceWriter.print("public void bindErrors("
+ parameterizedType.getQualifiedSourceName()
+ " erroable, Map<String, List<String>> errors) {");
sourceWriter.print(" System.out.println(\"Implement it now:)\");");
sourceWriter.print("}");
}
}
Now I have only to generate the code for bindErrors and implement the setFiledErrors method in my UserEditor like this:
public void setFieldErrors(Map<String, List<String>> fieldErrors) {
errorBinder.bindErrors(this, fieldErrors);
}
I hope this can help someone to bootstrap their own GWT generators.
GWT, java, Javascript, Software, WEB 2.0