RESOLVED - issue SWF-363: Make the Flow Definition Registry Implementation Namespace Aware

http://opensource.atlassian.com/projects/spring/browse/SWF-363
Incomplete - task 2: Explore FlowExecutorArgumentHandler
This commit is contained in:
Ben Hale
2007-08-14 22:16:13 +00:00
parent 89b67d666e
commit 0e86da176a
13 changed files with 535 additions and 282 deletions

View File

@@ -30,6 +30,10 @@ Package org.springframework.webflow.core
* Fixed DefaultExpressionParserFactory to not require OGNL on the classpath if another expression parser
is configured (SWF-335).
Package org.springframework.webflow.definition.registry
* Added namespace awareness to the flow registry allowing flows with the same id to be placed in different namespaces
and be referenced by a 'flow path' (SWF-363).
Package org.springframework.webflow.engine
* Added invoke(String, Action) method to AbstractFlowBuilder.
* Added initBuilder() hook method to AbstractFlowBuilder.

View File

@@ -18,8 +18,8 @@ package org.springframework.webflow.definition.registry;
import org.springframework.webflow.definition.FlowDefinition;
/**
* A holder holding a reference to a Flow definition. Provides a layer of
* indirection, enabling things like "hot-reloadable" flow definitions.
* A holder holding a reference to a Flow definition. Provides a layer of indirection, enabling things like
* "hot-reloadable" flow definitions.
*
* @see FlowDefinitionRegistry#registerFlowDefinition(FlowDefinitionHolder)
*
@@ -28,27 +28,23 @@ import org.springframework.webflow.definition.FlowDefinition;
public interface FlowDefinitionHolder {
/**
* Returns the <code>id</code> of the flow definition held by this holder.
* This is a <i>lightweight</i> method callers may call to obtain the id of
* the flow without triggering full flow definition assembly (which may be
* an expensive operation).
* Returns the <code>id</code> of the flow definition held by this holder. This is a <i>lightweight</i> method
* callers may call to obtain the id of the flow without triggering full flow definition assembly (which may be an
* expensive operation).
*/
public String getFlowDefinitionId();
/**
* Returns the flow definition held by this holder. Calling this method the
* first time may trigger flow assembly (which may be expensive).
* @throws FlowDefinitionConstructionException if there is a problem constructing
* the target flow definition
* Returns the flow definition held by this holder. Calling this method the first time may trigger flow assembly
* (which may be expensive).
* @throws FlowDefinitionConstructionException if there is a problem constructing the target flow definition
*/
public FlowDefinition getFlowDefinition() throws FlowDefinitionConstructionException;
/**
* Refresh the flow definition held by this holder. Calling this method
* typically triggers flow reassembly, which may include a refresh from an
* externalized resource such as a file.
* @throws FlowDefinitionConstructionException if there is a problem constructing
* the target flow definition
* Refresh the flow definition held by this holder. Calling this method typically triggers flow re-assembly, which
* may include a refresh from an externalized resource such as a file.
* @throws FlowDefinitionConstructionException if there is a problem constructing the target flow definition
*/
public void refresh() throws FlowDefinitionConstructionException;
}

View File

@@ -18,11 +18,10 @@ package org.springframework.webflow.definition.registry;
import org.springframework.webflow.definition.FlowDefinition;
/**
* A runtime service locator interface for retrieving flow definitions by
* <code>id</code>.
* A runtime service locator interface for retrieving flow definitions by <code>id</code>.
* <p>
* Flow locators are needed by flow executors at runtime to retrieve
* fully-configured flow definitions to support launching new flow executions.
* Flow locators are needed by flow executors at runtime to retrieve fully-configured flow definitions to support
* launching new flow executions.
*
* @author Keith Donald
* @author Erwin Vervaet
@@ -30,14 +29,12 @@ import org.springframework.webflow.definition.FlowDefinition;
public interface FlowDefinitionLocator {
/**
* Lookup the flow definition with the specified <code>id</code>.
* @param id the flow definition id
* Lookup the flow definition with the specified <code>path</code>.
* @param flowPath the flow definition path
* @return the flow definition
* @throws NoSuchFlowDefinitionException when the flow definition with the
* specified id does not exist
* @throws FlowDefinitionConstructionException if there is a problem constructing
* the identified flow definition
* @throws NoSuchFlowDefinitionException when the flow definition with the specified id does not exist
* @throws FlowDefinitionConstructionException if there is a problem constructing the identified flow definition
*/
public FlowDefinition getFlowDefinition(String id)
throws NoSuchFlowDefinitionException, FlowDefinitionConstructionException;
public FlowDefinition getFlowDefinition(String flowPath) throws NoSuchFlowDefinitionException,
FlowDefinitionConstructionException;
}

View File

