Saturday, August 20, 2011

Transparent dynamic forms with LazyList

Some time ago I wrote few words about some dynamic forms implementation methods using LazyList or AutoPopulatingList with items removing. The problem that interested me today was related to this subject, but concerned something else: how to implement dynamic forms in the way, so that it can be used transparently for any object (even a Hibernate entity) without creating specialized form beans for that.

If you write a Hibernate application there are at least two schools about implementing forms backend. First tells you to create a object for each form, representing given form data, second lets you use directly Hibernate entities in forms. The first one is maybe cleaner than the second one from the "patterns compliance" point of view, but produces a lot of unnecessary code. There's s lot of stuff on the net to read about this. Some people do this, some that. I'm not eager to generalize things and give the best solution always, but from my point of view it just depends. It depends mainly on the project size and overall application requirements, but generally I believe that if you don't make a big financial system with 20+ developers, and if you can apply session-per-request (or session-per-conversation) model, (ie. you don't have eg. many databases and session factories, distributed transactions management etc.) working together with OSIV, you may consider using directly you business objects across the whole application.

I can evaluate that 50-80% forms in the usual web application are just the domain object editors. In mentioned application, if you like to edit all these entities with form beans, you need to maintain large (1:1 with business model) class hierarchy, looking exactly the same like the domain object hierarchy. Moreover, you need to maintain a lot of conversion code, making one objects from another. Is this really needed? I consider not, as well as for example these guys. I'm the follower of using domain objects throughout whole application, in as many places as you can, and write just a feature code, instead of maintaining every day tons of code that will never bring any benefits.

Today I had a time to verify these assumptions in pure Spring environment. I previously made other projects with such approach, but never really used pure Spring in a web layer. I consider this as a very good (and lightweight in proportion to pure JEE) middleware, but about Spring MVC/WebFlow I had a different opinion, in which these are not well-rounded and very crude. For those people looking for better solutions I can recommend Stripes Frameworks, what is the best web framework I used until now in Java world. Spring guys could learn a lot from Stripes guys about how to create a greatly flexible web (layer) framework. But, let's don't gripe, I use also Spring in Stripes-based applications as a base framework (IOC, scheduling, webservice, you know ...) and it's great. The only caveat I can have to MVC/WebFlow part.

Returning to the subject: how to implement transparent dynamic forms for any class used as form bean, I need to tell that this all is about the Binder you use. By the Binder I mean the code that converts your plain strings from HTTP request into an object property values. For example StripesFramework has its own binder that is very flexible and extendable, so I've never had problems with writing anything with this. Regarding Spring code it looks little worse, but this is for a while. Anyway, if you'd like to have dynamic forms handling transparent, you  need to have appropriately configured binder, that is able to manage with these concerns. If you don't have a flexible Binder, you need to implement all stuff with your form beans with eg. LazyList wrappers, and also all this stuff behind.

In the Spring the main role in binding plays BeanWrapperImpl class. If you take a glance at implementation, you will see some code in getPropertyValue() method taking elements from the target bean collections. This is base implementation assuming that you have these elements already initialized, otherwise it throws exceptions (default Spring implementation).

OK, this is the code that should be extended:  the BeanWrapperImpl should be switched to a different implementation, considering dynamic lists for whatever objects playing "form bean" role. But, unfortunately, not for Spring. The BeanWrapperImpl class simply cannot be extended, because of its horrible design, like a lot of private methods and dependencies to other package-visible classes. In spring MVC, this (the most) important part of the request processing is completely monolith and not extendable. From the good framework I expect that if the behavior is undesirable, I can change this, but Spring doesn't give that.

But, what if I want to have this in a Spring or whatever else badly designed binder? I just need to make it myself in the independent code. The working solution I applied is below. First thing is to get to a binding code. In your framework you will probably have somewhere some code looking like this:

public class Binder {

  public void bind(Object root, RawValues values) {
    doBind(..);
  }     
        
}
 
This is the base binder call that requires a root object (form bean) and the request parameter values in some form. For example if you use WebFlow in FormAction the binder (WebDataBinder) is being created in createBinder() method. You can switch the implementation there to you binder class and override some behavior (the DataBinder in Spring can be easily inherited, but it internally uses BeanWrapperImpl that does all job, and it cannot). This is how does look my data binder implementation for Spring:

public class DynamicDataBinder extends WebDataBinder {
        
  @Override
  protected void doBind(final MutablePropertyValues mpvs) {
    new DynamicBindingWrapper(getTarget(), new Runnable() {
      @Override
      public void run() {
        _doBind(mpvs);
      }
    });
  }
        
  protected void _doBind(MutablePropertyValues mpvs) {
    super.doBind(mpvs);
  }
        
}

This is the Spring example, but of course you can use this for any framework you use, because binding needs always to look similar in any framework (requires an object to be bound and the parameters). What I do here clearly is to wrap the binding process (done in super.toBind() - I needed to put it into separate method because of anonymous Runnable class utilization), that can be done in its default way, with some code applying the dynamic forms behavior to form bean.

The thing that enclosing code need to do is to:
  • make all required collections appropriate for dynamic form binding
  • allow to execute the binding in the way that binder code in unaware of non-existing collection items
  • restore collections to previous state, to be further (after binding) processes in a regular way
