miércoles, 25 de julio de 2007

Integrating DWR & Spring Webflow

I've written several times before about how to integrate DWR with Spring. In fact both technologies can be integrated pretty well without much fanfare, they practically do it out-of-the-box. This includes Spring MVC which just needs a couple of hacks (just like proxies or scoped beans). With Spring WebFlow (SWF) it happens again but unfortunately the documentation is really scarce making the process somewhat more confusing than it should. Let's review step by step how to do it.

The first thing to take into account is how to access the flow from outside SWF. This is done via the FlowExecutor interface and the single implementation available. This interface defines just three methods to control a flow (launch, refresh, resume), very little to suit our needs so our first task is to extend it and add our desired functionality

public interface ExternalFlowAccess extends FlowExecutor {
   Map<String, Object> getBackingObject(String key, HttpServletRequest req);
   ... pieceMerge(...);
   ... validate(...);
   ... addObjectToCollection(...);
}

Several methods have been created, the first one is intended to retrieve the backing object, the others supposedly would have some other functionality. During the rest of the post (to simplify the code) I will just implement the first operation.

The interesting things in the above code are:
  • Extending FlowExecutor gives us access to the launch, refresh and resume methods automatically
  • The flowExecutionKey (the flow identifier) has always to be included as is the only way to retrieve the correct flow
  • Everytime a flow is called the flow internal state changes so a new flowExecutionKey has to be returned (as a String)
Once the needs are settled an implementation is in order:

public class DWRFlowAccess implements ExternalFlowAccess, ServletContextAware

   private ServletContext sc;
   private FlowExecutor flowExecutor;

   public void setServletContext(ServletContext sc) {
      this.sc = sc;
   }

   @Required
   public void setFlowExecutor (FlowExecutor flowExecutor) {
      this.flowExecutor = flowExecutor;
   }

   ResponseInstruction resume(String key, String evtId, ExternalContext ctx) {
      return flowExecutor.resume(key, evtId, ctx);
   }

   ...

   private String triggerEvt(String key, String evtId, HttpServletRequest req) {
      ServletExternalContext extCtx = new ServletExternalContext(sc, req, null);
      flowExecutor.resume(key, evtId, extCtx).getFlowExecutionKey();
   }

   public Map<String, Object> getBackingObject(String key,
                                                HttpServletRequest req) {
      Map<String, Object> res = new HashMap<String, Object>();
      res.put("flowExecutionKey", triggerEvt(key, "bkObj", req));
      res.put("bkObj", req.getAttribute("bkObj"));
      return res;
   }

   ...

}

I've tried to keep the code above as simpler as possible yet it should show the way to follow. The original methods are implemented redirecting the call to the injected flow executor. The specific (new) methods are implemented calling the resume method (it signals an event to a paused flow) and passing/retrieving parameters from the HTTP request. Just using this technique everything can be done but it has a handicap, it forces the flow designer to modify each flow adding new states for AJAX calls (if they weren't already available). Before looking into this problem let's see the configuration that has to be added (Spring MVC is supposed here):

<flow:executor id="flowExecutor" ...>
   <flow:repository type="continuation" max-conversations="1" ... />
</flow:executor>

<bean id="flowMapper" class="...SimpleUrlHandlerMapping">
   <property name="mappings">
      <props>
         <prop key="/execute.flow/*">flowController</prop>
         <prop key="/k/*">flowController</prop>
      </props>
   </property>
</bean>

<bean id="DWRflowAccess" class="...DWRFlowAccess">
   <property name="flowExecutor" ref="flowExecutor" />
   <dwr:remote javascript="Flow" />
   <aop:scoped-proxy proxy-target-class="false" />
</bean>

The above code declares the flow executor, maps the needed paths to it and declares a bean that will be remoted by DWR as Flow (the scoped proxy is a safety guard). The only interesting tidbit is the use of REST-style URLs. This is a must! Otherwise the changing flow execution key will not be retrieved (by default SFW just obtains the first key from the array which always happens to be the one from the query string).

