Donnerstag, 11. Juni 2015

FormGenerator: Automatic html forms from java objects

Purpose

One of Ruby on Rails' features I quite liked but missed in other frameworks, is code generation. The scaffolding produces forms and controllers for a given object automatically and saves the files in appropriate folders. This is mainly convention driven.

With Java, we have a nice type system, so why do I have to write forms over and over again? I used reflection to automatically generate an html form for a given object.

Algorithm

Basically, the FormGenerator gets an arbitrary object attached. On this object, first all fields and inherited fields have to be obtained. Since it would be useless to just process public fields, I had to use a small piece of utility code I found here.



public Iterable<Field> getFieldsUpTo(@Nonnull Class<?> startClass,
                                     @Nullable Class<?> exclusiveParent) {

  List<Field> currentClassFields = Lists.newArrayList(startClass.getDeclaredFields());
  Class<?> parentClass = startClass.getSuperclass();

  if (parentClass != null &&
     (exclusiveParent == null || !(parentClass.equals(exclusiveParent)))) {
    List<Field> parentClassFields =
                (List<Field>) getFieldsUpTo(parentClass, exclusiveParent);
    currentClassFields.addAll(parentClassFields);
  }

  return currentClassFields;
}


After the information about the object is obtained, the form fields are wrapped by a form begin/end pair. The private fields have to be made accassible - note the exception handling.


try {
  field.setAccessible(true);
} catch (SecurityException e) {
  Logger.getGlobal().info(String.format("Field %s can't be accessed, so no input for this field.", field.getName()));
  return result;
}


Depending on the type of the field, inputs should be generated. The types are determined at runtime, so a switch is needed for the value extraction.



if(type.equals(String.class)) {
  result += generate(formGenerator.getFieldName(field.getName()), (String) field.get(object));
} else if(type.equals(Boolean.class)) {
result += generate(formGenerator.getFieldName(field.getName()), (Boolean) field.get(object));
} else if(type.equals(boolean.class)) {
  result += generate(formGenerator.getFieldName(field.getName()), field.getBoolean(object));
} else if(type.equals(Integer.class)) {
  result += generate(formGenerator.getFieldName(field.getName()), (Integer) field.get(object));
} else if(type.equals(int.class)) {
  result += generate(formGenerator.getFieldName(field.getName()), field.getInt(object));
} else if(type.equals(Float.class)) {
  result += generate(formGenerator.getFieldName(field.getName()), (Float) field.get(object));
} else if(type.equals(float.class)) {
  result += generate(formGenerator.getFieldName(field.getName()), field.getFloat(object));
} else if(type.equals(List.class)) {
  result += "<div class=\"well\">" + newLine;
  result += String.format("<div id=\"%s\">%s", formGenerator.getFieldName(field.getName()), newLine);
  result += generate(formGenerator.getFieldName(field.getName()), (List) field.get(object), (ParameterizedType) field.getGenericType());
  result += "</div>" + newLine;
  result += "</div>" + newLine;
}

After all primitive types and lists/collections/iterables or whatever are treated, this method can be called recursively to treat arbitrary classes for fields again. It's probably not the best idea to hardcode css classes into this methods, but for my purposes and right now, bootstrap is the only ui framework I satisfy.

Attention has to be paid for generics. For lists, I implemented a treatment in the following way.



static String generate(String fieldName, List value, ParameterizedType type) {
    StringBuilder builder = new StringBuilder();

    int counter = 0;
    for (Object listItem : value) {
        Class<?> componentClass = (Class<?>) type.getActualTypeArguments()[0];
        String listItemFieldName = fieldName + "_" + counter;
        try {
            Method InputGeneratorMethod = InputGenerator.class.getDeclaredMethod("generate", String.class, componentClass);
            String generatedFormElements = (String) InputGeneratorMethod.invoke(null, listItemFieldName, listItem);
            builder.append(generatedFormElements);
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        counter++;
    }

    return builder.toString();
}

The method invocation can be done via reflection again. With the given type, the correct overloaded method is chosen at runtime. However, this could lead to exceptions that one has to handle properly *cough*.

Results

The following class definition is used in my tests.


class MyClass {
    private String testString = "testStringValue";
    private Boolean testBoolean = true;
    private Boolean testBooleanObject = false;
    private int testInt = 12;
    private Integer testInteger = 14;
    private float testFloat = 12.0f;
    private Float testFloatObject = 14.0f;
    private List<String> testStringList = new ArrayList() {{
        add("a0");
        add("a1");
    }};
    private List<Boolean> testBooleanList = new ArrayList() {{
        add(true);
        add(false);
    }};
}

And the generated form looks like this.


It's just an early version yet, there is plenty of stuff left to do. For example the recursive generation for arbirtary objects. Or an injector for style classes. Or field annotations for named fields and non-exported or disabled fields. After this, I'll try to write a reflective argument extractor for ninja, that is capable of parsing request data from generated forms and propagate it back.

Keine Kommentare:

Kommentar veröffentlichen