Thursday, May 20, 2010

Using Vaadin with Seam

I'm a big fan of both frameworks. Unfortunately, the direct ajax model doesn't yet integrate as well as JSF does with Seam. I created a model which does the trick, but I'm not that content with it yet (see DAAM), and the implementation is still experimental. For my current project I have to create some simple administration interfaces, for which Vaadin is a really neat choice. And of course I don't want to give up the convenience of Seam.

There's a simple way of enabling Seam stuff in a Vaadin application, as also proposed here: http://vaadin.com/forum/-/message_boards/message/116273. This enables using Seam Contexts and Seam transaction management in your Vaadin application code. Somehow the solution didn't work out for me, so I created my own servlet filter which does the same two thing (contexts and transactions):

 @Override
 public void doFilter(final ServletRequest request, final ServletResponse response, final FilterChain chain) throws IOException, ServletException {
  new ContextualHttpServletRequest((HttpServletRequest) request) {
   @Override
   public void process() throws Exception {
    try {
     beginTransaction();
     chain.doFilter(request, response);
    } finally {
     commitOrRollBack();
    }
   }
  }.run();
 }

The transaction handling methods are copy-pasted from the Seam JSF integration implementation:

/**
  * Code from SeamPhaseListener (2.2.0 GA)
  */
 public static void beginTransaction() {
  try {
   if (!Transaction.instance().isActiveOrMarkedRollback()) {
    Transaction.instance().begin();
   }
  } catch (Exception e) {
   throw new IllegalStateException("Could not start transaction", e);
  }
 }
 
 /**
  * Code from SeamPhaseListener (2.2.0 GA)
  */
 public static void commitOrRollBack() {
  try {
   if (Transaction.instance().isActive()) {
    try {
     Transaction.instance().commit();

    } catch (IllegalStateException e) {
     log.info("TX commit failed with illegal state exception. This may be " + "because the tx timed out and was rolled back in the background.", e);
    }
   } else if (Transaction.instance().isRolledBackOrMarkedRollback()) {
    Transaction.instance().rollback();
   }
  } catch (Exception e) {
   throw new IllegalStateException("Could not commit transaction", e);
  }
 }

But this is not all the way we can go. I want to use my EntityManager and other Seam components in my UI code. My UI classes are of course not Seam components (this is what is basically different in DAAM), but we can do a little trick. The methods in our UI classes are usually invoked by user interface events, eg. button clicks. I created a basic event delegate that, before actually invoking the delegated method, looks at the target object and handles its @In annotations. The implementation is quite simple:

public class InjectingEventDelegate {
 
 Object component;
 
 String methodName;
 
 public InjectingEventDelegate(Object component, String methodName) {
  this.component = component;
  this.methodName = methodName;
 }
 
 public void doDelegate() {
  inject();
  try {
   Method method = component.getClass().getMethod(methodName);
   method.invoke(component);
  } catch (Exception e) {
   throw new RuntimeException(e);
  }
 }
 
 protected void inject() {
  for (Field field : component.getClass().getDeclaredFields()) {
   if (field.isAnnotationPresent(In.class)) {
    In in = field.getAnnotation(In.class);
    String name = field.getName();
    if (!StringUtils.isEmpty(in.value()))
     name = in.value();
    Object toInject = Component.getInstance(name);
    if (toInject == null)
     throw new RuntimeException("Seam component with name '" + name + "' not found, trying to inject field " + field.getName() + " on " + component + " for invoking " + methodName);
    try {
     field.set(component, toInject);
    } catch (Exception e) {
     throw new RuntimeException("Count not inject field " + field.getName() + " on " + component + " for invoking " + methodName + ". Is the field declared public?", e);
    }
   }
  }
 }

}

To help binding these delegates, I created an annotation based action binder, as follows

public class ActionBinder {
 
 public static void bind(Object component) {
  for (Field field : component.getClass().getDeclaredFields()) {
   if (field.isAnnotationPresent(ActionBinding.class)) {
    ActionBinding actionBinding = field.getAnnotation(ActionBinding.class);
    try {
     Object fieldValue = field.get(component);
     Object delegate = new InjectingEventDelegate(component, actionBinding.value());
     if (fieldValue instanceof Button) {
      ((Button)fieldValue).addListener(ClickEvent.class, delegate, "doDelegate");
     }
    } catch (Exception e) {
     throw new RuntimeException(e);
    }
   }
  }
 }

}

And that's it. We can now create UIs like this:

public class Page extends VerticalLayout {
 
 @ActionBinding("add")
 public Button addButton;

        @In
        public EntityManager em;

        public Page() {
            addButton = new Button("add");
            addComponent(add);
            ActionBinder.bind(this);
        }

        public void add() {
            // do operations with EntityManager injected.
        }
}

2 comments:

  1. Hi Gabor

    Nice stuff. How is this working? Any gotchas?

    Is there a place to download your SEAM integration, or is the above all you need?

    ReplyDelete
  2. Hi Warren,
    It's working well, there are just some things you have to be aware of:
    1) You easily get used to that you have injection, but it's not in all places (eg. other listeners), only if it is handled by the injectingeventdelegate :)
    2) Every single server request implies a new transaction (this is common in jsf, but with vaadin it might be unusual), and there is no conversation handling yet - so you have to be cautious using JPA, to avoid lazy exceptions.

    The above is all you need, i saw no point in putting them in a jar.

    ReplyDelete