The last point is very important, because for Hibernate entities that are already persistent, we cannot switch a collection (eg. to HashSet)  and disjoin the Hibernate persistent (eg. PersistentSet) implementation. For persistent entities we have to ensure that after binding all entity collections are in the same implementations as they were before binding.

To mark the collections that during the binding should behave "dynamically" I  use additional annotation, because only for some special collection we need to apply this. For example if we have an Invoice having InvoiceItem collection that we need to edit in a dynamic way on a single form. The annotation I use is very simple:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DynamicList {
  Class value();
}

In the value() attribute we will point the exact collection generic type, so that the list could produce new items. This also can be done by reflection, but here for simplify we will name exact class name. So, our exemplary Invoice for dynamic item handling should look like that:

private class Invoice {

  @DynamicList(InvoiceItem.class)
  protected List<InvoiceItem> items = new ArrayList<InvoiceItem>();

  public List<InvoiceItem> getItems() {
    return items;
  }

  public void setItems(List<InvoiceItem> items) {
    this.items = items;
  }
  
}

I've intentionally omitted Hibernate annotations, that should be put on this class and its fields (if you use annotation Hibernate configuration). We are currently interested only in dynamic forms.

Now, I have to say something about how I assume this works. The dynamic collection for me is completely (also considering underlying objects graph) reflected in the HTML form. This may be not appropriate in some cases, so if it's not appropriate for you, you need to apply different "merging" mechanism. For my (usual) cases it's easy, because if we have all stuff held in the form (these already persisted items, together with a new ones), we may always produce the entity collection from the beginning: by clearing the existing collection and producing new one (the existing items will be deleted and re-created). This is the simplest approach and it's appropriate in most cases.

Having all these assumptions, the final algorithm is following:
  • we look for @DynamicList collection fields in target form bean, these ones  we've found we wrap with ShrinkableLazyList to be "dynamic form aware",
  • when we produce new items by LazyList factory, we need also to wrap these entities in the same fashion, because we may have much longer objects graph requiring the same dynamic forms behavior,
  • on the end of process we need to "return" previous collection instances to the original form beans (if it's persistent Hibernate entity, it would very much like to have the same PersistentCollection instance that it assumes to be present in particular entity property).
The implementation looks following:

public class DynamicBindingWrapper {

  /** 
   * A SkrinkableLazyList implementation for collections wrapping.
   * By this implementation we assert that this wrapping collection
   * is compatible with both Set and List.
   **/
  protected class DynamicBindingCollection extends ShrinkableLazyList
      implements Set {

    protected Object target;
    protected Field field;
    protected Collection wrapped;

    protected Class itemClass;

    public DynamicBindingCollection(Object target, Field field,
        final Class itemClass) throws IllegalArgumentException,
        IllegalAccessException {
      // initialize on empty wrapped collection + init items factory       
      super(new ArrayList(), new Factory() { 
 
        @Override
        public Object create() {
          try {
            Object o = itemClass.newInstance();
            begin(o); // all new instances need to be wrapped to assert  
                      // dynamic binding on nested paths 
            return o;
          } catch (Exception e) {
            e.printStackTrace();
            return null;
          }
        }

      }, true);

      // init fields       
      this.target = target;
      this.field = field;
      this.itemClass = itemClass;

      // wrap the original object 
      field.setAccessible(true);
      Object value = field.get(target);

      if (value instanceof Set || value instanceof List) {
        this.wrapped = (Collection) value;
        field.set(target, this); // wrap the target bean collection
        register(this); // register for unwrapping       
      } else
        throw new IllegalArgumentException(
            "Only Set and List instances can be wrapped for DynamicList");
    }

    public void unwrap() throws IllegalArgumentException,
        IllegalAccessException {
      // clear wrapped collection to be populated again from the form 
      wrapped.clear();

      // get real values (indirectly calls ShrinkableLazyList.shrink())       
      for (Object o : this)
        wrapped.add(o); 
 
      // restore original collection 
      field.setAccessible(true);
      field.set(target, wrapped);
    }

  }

  protected List<DynamicBindingCollection> collections = 
    new ArrayList<DynamicBindingWrapper.DynamicBindingCollection>();

  public DynamicBindingWrapper(Object target, Runnable task) {
    begin(target);
    task.run();
    done();
  }

  /** Main wrapping method **/

  protected void begin(Object target) {
    try {
      Class clazz = target.getClass();
      while (clazz != null) {

        for (Field field : clazz.getDeclaredFields()) {
          DynamicList annot;
          if ((annot = field.getAnnotation(DynamicList.class)) != null)
            // wrap the collection with lazy list
new DynamicBindingCollection(target, field, annot.value()); } clazz = clazz.getSuperclass(); } } catch (Exception e) { throw new RuntimeException("Cannot get properties from bean", e); } } /** Ends binding session and unwraps instances */ protected void done() { try { for (DynamicBindingCollection collection : collections) collection.unwrap(); } catch (Exception e) { throw new RuntimeException("Cannot unwrap dynamic collection", e); } } protected void register(DynamicBindingCollection collection) { collections.add(collection); } }

Today tests showed that this works nice both for transient and persistent Hibernate entities used as "form beans" (and of course for non-Hibernate form beans as well).

No comments:

Post a Comment