@@ -15,7 +15,6 @@
*/
package org.springframework.webflow.definition.registry;
import org.springframework.webflow.definition.FlowDefinition;
/**
* A container of flow definitions. Extends the {@link FlowDefinitionRegistryMBean} management interface exposing
@@ -26,6 +25,7 @@ import org.springframework.webflow.definition.FlowDefinition;
* registry hierarchy.
*
* @author Keith Donald
* @author Ben Hale
*/
public interface FlowDefinitionRegistry extends FlowDefinitionLocator, FlowDefinitionRegistryMBean {
@@ -36,21 +36,21 @@ public interface FlowDefinitionRegistry extends FlowDefinitionLocator, FlowDefin
*/
public void setParent(FlowDefinitionRegistry parent);
/**
* Return all flow definitions registered in this registry. Note that this will trigger flow assembly for all
* registered flow definitions (which may be expensive).
* @return the flow definitions
* @throws FlowDefinitionConstructionException if there is a problem constructing one of the registered flow
* definitions
*/
public FlowDefinition[] getFlowDefinitions() throws FlowDefinitionConstructionException;
/**
* Register a flow definition in this registry. Registers a "holder", not the Flow definition itself. This allows
* the actual Flow definition to be loaded lazily only when needed, and also rebuilt at runtime when its underlying
* resource changes without redeploy.
* resource changes without re-deploy.
* @param flowHolder a holder holding the flow definition to register
*/
public void registerFlowDefinition(FlowDefinitionHolder flowHolder);
/**
* Register a flow definition in this registry under a specific namespace. Registers a "holder", not the Flow
* definition itself. This allows the actual Flow definition to be loaded lazily only when needed, and also rebuilt
* at runtime when its underlying resource changes without re-deploy.
* @param flowHolder a holder holding the flow definition to register
* @param namespace the namespace to register the flow definition in
*/
public void registerFlowDefinition(FlowDefinitionHolder flowHolder, String namespace);
}

View File

