In our current project we want to have multiple Spring Web Flow-flows in one WAR-file. But we also want the flows and pages to be inside seperate JAR files, making the application a bit more managable and modulair.

This sounds straightforward but it took quite a bit of code and time…

First I created a single WAR-project with all the basic Spring, JSF and Facelet configuration. Like any Spring Web Flow (SWF) project we have a project-servlet.xml.

The first thing I did was changing our flowRegistry:

      <!-- The registry of executable flow definitions -->
      <webflow:flow-registry id="flowRegistry" 
         flow-builder-services="facesFlowBuilderServices" 
         base-path="classpath*:flows">
            <webflow:flow-location-pattern value="/**/*-flow.xml" />
      </webflow:flow-registry>

The classpath*: allows SWF to search the whole classpath for flow-directories containing a flow definition.
In our case it would be: /flows/module1/module1-flow.xml

When you try to run this, and access a page we got the following exception:

Caused by: java.lang.IllegalStateException: A ContextResource is required to get relative view paths within this context; the resource was file [D:\projects\projectfromjar\module1\bin\flows\module1\page1.xhtml]
	at org.springframework.faces.webflow.FlowViewHandler.resolveResourcePath(FlowViewHandler.java:110)
	at org.springframework.faces.webflow.FlowViewHandler.restoreView(FlowViewHandler.java:74)
	at com.sun.facelets.FaceletViewHandler.restoreView(FaceletViewHandler.java:316)
	at org.springframework.faces.webflow.JsfViewFactory.getView(JsfViewFactory.java:93)
	at org.springframework.webflow.engine.ViewState.resume(ViewState.java:193)
	at org.springframework.webflow.engine.Flow.resume(Flow.java:545)
	at org.springframework.webflow.engine.impl.FlowExecutionImpl.resume(FlowExecutionImpl.java:259)
	... 38 more

For some reason Spring Web Flow doesn’t want to load the facelet. After browsing around Spring’s forums I came across some solutions. They didn’t do the trick, only when combining several methods I got it working for Spring Web Flow 2.0.7.

This is how I did it, we need to tell Facelets to use our custom ClassPathResourceResolver:

      <!-- To load flows and pages from JARs, use this resolver -->
      <context-param>
            <param-name>facelets.RESOURCE_RESOLVER</param-name>
            <param-value>nl.redcode.ClassPathResourceResolver</param-value>
      </context-param>

The resolver itself is basic but does the job:

package nl.redcode;

import java.net.URL;

import com.sun.facelets.impl.ResourceResolver;

public class ClassPathResourceResolver implements ResourceResolver {

	public URL resolveUrl(String path) {
	    return getClass().getResource(path);
	}
}

This will help Facelets to translate a given path to a java.net.URL using the current classloader.
Spring Web Flow is currently giving us a FileSystemResource, and this doesn’t work because we want to load the pages with our classloader. For this we have the following wrapper:

package nl.redcode;

import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.ContextResource;
import org.springframework.core.io.Resource;
import org.springframework.util.StringUtils;

class CustomClassPathContextResource extends ClassPathResource implements ContextResource {

    public CustomClassPathContextResource(String path, ClassLoader classLoader) {
        super(path, classLoader);
    }

    public String getPathWithinContext() {
        return getPath();
    }

    public Resource createRelative(String relativePath) {
        String pathToUse = StringUtils.applyRelativePath(getPath(), relativePath);
        return new CustomClassPathContextResource(pathToUse, getClassLoader());
    }
}

To force Spring to use this Resource instead of FileSystemResource we use a post processor:

package nl.redcode;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.webflow.definition.registry.FlowDefinitionRegistry;

public class FlowRegistryClassPathPostProcessor implements BeanPostProcessor {
	
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        return bean;
    }
    
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if(bean instanceof FlowDefinitionRegistry) {
            alterRegistry((FlowDefinitionRegistry) bean);
        }
        return bean;
    }
    
    protected void alterRegistry(FlowDefinitionRegistry registry) {
        for(String flowId : registry.getFlowDefinitionIds()) {
            ApplicationContext ctx = registry.getFlowDefinition(flowId).getApplicationContext();
            overrideResourceLoader((GenericApplicationContext) ctx);
        }
    }
    
    /**
     * Override the ResourceLoader:
     * We know our flow is always defined as "flows/module1".
     * From the context we can derive the name of the flow (module1)
     * So we build up the ClassPathResource base as:
     * "flows/"+ ctx.getResource("")+ "/"
     * 
     * @param ctx
     */
    protected void overrideResourceLoader(GenericApplicationContext ctx) {
    	final ClassPathResource cpr = new ClassPathResource("flows/"+ctx.getResource("").getFilename()+"/");

    	ctx.setResourceLoader(new ResourceLoader() {
        	
        	public ClassLoader getClassLoader() {
        		return cpr.getClassLoader();
        	}

        	public Resource getResource(String location) {
        		return new CustomClassPathContextResource(cpr.getPath() + location, getClassLoader());
        	}
        });
    }
}

When the FlowDefinitionRegistry is created we provide it with a new ResourceLoader. When the resources are requested by Spring Web Flow we create our own CustomClassPathContextResource. This consists of our current location plus the defined location (viewId).

To register this post processor add it to your project-servlet.xml:

<bean id="flowRegistryClassPathPostProcessor" class="nl.redcode.FlowRegistryClassPathPostProcessor" />
How to use it

In our project we have the following flow(s) defined in a seperate JAR:
/flows/module1/module1-flow.xml

And our pages are in the same directory:
/flows/module1/page1.xhtml
/flows/module1/page2.xhtml
etc…

In the flow we can now use the following view-id’s:

<?xml version="1.0" encoding="UTF-8"?>
<flow 	xmlns="http://www.springframework.org/schema/webflow"
		xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		xsi:schemaLocation="http://www.springframework.org/schema/webflow

http://www.springframework.org/schema/webflow/spring-webflow-2.0.xsd">

    <view-state id="start" view="page1.xhtml"/>

</flow>

Its also possible to have pages in other places, you can define the views as relative paths. For example:
“../../shared_pages/page2.xhtml” turns into “/shared_pages/page2.xhtml”
“../pages/page3.xhtml” turns into “/flows/pages/page3.xhtml”

The only problem we still have using this method is the deployment with RAD/Eclipse WTP and Facelets auto-refresh. For some reason after deploying our application locks the files in the bin-directory. Eclipse then can’t delete this directory and fails to publish. Ending in one big #fail. But a simple clean, clean, republish, clean, rebuild, restart, shout, scream, cry, rebuild and republish will solve this.

The big advantage is the fact that the flows and pages are now defined in their own JAR files, making releases and sharing classes (like shared services/shared menu’s etc) much easier.

Information on Spring forum

Tagged with:
 

3 Responses to Splitting up Spring Web Flow & Facelets into JARs

  1. Paul N says:

    Nice post.

    Does this mean that, with the absence of a org.springframework.faces.mvc.JsfView facelets view resolver, all view state attributes have to have a .xhtml suffix ?

    I’m used to a view resolver like this, that means you don’t have append the suffix in all your flow view names, as shown below.

  2. donny says:

    Any idea how to achieve this with JSF 2.0 + SWF 2.2? Thanks, donny

  3. Dhrubo says:

    I tried this with JSF2, SWF 2.3.1 — there seems to be some lifecycle issue while the post processor method is running

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title="" rel=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>