Announcement Announcement Module
Collapse
No announcement yet.
Lessons learned adding SWF to a Roo app Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • Lessons learned adding SWF to a Roo app

    I wanted to add some more advanced navigation to my Roo app, and thought that Spring Web Flow might be the way to go. Here's what I learned in the course of getting it all to work.

    As background, I'd never used SWF before apart from on the Spring Rich Web Apps course, where (a) the lab exercises do a lot of the heavy lifting for you and (b) the base app is not a Roo app. So while I may have achieved a working solution, it might not be in the most orthodox SWF style, in which case hopefully someone more expert will put me straight.

    One obstacle I encountered is that the "booking-mvc" sample project provided with the SWF download to demonstrate using SWF with MVC and JSP doesn't show a drop-down being populated by domain objects. This a pretty glaring omission IMHO, given how often this requirement arises in real-world apps and the fact that getting it working is non-trivial (at least for an SWF newbie like me).

    SWF Basics (not Roo-specific)

    Once you've added all the necessary plumbing (which Roo does most of), the heart of SWF is the XML file that defines each flow. This file takes the place of the Java @Controllers used by normal Spring MVC, i.e. it's responsible for things such as:
    • create or obtaining the form backing object
    • providing any reference data (e.g. lists of dependent objects for drop-downs)
    • invoking (or suppressing) binding and validation
    • specifying which fields should be editable (similar to how a Java controller calls WebDataBinder#setAllowedFields from an @InitBinder method)
    • invoking any business methods (e.g. persisting/updating a domain object from the values on a form)
    General SWF Tips
    • any domain objects used as flow variables need to be Serializable; SWF will give you a nice message if they're not
    • for proper binding of domain object fields to form controls, any domain objects used as drop-down items need an equals() method that compares (at least) the id field
    • SWF's default FlowExecutionExceptionHandler only displays the outer exception, not the root cause; to get any useful exception messages, you need to plug in your own class that recursively calls Throwable#getCause(); see my sample code below
    • to reduce maintenance in your POM, use a ${spring.webflow.version} property for the SWF dependencies instead of repeating the literal version number
    Roo-Specific Tips
    • Roo provides a "web flow" shell command that does most of the plumbing for you (e.g. creating webflow-config.xml and importing it from webmvc-config.xml).
    • However, in Roo 1.0.2 at least, the "web flow" command does not wire in the latest version of SWF; to use it, you need to edit the POM as follows:
      • set the SWF version as desired (using a POM property as explained above)
      • add the latest 3.0.x release of spring-expression.jar
      • remove the dependency upon JBoss EL
      • exclude spring-webflow's Spring 3.0.3 dependencies (find these using mvn:dependency-tree)
    • Your flow will often need to obtain domain objects using Roo finders, which are static. How to call a static method from SWF depends which expression language you are using. In Spring EL, the expression syntax is as follows:
      • expression="T(com.example.pizza.Pizza).findAllPizz as()"
    • By default, SWF uses its own validation logic that ignores your entities' JSR-303 annotations. The workaround is to write your own SWF-style validation classes and methods that delegate to your app's existing JSR-303 validator, as explained here.
    • The "view" attributes of your flow's "view-state" elements need to be logical view names, such as those defined in your Tiles views.xml files. This works thanks to Roo's addition of the mvcViewFactoryCreator bean to webflow-config.xml.
    • For some reason (and I could be wrong about this), flows can't be located:
      • within a sub-folder of any domain type, e.g. WEB-INF/views/book/myFlow (because the Book object's Java @Controller will handle the request instead, even though Roo assigns order=0 to the FlowHandlerMapping, wtf?)
      • outside the WEB-INF/views folder hierarchy (e.g. in WEB-INF/flows)
    • To use your Roo domain objects in drop-downs, implement your own ConversionService that can convert the listed domain objects to Strings (whereas the MVC Java @Controllers seem to be able to handle this requirement automatically). A sample conversion service is provided below.
    • For SWF transitions to work, the buttons on your JSPX forms need to have a "name" attribute of the form "_eventId_xxxx", where "xxxx" is the event name used within your flow XML file.
    A More Useful FlowExecutionExceptionHandler

    Code:
    package com.example.books.web.flow;
    
    import java.io.IOException;
    
    import org.springframework.stereotype.Component;
    import org.springframework.webflow.engine.FlowExecutionExceptionHandler;
    import org.springframework.webflow.engine.RequestControlContext;
    import org.springframework.webflow.execution.FlowExecutionException;
    import org.springframework.webflow.execution.View;
    
    @Component("myFlowExceptionHandler")
    public class MyFlowExecutionExceptionHandler
        implements FlowExecutionExceptionHandler
    {
        @Override
        public boolean canHandle(final FlowExecutionException exception) {
            return true;
        }
    
        @Override
        public void handle(final FlowExecutionException exception,
                final RequestControlContext context)
        {
            debug(exception);
            Throwable cause = exception.getCause();
            while (cause != null) {
                debug(cause);
                cause = cause.getCause();
            }
        }
    
        private void debug(final Throwable throwable) {
            System.out.println(throwable.getClass().getName());
            System.out.println(throwable.getMessage());
            throwable.printStackTrace();
        }
    }
    Wire this into your flow XML files as follows:

    Code:
    <flow ...>
        ...
        <exception-handler bean="myFlowExceptionHandler"/>
    </flow>
    Sample ConversionService

    Code:
    package com.example.books.web.flow.convert;
    
    import org.springframework.binding.convert.service.DefaultConversionService;
    import org.springframework.stereotype.Component;
    
    @Component
    public class MyConversionService extends DefaultConversionService {
    
        /**
         * Constructor
         */
        public MyConversionService() {
            addDefaultConverters();
            addConverter(new PublisherToStringConverter());
        }
    }
    Code:
    package com.example.books.web.flow.convert;
    
    import org.springframework.binding.convert.converters.Converter;
    import org.springframework.util.Assert;
    
    import com.example.books.domain.Publisher;
    
    public class PublisherToStringConverter implements Converter {
        
        @Override
        public Class getTargetClass() {
            return String.class;
        }
    
        @Override
        public Class getSourceClass() {
            return Publisher.class;
        }
    
        @Override
        public Object convertSourceToTargetClass(
                final Object source, final Class targetClass)
        {
            Assert.isTrue(String.class.equals(targetClass));
            if (source == null) {
                return null;
            }
            final Publisher publisher = (Publisher) source;
            return String.valueOf(publisher.getId());
        }
    }
    Wire this in via webflow-config.xml as follows:
    Code:
    <webflow:flow-builder-services ...
           conversion-service="myConversionService" />
    Conclusion

    I hope this all helps someone, and please set me straight if I've got anything wrong or made it harder than it needs to be.
    Last edited by Andrew Swan; Aug 31st, 2010, 11:40 PM. Reason: Corrected egregious claim that you can't call static methods from SWF

  • #2
    Ever solve the converter issue?

    Thanks for this write-up - I'm attempting a similar thing myself at the moment. I also ran into the ConversionService issue: I was wondering whether you ever go to the bottom of why the custom Converters work fine from Spring MVC, but not from SWF?

    For the record, I have my webflow re-use the auto-generated JSPX files, so the view technology is identical. I've also tried using DefaultConversionService & passing the Roo-generated ApplicationConversionService into the constructor (since it seems to support this delegation model), however, i still get conversion errors on my OneToMany mappings.

    I realize I can probably solve this by wiring in my own converters manually, but I'm trying to leverage Roo's autogenerated artifacts as much as possible & integrate SWF as lightly as possible.

    Comment


    • #3
      Originally posted by rdouglas View Post
      I was wondering whether you ever go to the bottom of why the custom Converters work fine from Spring MVC, but not from SWF?
      No, I didn't. But in other news, I see that SWF 2.3.0 has been released, with JSR-303 support, which means that this statement is no longer true:

      By default, SWF uses its own validation logic that ignores your entities' JSR-303 annotations. The workaround is to write your own SWF-style validation classes and methods that delegate to your app's existing JSR-303 validator, as explained here.
      I haven't tried 2.3.0 yet, but it looks as though that workaround should no longer be necessary.

      Comment


      • #4
        I'm using 2.3.0 (with Roo 1.1.2) - overall the experience seems much the same as you describe.

        One thing about the issue of flow location you mention above - I think flows can be located anywhere, but a combination of defaults probably prevent you from moving them out.

        For one, the default FlowRegistry configured by Roo sets a basePath of /WEB-INF/views; the other is the TilesConfigurer in the MVC config, which only searches below /WEB-INF/views for the views.xml Tiles definitions. I updated the FlowRegistry basePath to /WEB-INF/flows and added /WEB-INF/flows/**/views.xml to the TilesConfigurer & flows seem to work fine.

        Comment


        • #5
          More info

          I did some more digging & solved the Converter error issue. The solution is posted here.

          Along the way I also figured out why you can't place flows such that their ID conflicts with the existing MVC views, even though FlowHandlerMapping specifies order=0.

          Turns out, the <mvc:annotation-driven> magic silently registers all its mappings with order=0 too. Presumably its processed first since webmvc-config.xml imports webflow-config.xml so it takes precedence. I believe, but haven't tried, that you could set order=-1 to have flows take precedence.

          Comment


          • #6
            Originally posted by rdouglas View Post

            Along the way I also figured out why you can't place flows such that their ID conflicts with the existing MVC views, even though FlowHandlerMapping specifies order=0.

            Turns out, the <mvc:annotation-driven> magic silently registers all its mappings with order=0 too. Presumably its processed first since webmvc-config.xml imports webflow-config.xml so it takes precedence. I believe, but haven't tried, that you could set order=-1 to have flows take precedence.
            Interesting, I wonder if that's why I have seen beans be rejected during upstart and no obvious way – well, to me anyway –to tell whether it was my config or the annotation driven causing it.
            I've been looking for am extensive detailed listing somewhere of how to replicate what mvc:annotation-driven does. I've only see the brief description in the blogs and in some books.
            Where are the details? Anyone know?

            Comment


            • #7
              Originally posted by MiB View Post
              I've been looking for am extensive detailed listing somewhere of how to replicate what mvc:annotation-driven does. I've only see the brief description in the blogs and in some books. Where are the details?
              I think your best bet is to ask that in the web forum...

              Comment

              Working...
              X