@@ -17,6 +17,7 @@ package org.springframework.webflow.definition.registry;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
@@ -34,200 +35,192 @@ import org.springframework.webflow.definition.FlowDefinition;
* definitions that no longer exist.
*
* @author Keith Donald
* @author Ben Hale
*/
public class FlowDefinitionRegistryImpl implements FlowDefinitionRegistry {
private static final Log logger = LogFactory.getLog(FlowDefinitionRegistryImpl.class);
private static final Log logger = LogFactory.getLog(FlowDefinitionRegistryImpl.class);
/**
* The map of loaded Flow definitions maintained in this registry.
*/
private Map flowDefinitions;
/**
* The map of loaded Flow definitions maintained in this registry.
*/
private Map flowDefinitions;
/**
* An optional parent flow definition registry.
*/
private FlowDefinitionRegistry parent;
/**
* An optional parent flow definition registry.
*/
private FlowDefinitionRegistry parent;
public FlowDefinitionRegistryImpl() {
flowDefinitions = new TreeMap();
}
// implementing FlowDefinitionRegistryMBean
public String[] getFlowDefinitionIds() {
return (String[]) flowDefinitions.keySet().toArray(new String[flowDefinitions.size()]);
}
public int getFlowDefinitionCount() {
return flowDefinitions.size();
}
public boolean containsFlowDefinition(String id) {
Assert.hasText(id, "The flow id is required");
return flowDefinitions.get(id) != null;
}
public void refresh() throws FlowDefinitionConstructionException {
if (logger.isDebugEnabled()) {
logger.debug("Refreshing flow definition registry '" + this + "'");
public FlowDefinitionRegistryImpl() {
flowDefinitions = new TreeMap();
}
ClassLoader loader = Thread.currentThread().getContextClassLoader();
try {
// workaround for JMX
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
LinkedList needsReindexing = new LinkedList();
Iterator it = flowDefinitions.entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String key = (String) entry.getKey();
FlowDefinitionHolder holder = (FlowDefinitionHolder) entry.getValue();
holder.refresh();
if (!holder.getFlowDefinitionId().equals(key)) {
needsReindexing.add(new Indexed(key, holder));
// implementing FlowDefinitionRegistryMBean
public String[] getFlowDefinitionPaths() {
List flowPaths = new LinkedList();
for (Iterator namespaces = flowDefinitions.entrySet().iterator(); namespaces.hasNext();) {
Map.Entry namespaceEntry = (Map.Entry) namespaces.next();
String namespaceName = (String) namespaceEntry.getKey();
Map namespace = (Map) namespaceEntry.getValue();
for (Iterator ids = namespace.keySet().iterator(); ids.hasNext();) {
flowPaths.add(FlowPathUtils.buildFlowPath(namespaceName, (String) ids.next()));
}
}
}
it = needsReindexing.iterator();
while (it.hasNext()) {
Indexed indexed = (Indexed) it.next();
reindex(indexed.holder, indexed.key);
}
} finally {
Thread.currentThread().setContextClassLoader(loader);
return (String[]) flowPaths.toArray(new String[flowPaths.size()]);
}
}
public void refresh(String flowId) throws NoSuchFlowDefinitionException, FlowDefinitionConstructionException {
if (logger.isDebugEnabled()) {
logger.debug("Refreshing flow with id '" + flowId + "'");
public int getFlowDefinitionCount() {
int count = 0;
for (Iterator namespaces = flowDefinitions.values().iterator(); namespaces.hasNext();) {
Map namespace = (Map) namespaces.next();
count += namespace.size();
}
return count;
}
ClassLoader loader = Thread.currentThread().getContextClassLoader();
try {
// workaround for JMX
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
FlowDefinitionHolder holder = getFlowDefinitionHolder(flowId);
holder.refresh();
if (!holder.getFlowDefinitionId().equals(flowId)) {
reindex(holder, flowId);
}
} finally {
Thread.currentThread().setContextClassLoader(loader);
public boolean containsFlowDefinition(String flowPath) {
Assert.hasText(flowPath, "The flow path is required");
Map namespace = getNamespace(FlowPathUtils.extractFlowNamespace(flowPath));
return namespace.containsKey(FlowPathUtils.extractFlowId(flowPath));
}
}
// implementing FlowDefinitionLocator
public FlowDefinition getFlowDefinition(String id) throws NoSuchFlowDefinitionException,
FlowDefinitionConstructionException {
Assert.hasText(id,
"Unable to load a flow definition: no flow id was provided. Please provide a valid flow identifier.");
if (logger.isDebugEnabled()) {
logger.debug("Getting flow definition with id '" + id + "'");
public void refresh() throws FlowDefinitionConstructionException {
if (logger.isDebugEnabled()) {
logger.debug("Refreshing flow definition registry '" + this + "'");
}
for (Iterator namespaces = flowDefinitions.entrySet().iterator(); namespaces.hasNext();) {
Map.Entry namespaceEntry = (Map.Entry) namespaces.next();
String namespaceName = (String) namespaceEntry.getKey();
Map namespace = (Map) namespaceEntry.getValue();
for (Iterator ids = namespace.keySet().iterator(); ids.hasNext();) {
refresh(FlowPathUtils.buildFlowPath(namespaceName, (String) ids.next()));
}
}
}
try {
return getFlowDefinitionHolder(id).getFlowDefinition();
} catch (NoSuchFlowDefinitionException e) {
if (parent != null) {
// try parent
return parent.getFlowDefinition(id);
}
throw e;
public void refresh(String flowPath) throws NoSuchFlowDefinitionException, FlowDefinitionConstructionException {
if (logger.isDebugEnabled()) {
logger.debug("Refreshing flow with path '" + flowPath + "'");
}
ClassLoader loader = Thread.currentThread().getContextClassLoader();
try {
// workaround for JMX
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
FlowDefinitionHolder holder = getFlowDefinitionHolder(flowPath);
holder.refresh();
if (!holder.getFlowDefinitionId().equals(FlowPathUtils.extractFlowId(flowPath))) {
reindex(holder, FlowPathUtils.extractFlowNamespace(flowPath), flowPath);
}
} finally {
Thread.currentThread().setContextClassLoader(loader);
}
}
}
// implementing FlowDefinitionRegistry
// implementing FlowDefinitionLocator
public void setParent(FlowDefinitionRegistry parent) {
if (logger.isDebugEnabled()) {
logger.debug("Setting parent flow definition registry to '" + parent + "'");
public FlowDefinition getFlowDefinition(String path) throws NoSuchFlowDefinitionException,
FlowDefinitionConstructionException {
Assert.hasText(path,
"Unable to load a flow definition: no flow path was provided. Please provide a valid flow path.");
if (logger.isDebugEnabled()) {
logger.debug("Getting flow definition with path '" + path + "'");
}
try {
return getFlowDefinitionHolder(path).getFlowDefinition();
} catch (NoSuchFlowDefinitionException e) {
if (parent != null) {
// try parent
return parent.getFlowDefinition(path);
}
throw e;
}
}
this.parent = parent;
}
public FlowDefinition[] getFlowDefinitions() throws FlowDefinitionConstructionException {
FlowDefinition[] flows = new FlowDefinition[flowDefinitions.size()];
Iterator it = flowDefinitions.values().iterator();
int i = 0;
while (it.hasNext()) {
FlowDefinitionHolder holder = (FlowDefinitionHolder) it.next();
flows[i] = holder.getFlowDefinition();
i++;
// implementing FlowDefinitionRegistry
public void setParent(FlowDefinitionRegistry parent) {
if (logger.isDebugEnabled()) {
logger.debug("Setting parent flow definition registry to '" + parent + "'");
}
this.parent = parent;
}
return flows;
}
public void registerFlowDefinition(FlowDefinitionHolder flowHolder) {
Assert.notNull(flowHolder, "The flow definition holder to register is required");
if (logger.isDebugEnabled()) {
logger.debug("Registering flow definition with id '" + flowHolder.getFlowDefinitionId() + "'");
public void registerFlowDefinition(FlowDefinitionHolder flowHolder) {
registerFlowDefinition(flowHolder, "");
}
index(flowHolder);
}
/**
* Remove identified flow definition from this registry. If the given id is not known in this registry, nothing will
* happen.
* @param id the flow definition id
*/
public void removeFlowDefinition(String id) {
Assert.hasText(id, "The flow id is required");
if (logger.isDebugEnabled()) {
logger.debug("Removing flow definition with id '" + id + "'");
public void registerFlowDefinition(FlowDefinitionHolder flowHolder, String namespace) {
Assert.notNull(flowHolder, "The flow definition holder to register is required");
Assert.notNull(namespace, "The flow namespace is required");
if (logger.isDebugEnabled()) {
logger.debug("Registering flow definition with id '" + flowHolder.getFlowDefinitionId()
+ "' in namespace '" + namespace + "'");
}
index(flowHolder, namespace);
}
flowDefinitions.remove(id);
}
// internal helpers
/**
* Reindex given flow definition.
* @param holder the holder holding the flow definition to reindex
* @param oldId the id that was previously assigned to given flow definition
*/
private void reindex(FlowDefinitionHolder holder, String oldId) {
flowDefinitions.remove(oldId);
index(holder);
}
/**
* Index given flow definition.
* @param holder the holder holding the flow definition to index
*/
private void index(FlowDefinitionHolder holder) {
Assert.hasText(holder.getFlowDefinitionId(), "The flow holder to index must return a non-blank flow id");
flowDefinitions.put(holder.getFlowDefinitionId(), holder);
}
/**
* Returns the identified flow definition holder. Throws an exception if it cannot be found.
*/
private FlowDefinitionHolder getFlowDefinitionHolder(String id) throws NoSuchFlowDefinitionException {
FlowDefinitionHolder flowHolder = (FlowDefinitionHolder) flowDefinitions.get(id);
if (flowHolder == null) {
throw new NoSuchFlowDefinitionException(id, getFlowDefinitionIds());
/**
* Remove identified flow definition from this registry. If the given id is not known in this registry, nothing will
* happen.
* @param flowPath the flow definition path
*/
public void removeFlowDefinition(String flowPath) {
Assert.hasText(flowPath, "The flow path is required");
if (logger.isDebugEnabled()) {
logger.debug("Removing flow definition with path '" + flowPath + "'");
}
Map namespace = getNamespace(FlowPathUtils.extractFlowNamespace(flowPath));
namespace.remove(FlowPathUtils.extractFlowId(flowPath));
}
return flowHolder;
}
/**
* Simple value object that holds the key for an indexed flow definition holder in this registry. Used to support
* reindexing on a refresh.
*
* @author Keith Donald
*/
private static class Indexed {
// internal helpers
private String key;
private FlowDefinitionHolder holder;
public Indexed(String key, FlowDefinitionHolder holder) {
this.key = key;
this.holder = holder;
/**
* Re-index given flow definition.
* @param holder the holder holding the flow definition to re-index
* @param namespace the namespace to index the new flow in
* @param oldFlowPath the flowPath that was previously assigned to given flow definition
*/
private void reindex(FlowDefinitionHolder holder, String namespace, String oldFlowPath) {
removeFlowDefinition(oldFlowPath);
index(holder, namespace);
}
}
public String toString() {
return new ToStringCreator(this).append("flowDefinitions", flowDefinitions).append("parent", parent).toString();
}
/**
* Index given flow definition.
* @param holder the holder holding the flow definition to index
* @param namespaceName the namespace to index the flow definition in
*/
private void index(FlowDefinitionHolder holder, String namespaceName) {
Assert.hasText(holder.getFlowDefinitionId(), "The flow holder to index must return a non-blank flow id");
Map namespace = getNamespace(namespaceName);
namespace.put(holder.getFlowDefinitionId(), holder);
}
/**
* Returns the identified flow definition holder. Throws an exception if it cannot be found.
*/
private FlowDefinitionHolder getFlowDefinitionHolder(String flowPath) throws NoSuchFlowDefinitionException {
Map namespace = getNamespace(FlowPathUtils.extractFlowNamespace(flowPath));
FlowDefinitionHolder flowHolder = (FlowDefinitionHolder) namespace.get(FlowPathUtils.extractFlowId(flowPath));
if (flowHolder == null) {
throw new NoSuchFlowDefinitionException(flowPath, getFlowDefinitionPaths());
}
return flowHolder;
}
/**
* Returns the namespace map for a given namespace. Creates the map if it does not exist.
*/
private Map getNamespace(String namespace) {
if (!flowDefinitions.containsKey(namespace)) {
flowDefinitions.put(namespace, new TreeMap());
}
return (Map) flowDefinitions.get(namespace);
}
public String toString() {
return new ToStringCreator(this).append("flowDefinitions", flowDefinitions).append("parent", parent).toString();
}
}

View File

@@ -16,13 +16,12 @@
package org.springframework.webflow.definition.registry;
/**
* A management interface for managing flow definition registries at runtime.
* Provides the ability to query the size and state of the registry, as well as
* refresh registered flow definitions at runtime.
* A management interface for managing flow definition registries at runtime. Provides the ability to query the size and
* state of the registry, as well as refresh registered flow definitions at runtime.
* <p>
* Flow registries that implement this interface may be exposed for management
* over the JMX protocol. The following is an example of using Spring's JMX
* <code>MBeanExporter</code> to export a flow registry to an MBeanServer:
* Flow registries that implement this interface may be exposed for management over the JMX protocol. The following is
* an example of using Spring's JMX <code>MBeanExporter</code> to export a flow registry to an MBeanServer:
*
* <pre class="code">
* &lt;!-- Creates the registry of flow definitions for this application --&gt;
* &lt;bean name=&quot;flowRegistry&quot; class=&quot;org.springframework.webflow...XmlFlowRegistryFactoryBean&quot;&gt;
@@ -44,18 +43,19 @@ package org.springframework.webflow.definition.registry;
* &lt;/property&gt;
* &lt;/bean&gt;
* </pre>
* With the above configuration, you may then use any JMX client (such as Sun's
* jConsole which ships with JDK 1.5) to refresh flow definitions at runtime.
*
* With the above configuration, you may then use any JMX client (such as Sun's jConsole which ships with JDK 1.5) to
* refresh flow definitions at runtime.
*
* @author Keith Donald
*/
public interface FlowDefinitionRegistryMBean {
/**
* Returns the ids of the flow definitions registered in this registry.
* @return the flow definition ids
* Returns the paths of the flow definitions registered in this registry.
* @return the flow definition paths
*/
public String[] getFlowDefinitionIds();
public String[] getFlowDefinitionPaths();
/**
* Return the number of flow definitions registered in this registry.
@@ -64,28 +64,23 @@ public interface FlowDefinitionRegistryMBean {
public int getFlowDefinitionCount();
/**
* Queries this registry to determine if a specific flow is contained within
* it.
* @param id the flow definition id
* @return true if a flow definition is contained in this registry with the
* id provided
* Queries this registry to determine if a specific flow is contained within it.
* @param flowPath the flow definition path
* @return true if a flow definition is contained in this registry with the id provided
*/
public boolean containsFlowDefinition(String id);
public boolean containsFlowDefinition(String flowPath);
/**
* Refresh this flow definition registry, reloading all Flow definitions
* from their externalized representations.
* Refresh this flow definition registry, reloading all Flow definitions from their externalized representations.
*/
public void refresh() throws FlowDefinitionConstructionException;
/**
* Refresh the Flow definition in this registry with the <code>id</code>
* provided, reloading it from it's externalized representation.
* @param flowDefinitionId the id of the flow definition to refresh
* @throws NoSuchFlowDefinitionException if a flow with the id provided is not
* stored in this registry
* Refresh the Flow definition in this registry with the <code>path</code> provided, reloading it from it's
* externalized representation.
* @param flowPath the path of the flow definition to refresh
* @throws NoSuchFlowDefinitionException if a flow with the id provided is not stored in this registry
*/
public void refresh(String flowDefinitionId)
throws NoSuchFlowDefinitionException, FlowDefinitionConstructionException;
public void refresh(String flowPath) throws NoSuchFlowDefinitionException, FlowDefinitionConstructionException;
}

View File

@@ -0,0 +1,55 @@
package org.springframework.webflow.definition.registry;
import org.springframework.util.Assert;
/**
* Simple utility for working with flow paths. Only intended for internal use.
*
* @author Ben Hale
*/
class FlowPathUtils {
private static final String PATH_DELIMITER = "/";
/**
* Parses a flow path and returns the namespace part of the path.
* @param flowPath The path to parse
* @return The namespace part of the path
*/
public static String extractFlowNamespace(String flowPath) {
Assert.hasText(flowPath, "The flow path must not be empty or null");
int index = flowPath.lastIndexOf(PATH_DELIMITER);
if (index == -1) {
return "";
} else {
return flowPath.substring(0, index);
}
}
/**
* Parses a flow path and returns the id part of the path.
* @param flowPath The path to parse
* @return The id part of the path
*/
public static String extractFlowId(String flowPath) {
Assert.hasText(flowPath, "The flow path must not be empty or null");
int index = flowPath.lastIndexOf(PATH_DELIMITER);
if (index == -1) {
return flowPath;
} else {
return flowPath.substring(index + 1);
}
}
/**
* Creates a flow path based on given namespace and id.
* @param namespace The namespace of the path
* @param id The id of the path
* @return The complete flow path
*/
public static String buildFlowPath(String namespace, String id) {
Assert.notNull(namespace, "namespace must have some value");
Assert.hasText(id, "The id must not be empty or null");
return namespace + PATH_DELIMITER + id;
}
}

View File

@@ -18,8 +18,7 @@ package org.springframework.webflow.definition.registry;
import org.springframework.webflow.definition.FlowDefinition;
/**
* A simple flow definition holder that just holds a constant singleton
* reference to a flow definition.
* A simple flow definition holder that just holds a constant singleton reference to a flow definition.
*
* @author Keith Donald
*/

View File

@@ -27,14 +27,13 @@ import org.springframework.webflow.engine.Flow;
import org.springframework.webflow.util.ResourceHolder;
/**
* A flow definition holder that can detect changes on an underlying flow
* definition resource and refresh that resource automatically.
* A flow definition holder that can detect changes on an underlying flow definition resource and refresh that resource
* automatically.
* <p>
* This class is threadsafe.
* This class is thread-safe.
* <p>
* Note that this {@link FlowDefinition} holder uses a {@link Flow} assembler.
* This is normal since a {@link Flow} is a {@link FlowDefinition}! This class
* bridges the <i>abstract</i> world of {@link FlowDefinition flow definitions}
* Note that this {@link FlowDefinition} holder uses a {@link Flow} assembler. This is normal since a {@link Flow} is a
* {@link FlowDefinition}! This class bridges the <i>abstract</i> world of {@link FlowDefinition flow definitions}
* with the <i>concrete</i> world of {@link Flow flow implementations}.
*
* @see FlowDefinition
@@ -44,7 +43,7 @@ import org.springframework.webflow.util.ResourceHolder;
* @author Keith Donald
*/
public class RefreshableFlowDefinitionHolder implements FlowDefinitionHolder {
private static final Log logger = LogFactory.getLog(RefreshableFlowDefinitionHolder.class);
/**
@@ -58,21 +57,19 @@ public class RefreshableFlowDefinitionHolder implements FlowDefinitionHolder {
private FlowAssembler assembler;
/**
* A last modified date for the backing flow definition resource, used to support
* automatic reassembly on resource change.
* A last modified date for the backing flow definition resource, used to support automatic reassembly on resource
* change.
*/
private long lastModified;
/**
* A flag indicating whether or not this holder is in the middle of the
* assembly process.
* A flag indicating whether or not this holder is in the middle of the assembly process.
*/
private boolean assembling;
/**
* Creates a new refreshable flow definition holder that uses the configured
* assembler (GOF director) to drive flow assembly, on initial use and on any
* resource change or refresh.
* Creates a new refreshable flow definition holder that uses the configured assembler (GOF director) to drive flow
* assembly, on initial use and on any resource change or refresh.
* @param assembler the flow assembler to use
*/
public RefreshableFlowDefinitionHolder(FlowAssembler assembler) {
@@ -91,8 +88,7 @@ public class RefreshableFlowDefinitionHolder implements FlowDefinitionHolder {
if (!isAssembled()) {
lastModified = calculateLastModified();
assembleFlow();
}
else {
} else {
refreshIfChanged();
}
return flowDefinition;
@@ -101,7 +97,7 @@ public class RefreshableFlowDefinitionHolder implements FlowDefinitionHolder {
public synchronized void refresh() throws FlowBuilderException {
assembleFlow();
}
// internal helpers
/**
@@ -122,8 +118,8 @@ public class RefreshableFlowDefinitionHolder implements FlowDefinitionHolder {
long calculatedLastModified = calculateLastModified();
if (this.lastModified < calculatedLastModified) {
if (logger.isDebugEnabled()) {
logger.debug("Resource modification detected, reloading flow definition with id '" +
assembler.getFlowId() + "'");
logger.debug("Resource modification detected, reloading flow definition with id '"
+ assembler.getFlowId() + "'");
}
assembleFlow();
this.lastModified = calculatedLastModified;
@@ -131,21 +127,18 @@ public class RefreshableFlowDefinitionHolder implements FlowDefinitionHolder {
}
/**
* Helper that retrieves the last modified date by querying the backing flow
* resource.
* Helper that retrieves the last modified date by querying the backing flow resource.
* @return the last modified date, or -1 if it could not be retrieved
*/
protected long calculateLastModified() {
if (getFlowBuilder() instanceof ResourceHolder) {
Resource resource = ((ResourceHolder)getFlowBuilder()).getResource();
Resource resource = ((ResourceHolder) getFlowBuilder()).getResource();
if (logger.isDebugEnabled()) {
logger.debug(
"Calculating last modified timestamp for flow definition resource '" + resource + "'");
logger.debug("Calculating last modified timestamp for flow definition resource '" + resource + "'");
}
try {
return resource.getFile().lastModified();
}
catch (IOException e) {
} catch (IOException e) {
// ignore, last modified checks not supported
}
}
@@ -153,7 +146,7 @@ public class RefreshableFlowDefinitionHolder implements FlowDefinitionHolder {
}
/**
* Returns the last modifed date of the backed flow definition resource.
* Returns the last modified date of the backed flow definition resource.
* @return the last modified date
*/
protected long getLastModified() {
@@ -161,8 +154,7 @@ public class RefreshableFlowDefinitionHolder implements FlowDefinitionHolder {
}
/**
* Assemble the held flow definition, delegating to the configured
* FlowAssembler (director).
* Assemble the held flow definition, delegating to the configured FlowAssembler (director).
*/
protected void assembleFlow() throws FlowBuilderException {
if (logger.isDebugEnabled()) {
@@ -171,15 +163,13 @@ public class RefreshableFlowDefinitionHolder implements FlowDefinitionHolder {
try {
assembling = true;
flowDefinition = assembler.assembleFlow();
}
finally {
} finally {
assembling = false;
}
}
/**
* Returns a flag indicating if this holder has performed and completed
* flow definition assembly.
* Returns a flag indicating if this holder has performed and completed flow definition assembly.
*/
protected boolean isAssembled() {
return flowDefinition != null;

View File

@@ -25,27 +25,36 @@ import org.springframework.webflow.definition.StateDefinition;
* Unit tests for {@link FlowDefinitionRegistryImpl}.
*/
public class FlowDefinitionRegistryImplTests extends TestCase {
private FlowDefinitionRegistryImpl registry = new FlowDefinitionRegistryImpl();
private FlowDefinition fooFlow;
private FlowDefinition barFlow;
protected void setUp() {
fooFlow = new FooFlow();
barFlow = new BarFlow();
}
public void testEmptyRegistryAsserts() {
assertEquals(0, registry.getFlowDefinitionCount());
assertEquals(0, registry.getFlowDefinitionIds().length);
assertEquals(0, registry.getFlowDefinitions().length);
assertEquals(0, registry.getFlowDefinitionPaths().length);
}
public void testNoSuchFlowDefinition() {
try {
registry.getFlowDefinition("bogus");
fail("Should've bombed with NoSuchFlow");
} catch (NoSuchFlowDefinitionException e) {
}
catch (NoSuchFlowDefinitionException e) {
}
public void testNoSuchFlowDefinitionWithNamespace() {
try {
registry.getFlowDefinition("/namespace/bogus");
fail("Should've bombed with NoSuchFlow");
} catch (NoSuchFlowDefinitionException e) {
}
}
@@ -53,11 +62,17 @@ public class FlowDefinitionRegistryImplTests extends TestCase {
public void testRegisterFlow() {
registry.registerFlowDefinition(new StaticFlowDefinitionHolder(fooFlow));
assertEquals(1, registry.getFlowDefinitionCount());
assertEquals("foo", registry.getFlowDefinitionIds()[0]);
assertEquals("foo", registry.getFlowDefinitions()[0].getId());
assertEquals("/foo", registry.getFlowDefinitionPaths()[0]);
assertEquals("foo", registry.getFlowDefinition("foo").getId());
}
public void testRegisterFlowWithNamespace() {
registry.registerFlowDefinition(new StaticFlowDefinitionHolder(barFlow), "/namespace");
assertEquals(1, registry.getFlowDefinitionCount());
assertEquals("/namespace/bar", registry.getFlowDefinitionPaths()[0]);
assertEquals("bar", registry.getFlowDefinition("/namespace/bar").getId());
}
public void testRegisterFlowSameIds() {
registry.registerFlowDefinition(new StaticFlowDefinitionHolder(fooFlow));
FooFlow newFlow = new FooFlow();
@@ -66,6 +81,14 @@ public class FlowDefinitionRegistryImplTests extends TestCase {
assertSame(newFlow, registry.getFlowDefinition("foo"));
}
public void testRegisterFlowSameIdsWithNamespace() {
registry.registerFlowDefinition(new StaticFlowDefinitionHolder(barFlow), "/namespace");
BarFlow newFlow = new BarFlow();
registry.registerFlowDefinition(new StaticFlowDefinitionHolder(newFlow), "/namespace");
assertEquals(1, registry.getFlowDefinitionCount());
assertSame(newFlow, registry.getFlowDefinition("/namespace/bar"));
}
public void testRegisterMultipleFlows() {
registry.registerFlowDefinition(new StaticFlowDefinitionHolder(fooFlow));
FooFlow newFlow = new FooFlow();
@@ -76,13 +99,30 @@ public class FlowDefinitionRegistryImplTests extends TestCase {
assertSame(newFlow, registry.getFlowDefinition("bar"));
}
public void testRegisterMultipleFlowsWithNamespace() {
registry.registerFlowDefinition(new StaticFlowDefinitionHolder(barFlow), "/namespace");
BarFlow newFlow = new BarFlow();
newFlow.id = "foo";
registry.registerFlowDefinition(new StaticFlowDefinitionHolder(newFlow), "/namespace");
assertEquals(2, registry.getFlowDefinitionCount());
assertSame(barFlow, registry.getFlowDefinition("/namespace/bar"));
assertSame(newFlow, registry.getFlowDefinition("/namespace/foo"));
}
public void testRefresh() {
testRegisterMultipleFlows();
registry.refresh();
assertEquals(2, registry.getFlowDefinitionCount());
assertSame(fooFlow, registry.getFlowDefinition("foo"));
}
public void testRefreshWithNamespace() {
testRegisterMultipleFlowsWithNamespace();
registry.refresh();
assertEquals(2, registry.getFlowDefinitionCount());
assertSame(barFlow, registry.getFlowDefinition("/namespace/bar"));
}
public void testRefreshValidFlow() {
testRegisterMultipleFlows();
registry.refresh("foo");
@@ -90,17 +130,33 @@ public class FlowDefinitionRegistryImplTests extends TestCase {
assertSame(fooFlow, registry.getFlowDefinition("foo"));
}
public void testRefreshValidFlowWithNamespace() {
testRegisterMultipleFlowsWithNamespace();
registry.refresh("/namespace/bar");
assertEquals(2, registry.getFlowDefinitionCount());
assertSame(barFlow, registry.getFlowDefinition("/namespace/bar"));
}
public void testRefreshNoSuchFlow() {
testRegisterMultipleFlows();
try {
registry.refresh("bogus");
fail("Should've bombed with NoSuchFlow");
}
catch (NoSuchFlowDefinitionException e) {
} catch (NoSuchFlowDefinitionException e) {
}
}
public void testRefreshNoSuchFlowWithNamespace() {
testRegisterMultipleFlowsWithNamespace();
try {
registry.refresh("/namespace/bogus");
fail("Should've bombed with NoSuchFlow");
} catch (NoSuchFlowDefinitionException e) {
}
}
public void testParentHierarchy() {
testRegisterMultipleFlows();
FlowDefinitionRegistryImpl child = new FlowDefinitionRegistryImpl();
@@ -111,9 +167,47 @@ public class FlowDefinitionRegistryImplTests extends TestCase {
assertEquals("bar", child.getFlowDefinition("bar").getId());
}
public void testParentHierarchyWithNamespace() {
testRegisterMultipleFlowsWithNamespace();
FlowDefinitionRegistryImpl child = new FlowDefinitionRegistryImpl();
child.setParent(registry);
BarFlow barFlow = new BarFlow();
child.registerFlowDefinition(new StaticFlowDefinitionHolder(barFlow), "/namespace");
assertSame(barFlow, child.getFlowDefinition("/namespace/bar"));
assertEquals("bar", child.getFlowDefinition("/namespace/bar").getId());
}
private static class FooFlow implements FlowDefinition {
private String id = "foo";
public AttributeMap getAttributes() {
return null;
}
public String getCaption() {
return null;
}
public String getDescription() {
return null;
}
public String getId() {
return id;
}
public StateDefinition getStartState() {
return null;
}
public StateDefinition getState(String id) throws IllegalArgumentException {
return null;
}
}
private static class BarFlow implements FlowDefinition {
private String id = "bar";
public AttributeMap getAttributes() {
return null;
}

View File

@@ -0,0 +1,130 @@
package org.springframework.webflow.definition.registry;
import junit.framework.TestCase;
public class FlowPathUtilsTests extends TestCase {
public void testNamespaceWithNoSlash() {
assertEquals("Incorrect namespace", "", FlowPathUtils.extractFlowNamespace("flow"));
}
public void testNamespaceWithSlash() {
assertEquals("Incorrect namespace", "", FlowPathUtils.extractFlowNamespace("/flow"));
}
public void testNamespaceWithNamespace() {
assertEquals("Incorrect namespace", "/namespace", FlowPathUtils.extractFlowNamespace("/namespace/flow"));
}
public void teswtNamespaceWithComplexNamespace() {
assertEquals("Incorrect namespace", "/complex/namespace", FlowPathUtils.extractFlowNamespace("/complex/namespace/flow"));
}
public void testNamespaceEmpty() {
try {
FlowPathUtils.extractFlowNamespace("");
fail("Should have detected empty input");
} catch (IllegalArgumentException e) {
}
}
public void testNamespaceWhitespace() {
try {
FlowPathUtils.extractFlowNamespace(" ");
fail("Should have detected empty input");
} catch (IllegalArgumentException e) {
}
}
public void testNamespaceNull() {
try {
FlowPathUtils.extractFlowNamespace(null);
fail("Should have detected empty input");
} catch (IllegalArgumentException e) {
}
}
public void testIdWithNoSlash() {
assertEquals("Incorrect id", "flow", FlowPathUtils.extractFlowId("flow"));
}
public void testIdWithSlash() {
assertEquals("Incorrect id", "flow", FlowPathUtils.extractFlowId("/flow"));
}
public void testIdWithNamespace() {
assertEquals("Incorrect id", "flow", FlowPathUtils.extractFlowId("/namespace/flow"));
}
public void testIdWithComplexNamespace() {
assertEquals("Incorrect id", "flow", FlowPathUtils.extractFlowId("/complex/namespace/flow"));
}
public void testIdEmpty() {
try {
FlowPathUtils.extractFlowId("");
fail("Should have detected empty input");
} catch (IllegalArgumentException e) {
}
}
public void testIdWhitespace() {
try {
FlowPathUtils.extractFlowId(" ");
fail("Should have detected empty input");
} catch (IllegalArgumentException e) {
}
}
public void testIdNull() {
try {
FlowPathUtils.extractFlowId(null);
fail("Should have detected empty input");
} catch (IllegalArgumentException e) {
}
}
public void testPathWithEmptyNamespace() {
assertEquals("Incorrect path", "/flow", FlowPathUtils.buildFlowPath("", "flow"));
}
public void testPathWithNamespace() {
assertEquals("Incorrect path", "/namespace/flow", FlowPathUtils.buildFlowPath("/namespace", "flow"));
}
public void testPathWithComplexNamespace() {
assertEquals("Incorrect path", "/complex/namespace/flow", FlowPathUtils.buildFlowPath("/complex/namespace", "flow"));
}
public void testPathWithNullNamespace() {
try {
FlowPathUtils.buildFlowPath(null, "flow");
fail("Should have detected empty input");
} catch (IllegalArgumentException e) {
}
}
public void testPathWithEmptyId() {
try {
FlowPathUtils.buildFlowPath("", "");
fail("Should have detected empty input");
} catch (IllegalArgumentException e) {
}
}
public void testPathWithWhitespaceId() {
try {
FlowPathUtils.buildFlowPath("", " ");
fail("Should have detected empty input");
} catch (IllegalArgumentException e) {
}
}
public void testPathWithNullId() {
try {
FlowPathUtils.buildFlowPath("", null);
fail("Should have detected empty input");
} catch (IllegalArgumentException e) {
}
}
}

View File

@@ -38,7 +38,7 @@ public class XmlFlowRegistrarTests extends TestCase {
registrar.addLocation(new ClassPathResource("flow.xml", getClass()));
registrar.registerFlowDefinitions(registry);
assertEquals(1, registry.getFlowDefinitionCount());
assertEquals("flow", registry.getFlowDefinitions()[0].getId());
assertEquals("flow", registry.getFlowDefinition("flow").getId());
}
public void testAddResource() {
@@ -46,6 +46,6 @@ public class XmlFlowRegistrarTests extends TestCase {
registrar.addResource(new FlowDefinitionResource("foo", new ClassPathResource("flow.xml", getClass())));
registrar.registerFlowDefinitions(registry);
assertEquals(1, registry.getFlowDefinitionCount());
assertEquals("foo", registry.getFlowDefinitions()[0].getId());
assertEquals("foo", registry.getFlowDefinition("foo").getId());
}
}

View File

@@ -31,9 +31,9 @@ public class XmlFlowRegistryFactoryBeanTests extends TestCase {
factoryBean.setFlowLocations(locations);
factoryBean.setBeanFactory(new StaticListableBeanFactory());
factoryBean.afterPropertiesSet();
FlowDefinitionRegistry registry = (FlowDefinitionRegistry)factoryBean.getObject();
FlowDefinitionRegistry registry = (FlowDefinitionRegistry) factoryBean.getObject();
assertEquals(1, registry.getFlowDefinitionCount());
assertEquals("flow", registry.getFlowDefinitions()[0].getId());
assertEquals("flow", registry.getFlowDefinition("flow").getId());
}
public void testCreateFromDefinitions() throws Exception {
@@ -42,8 +42,8 @@ public class XmlFlowRegistryFactoryBeanTests extends TestCase {
factoryBean.setFlowDefinitions(properties);
factoryBean.setBeanFactory(new StaticListableBeanFactory());
factoryBean.afterPropertiesSet();
FlowDefinitionRegistry registry = (FlowDefinitionRegistry)factoryBean.getObject();
FlowDefinitionRegistry registry = (FlowDefinitionRegistry) factoryBean.getObject();
assertEquals(1, registry.getFlowDefinitionCount());
assertEquals("foo", registry.getFlowDefinitions()[0].getId());
assertEquals("foo", registry.getFlowDefinition("foo").getId());
}
}