It's time now to revisit the automatic (dynamic) creation of states and transitions in a flow so it can support AJAX calls without the need to modify the existing XML definition (that is, during runtime). To do this some considerations are in order
  • This approach is just useful for methods that are standard (in theory, they could apply to any flow), like the ones defined above
  • As the states injected are not part of the flow itself, the flow should not end (or pause) in them. That is, the flow should be left (in nearly all the cases) in the same state as it was before the AJAX call (but not with the same flow execution key!)
  • It should not impact performance (but it can take some time on startup)
  • It should integrate seamlessly with SWF (no hacks if at all possible)
Java and SWF offer a nice base by extending FormAction. I'll try to explain how everything is supposed to work first (as it can get pretty complicated). Remember that all the following code is optional and can be avoided if each flow is modified accordingly.

The FormAction class during runtime is configured calling the setupForm method. This is usually a must. The idea is to leverage this method to create as many new states as methods declared in the ExternalFlowAccess interface. To reach each of this states a global transition will have to be declared as well. It needs to be global because it can trigger from any original state.

@Override
public Event setupForm(RequestContext context) {
   super.setupForm(context);
   Flow flow = (Flow) context.getActiveFlow();
   try {
      flow.getState("dwrstate");
   } catch (IllegalArgumentException) {
      // Create a state to handle all DWR actions
      ActionState state = new ActionState(flow, "dwrstate");
      // This will handle everything
      AnnotatedAction dwrBinding = new AnnotatedAction(this);
      // Using the DWROperationManager method
      dwrBinding.setMethod("DWROperationManager");
      state.getActionList().add(dwrBinding);
      // Create global transitions to this state for different events
      TargetStateResolver resolver = new DefaultTargetStateResolver("dwrstate");
      // Repeat for each method
      TransitionCriteria bkObj = new EventIdTransitionCriteria("bkObj");
      Transition transition = new Transition(bkObj, resolver);
      flow.getGlobalTransitionSet().add(transition);
   }
   return success();
}

Finally just one state was needed ("dwrstate"). It will handle all the events (in this case just "bkObj") using one method ("DWROperationManager") that will dispatch operations. The try-catch block was needed to ensure that just the first time everything is created.

public Event DWROperationManager (RequestContext ctx) {
   //Obtain the state we came from
   StateDefinition st = (StateDefinition) ctx.getRequestScope().get("origin");
   ActionState me = (ActionState) ctx.getActiveFlow().getState("dwrstate");
   //Remove all previous transitions
   for (TransitionDefinition transition : me.getTransitions())
      me.getTransitionSet().remove((Transition) transition);
   //Create a transition to return to the origin
   Transition t = new Transition(new DefaultStateResolver(st.getId()));
   me.getTransitionSet.add(t);
   // Dispatch the call
   String id = ctx.getLastEvent().getId();
   if ("bkObj".equals(id)) bkObj(ctx);
   return sucess();
}

There's a hack in the code above. It's just not possible (as far as I know) to retrieve the original state. To obtain it a custom listener has to be created. The code for the listener is easy fortunately

public class OriginalStateListener extends FlowExecutionListenerAdapter {
   public void requestSubmitted(RequestContext ctx) {
      try {
         ctx.getRequestScope().put("origin", ctx.getCurrentState());
      } catch (IllegalStateException ex) {
         // Is flow initialized?
      }
   }
}

The listener has to be declared inside the flow:executor bean definition. Finally just implement the logic for each operation.

public Event bkObj(RequestContext ctx) {
   MutableAttributeMap requestAttr = ctx.getExternalContext().getRequestmap();
   requestAttr.put("bkObj", getFormObject(ctx));
}

To configure everything nothing special has to be done. It's exactly the same as using a FormAction directly. I hope everything was clear enough. I'll try to create a sample project with a flow and post it to the DWR mailing list and at Internna's repository (but it will take some time).