commit e7ecbbc7e51b497d261f459cbcc14675950457bd Author: Ben Hale Date: Thu Dec 21 16:02:27 2006 +0000 Moved Spring Web Flow to a top level project diff --git a/build-spring-webflow/.cvsignore b/build-spring-webflow/.cvsignore new file mode 100644 index 00000000..8182ecf6 --- /dev/null +++ b/build-spring-webflow/.cvsignore @@ -0,0 +1,2 @@ +build.properties +target diff --git a/build-spring-webflow/.project b/build-spring-webflow/.project new file mode 100644 index 00000000..509da81f --- /dev/null +++ b/build-spring-webflow/.project @@ -0,0 +1,11 @@ + + + build-spring-webflow + + + + + + + + diff --git a/build-spring-webflow/build-continuous.xml b/build-spring-webflow/build-continuous.xml new file mode 100644 index 00000000..bb318816 --- /dev/null +++ b/build-spring-webflow/build-continuous.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build-spring-webflow/build.xml b/build-spring-webflow/build.xml new file mode 100644 index 00000000..313435f6 --- /dev/null +++ b/build-spring-webflow/build.xml @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Master build for Spring Web Flow and Spring Web Flow samples. + +Please execute + ant -p + +to see a list of all relevant targets. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + projects=${projects} + projects.names=${projects.names} + + + + + + + + projects=${projects} + + + + + + + + projects=${projects} + projects.names=${projects.names} + + + + + + + projects=${projects} + + + + + + + projects=${projects} + + + + + + + projects=${projects} + + + + + + + + + + projects=${projects} + + + + + + + projects=${projects} + + + + + + + projects=${projects} + + + + + + + projects=${projects} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build-spring-webflow/project.properties b/build-spring-webflow/project.properties new file mode 100644 index 00000000..eea65469 --- /dev/null +++ b/build-spring-webflow/project.properties @@ -0,0 +1,7 @@ +# Configurable property values that may be locally overriden by a build.properties file + +# The version number of the release that will be built +release.version=1.0.1 + +# The location of the common build system +common.build.dir=${basedir}/../common-build diff --git a/build-spring-webflow/readme.txt b/build-spring-webflow/readme.txt new file mode 100644 index 00000000..22142eed --- /dev/null +++ b/build-spring-webflow/readme.txt @@ -0,0 +1,27 @@ +This is where the master build that creates releases of Spring Web Flow resides. + +USERS +- To build all Spring Web Flow related projects: + + 1. From this directory, run: + ant dist + +Build Pre-requisites: +- javac 1.5 or > must be in your system path +- ant 1.6 or > must be in your system path +- ivy 1.3 or > (Note: a version of Ivy is included and will be used automatically if you do not already have + Ivy installed in your ANT_HOME/lib directory. + If you have Ivy already installed in %ANT_HOME%/lib make sure it is 1.3 or >. 1.2 won't work.) + +DEVELOPERS +- To build a new Spring Web Flow product release: + + 1. Update project.properties to reflect the new release version, if necessary. + + 2. From this directory, run: + ant release + + The release archive will be created and placed in: + target/release + +Questions? See http://forum.springframework.org. \ No newline at end of file diff --git a/common-build/.cvsignore b/common-build/.cvsignore new file mode 100644 index 00000000..8bbd83c4 --- /dev/null +++ b/common-build/.cvsignore @@ -0,0 +1,11 @@ +build.properties +*.jpx.local* +*.log +*.iws +*.tws +target +dist +gen-src +bak +mimedata +ivy-cache diff --git a/common-build/.project b/common-build/.project new file mode 100644 index 00000000..41d6903f --- /dev/null +++ b/common-build/.project @@ -0,0 +1,11 @@ + + + common-build + + + + + + + + diff --git a/common-build/clover-targets.xml b/common-build/clover-targets.xml new file mode 100644 index 00000000..50af58bb --- /dev/null +++ b/common-build/clover-targets.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common-build/common-targets.xml b/common-build/common-targets.xml new file mode 100644 index 00000000..c384b209 --- /dev/null +++ b/common-build/common-targets.xml @@ -0,0 +1,928 @@ + + + + + + + + + + + + + + + + ANT build for ${project.name} ${project.version}. + + Please execute: + ant -projecthelp + or + ant -p + to see a list of publically executable targets. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + reading ivy config + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + copying generated resources + + + + + + + + copying generated test resources + + + + + + + + + + + + + + + + + + + + + + + + compiling main sources + + + + + + + + + compiling test sources + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common-build/db-targets.xml b/common-build/db-targets.xml new file mode 100644 index 00000000..06aaa0ac --- /dev/null +++ b/common-build/db-targets.xml @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + User sql.src = '${sql.src}' + + + + + + + + + + + + Admin sql.src = '${sql.src}' + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common-build/doc-targets.xml b/common-build/doc-targets.xml new file mode 100644 index 00000000..895439fe --- /dev/null +++ b/common-build/doc-targets.xml @@ -0,0 +1,157 @@ + + + + + + + + + + +Targets for generating reference documentation. + +Please execute + ant -p + +to see a list of all relevant targets. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common-build/ejb21-targets.xml b/common-build/ejb21-targets.xml new file mode 100644 index 00000000..6732d54b --- /dev/null +++ b/common-build/ejb21-targets.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common-build/hibernate-targets.xml b/common-build/hibernate-targets.xml new file mode 100644 index 00000000..f82fce41 --- /dev/null +++ b/common-build/hibernate-targets.xml @@ -0,0 +1,127 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + generating database.properties from build.properties + + + + + + + + + + + + \ No newline at end of file diff --git a/common-build/ivyconf.properties b/common-build/ivyconf.properties new file mode 100644 index 00000000..f2d940b6 --- /dev/null +++ b/common-build/ivyconf.properties @@ -0,0 +1,2 @@ +repository.dir=${ivy.conf.dir}/../repository +integration.repo.dir=${ivy.conf.dir}/../integration-repo diff --git a/common-build/ivyconf.xml b/common-build/ivyconf.xml new file mode 100644 index 00000000..dcb9e570 --- /dev/null +++ b/common-build/ivyconf.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/common-build/lib/ivy-1.4.1.jar b/common-build/lib/ivy-1.4.1.jar new file mode 100644 index 00000000..09b0403e Binary files /dev/null and b/common-build/lib/ivy-1.4.1.jar differ diff --git a/common-build/project.properties b/common-build/project.properties new file mode 100644 index 00000000..d76f6920 --- /dev/null +++ b/common-build/project.properties @@ -0,0 +1,135 @@ +# values in here are overriden by build.properties and project.properties in +# individual projects, and build.properties in this dir, in that order + +# default wildcard for finding JUnit tests +# the convention is that our JUnit test classes have XXXTests-style names +test.includes=**/*Test.class,**/*Tests.class + +# wildcards to exclude among JUnit tests +test.excludes=**/Abstract* + +# we define standard directory locations here +# these should not normally never have to be overriden +# we define them here instead of in the XML so that Ant nested property +# replacement can work for properties overrides by other properties files + +# root of any bin files needed during build +bin.dir=${basedir}/bin + +# libs needed for build process. ivy pulls down jars into various dirs under here +lib.dir=${basedir}/lib + +# any static libs that need to be checked in instead of being pulled down by ivy +# should be placed somehwere under here. The standard build doesn't touch this though +static.lib.dir=${basedir}/static-lib + +# root of sources hierarchy +src.dir=${basedir}/src + +# main sources +src.java.main.dir=${src.dir}/main/java + +# test sources +src.java.test.dir=${src.dir}/test/java + +# root of web-apps sources +src.web.dir=${basedir}/src/main/webapp + +# root of source doc files +src.doc.dir=${basedir}/src/doc + +# root of etc hierarchy, typically used for templated sources and config files +src.etc.dir=${basedir}/src/etc + +# resources get copied into target classes dir with filtering +src.resources.dir=${src.dir}/main/resources +src.dtd.dir=${src.etc.dir}/dtd +src.tld.dir=${src.etc.dir}/tld + +# test resources get copied into target test classes dir with filtering +src.test.resources.dir=${src.etc.dir}/test-resources + +# root of build hierarchy +target.dir=${basedir}/target + +# any generated java sources go here +target.gen.java.dir=${target.dir}/gen-java-src +target.gen.java.test.dir=${target.dir}/gen-java-test-src + +# where built source class files and resources go +target.classes.dir=${target.dir}/classes + +# where built test class files and resources go +target.testclasses.dir=${target.dir}/test-classes + +# where test results end up +target.testresults.dir=${target.dir}/test-reports + +# where JavaDoc generate files go +target.javadocs.dir=${target.dir}/javadocs + +# where J2SE/J2EE modules (.jar, .war, .ear) go +target.artifacts.dir=${target.dir}/artifacts + +# where target lib archives (.JARs) go +target.lib.dir=${target.artifacts.dir}/lib + +# where WAR archive goes +target.artifacts.war.dir=${target.artifacts.dir}/war + +# where EAR archive goes +target.artifacts.ear.dir=${target.artifacts.dir}/ear + +# where any exploded web-app goes +target.war.expanded.dir=${target.artifacts.dir}/war-expanded + +# where any exploded EAR goes +target.ear.expanded.dir=${target.artifacts.dir}/ear-expanded + +# file used to provide token filter values during text file copies +target.filter.file=${target.dir}/filter.properties + +# where all project documentation resides +docs.dir=${basedir}/docs + +# where all project reference documentation resides +docs.ref.dir=${docs.dir}/reference + +# where all project reference documentation sources reside +docs.ref.src.dir=${docs.dir}/reference/src + +# where all DocBook libraries reside +docs.ref.lib.dir=${docs.dir}/reference/lib + +# where the project reference documentation is built +docs.ref.target.dir=${docs.dir}/reference/target + +# where the dist target is saved +dist.dir=${target.dir}/dist + +# sample: set proper packages here, for javadocs +packages=stub.* + +# override default ivy retrieve pattern to include configuration name in path, and no +# revision number The main advantage of no revision number in the name is that when pulling +# down subsequent snapshots of the same jar, a newer one will just overwrite a previous +# version. This means we are not forced to clean the lib dir between retrieves. +ivy.retrieve.pattern=${lib.dir}/[conf]/[artifact].[ext] + +# To add the revision, comment out the previous, and add next 2 to build.properties +# clear.libs.before.retrieve=anything +# ivy.retrieve.pattern=${lib.dir}/[conf]/[artifact]-[revision].[ext] + +# now define patterns for special retrieve used to build repo for release +release.repo.ivy.retrieve.pattern=[organisation]/[module]/[revision]/[artifact]-[revision].[ext] +release.repo.ivy.retrieve.ivy.pattern=[organisation]/[module]/ivy-[revision].xml + +# override default ivy distrib dir to go under targets +ivy.distrib.dir=${target.dir}/dist +# override where ivy.xml files are placed, so they are hierarchical and are safe +# to merge from multiple projects +ivy.srcivypattern=${ivy.distrib.dir}/ivys/[organisation]/[module]/ivy-[revision].xml + +# Location where temporary data goes. Only used for Oracle-specific create-user script +# needs to be customized for the user if location is not appropriate, or always for unix +data.dir=c:\\temp \ No newline at end of file diff --git a/common-build/readme.txt b/common-build/readme.txt new file mode 100644 index 00000000..d94b2b73 --- /dev/null +++ b/common-build/readme.txt @@ -0,0 +1,37 @@ +Contained in this directory is the Spring Jumpstart common build system used +to build all Spring projects. + +This generic build system is ant 1.6 based and also requires Ivy 1.3 or > for dependency management. + +Projects are expected to import master build files contained within +this directory as needed for the build targets they require. + +Build targets are organized into logical files: +- common-targets.xml : core targets applicable to all projects +- clover-targets.xml : for working with clover +- tomcat-targets.xml : for deploying webapps to the tomcat servlet container +etc... + +As an example, here is Spring Web Flow's project build.xml: + + + + + + + + + + + + + + + + + + +This build.xml imports the "common-targets.xml" fragment containing +core targets for compilation, distribution unit creation, and junit +testing. It also imports "clover-targets.xml" to facilitate the +generation of test coverage reports with clover. \ No newline at end of file diff --git a/common-build/spring-targets.xml b/common-build/spring-targets.xml new file mode 100644 index 00000000..286cc37d --- /dev/null +++ b/common-build/spring-targets.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common-build/templates/db/data.xml b/common-build/templates/db/data.xml new file mode 100644 index 00000000..bae3fc3d --- /dev/null +++ b/common-build/templates/db/data.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/common-build/templates/jsp/includeBottom.jsp b/common-build/templates/jsp/includeBottom.jsp new file mode 100644 index 00000000..4686f911 --- /dev/null +++ b/common-build/templates/jsp/includeBottom.jsp @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/common-build/templates/jsp/includeTop.jsp b/common-build/templates/jsp/includeTop.jsp new file mode 100644 index 00000000..5911a810 --- /dev/null +++ b/common-build/templates/jsp/includeTop.jsp @@ -0,0 +1,22 @@ +<%@ page contentType="text/html" %> +<%@ page session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> + + + +@TITLE@ + + + + + + + + \ No newline at end of file diff --git a/common-build/templates/jsp/pageshell.jsp b/common-build/templates/jsp/pageshell.jsp new file mode 100644 index 00000000..c60d2bb5 --- /dev/null +++ b/common-build/templates/jsp/pageshell.jsp @@ -0,0 +1,7 @@ +<%@ include file="includeTop.jsp" %> + +
+ +
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/common-build/templates/log4j/log4j.properties b/common-build/templates/log4j/log4j.properties new file mode 100644 index 00000000..5223d5b0 --- /dev/null +++ b/common-build/templates/log4j/log4j.properties @@ -0,0 +1,19 @@ +log4j.rootCategory=WARN, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=${@PROJECT_NAME@.root}/@PROJECT_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout + +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable debug interceptor +#log4j.category.org.springframework.aop.interceptor=DEBUG \ No newline at end of file diff --git a/common-build/templates/projects/standard/.classpath b/common-build/templates/projects/standard/.classpath new file mode 100644 index 00000000..ce8eb525 --- /dev/null +++ b/common-build/templates/projects/standard/.classpath @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/common-build/templates/projects/standard/.cvsignore b/common-build/templates/projects/standard/.cvsignore new file mode 100644 index 00000000..16b87eb5 --- /dev/null +++ b/common-build/templates/projects/standard/.cvsignore @@ -0,0 +1,8 @@ +target +lib +bak +build.properties +*.log +*.jpx.local* +*.iws +*.tws diff --git a/common-build/templates/projects/standard/.project b/common-build/templates/projects/standard/.project new file mode 100644 index 00000000..6f35c775 --- /dev/null +++ b/common-build/templates/projects/standard/.project @@ -0,0 +1,17 @@ + + + @PROJECT_NAME@ + + + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/common-build/templates/projects/standard/build.xml b/common-build/templates/projects/standard/build.xml new file mode 100644 index 00000000..c38a109a --- /dev/null +++ b/common-build/templates/projects/standard/build.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common-build/templates/projects/standard/ivy.xml b/common-build/templates/projects/standard/ivy.xml new file mode 100644 index 00000000..6bb7d161 --- /dev/null +++ b/common-build/templates/projects/standard/ivy.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common-build/templates/projects/standard/project.properties b/common-build/templates/projects/standard/project.properties new file mode 100644 index 00000000..1fecca74 --- /dev/null +++ b/common-build/templates/projects/standard/project.properties @@ -0,0 +1,5 @@ +# to override these or other properties with local user settings, +# create a unversioned build.properties file + +# The location of the common build system +common.build.dir=${basedir}/../common-build \ No newline at end of file diff --git a/common-build/templates/projects/standard/src/etc/filter.properties b/common-build/templates/projects/standard/src/etc/filter.properties new file mode 100644 index 00000000..c54e5880 --- /dev/null +++ b/common-build/templates/projects/standard/src/etc/filter.properties @@ -0,0 +1,33 @@ +# $Header$ + +# Contains filterable project settings. Setting placeholders in filterable project text +# files will be replaced with these values when the 'statics' build target is run. +# +# You may add static settings directly to this source file in the format: +# setting=value e.g MY_SETTING=myvalue +# This is appropriate usage if you know the setting value will never change. +# +# At build time this source file is copied to the ${target.dir} where additional +# dynamic settings may be appended using the task. Use this approach +# when a setting value depends on the build or the local user's environment. +# +# An example of this approach is shown below: +# +# build.xml +# +# +# +# +# +# +# +# +# This allows for dynamic replacement values that are sourced from local properties files to facilitate +# local user settings. +# +# To refer to filterable settings within project source files like config files, JSPs, or +# other text files use the standard ant placeholder format: +# @SETTING_NAME@ e.g, @MY_SETTING@ and @MY_LOCAL_SETTING@ +# +# Your settings: diff --git a/common-build/templates/projects/standard/src/etc/test-resources/log4j.properties b/common-build/templates/projects/standard/src/etc/test-resources/log4j.properties new file mode 100644 index 00000000..c6d11b4d --- /dev/null +++ b/common-build/templates/projects/standard/src/etc/test-resources/log4j.properties @@ -0,0 +1,16 @@ +log4j.rootCategory=WARN, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=${@PROJECT_NAME@.root}/@PROJECT_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout + +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n diff --git a/common-build/templates/projects/standard/src/etc/test-resources/readme.txt b/common-build/templates/projects/standard/src/etc/test-resources/readme.txt new file mode 100644 index 00000000..5ae540d8 --- /dev/null +++ b/common-build/templates/projects/standard/src/etc/test-resources/readme.txt @@ -0,0 +1,5 @@ +test-resources +--------- +Test-time related configuration files (*.properties, *.xml) and other resources reside in this directory. These files are copied into the test classpath, they are not included in the project distribution unit. + +You may remove this file once you understand the project structure and the purpose of this directory: this file also serves as a "empty directory" placeholder so CVS won't prune the directory. \ No newline at end of file diff --git a/common-build/templates/projects/standard/src/main/java/readme.txt b/common-build/templates/projects/standard/src/main/java/readme.txt new file mode 100644 index 00000000..d13c5d57 --- /dev/null +++ b/common-build/templates/projects/standard/src/main/java/readme.txt @@ -0,0 +1,5 @@ +java +------ +Project source code resides in this directory. Files here are compiled into classes placed in ${project.dir}/target/classes and added to the compile classpath. + +You may remove this file once you understand the project structure and the purpose of this directory: this file also serves as a "empty directory" placeholder so CVS won't prune the directory. diff --git a/common-build/templates/projects/standard/src/main/resources/readme.txt b/common-build/templates/projects/standard/src/main/resources/readme.txt new file mode 100644 index 00000000..b354a2ba --- /dev/null +++ b/common-build/templates/projects/standard/src/main/resources/readme.txt @@ -0,0 +1,5 @@ +resources +--------- +Tweakable application configuration files (*.properties) and other resources reside in this directory. + +You may remove this file once you understand the project structure and the purpose of this directory: this file also serves as a "empty directory" placeholder so CVS won't prune the directory. \ No newline at end of file diff --git a/common-build/templates/projects/standard/src/test/java/readme.txt b/common-build/templates/projects/standard/src/test/java/readme.txt new file mode 100644 index 00000000..4a11f99e --- /dev/null +++ b/common-build/templates/projects/standard/src/test/java/readme.txt @@ -0,0 +1,5 @@ +test +------ +Project unit and integration tests reside within this directory. Files here are compiled into classes placed in ${project.dir}/target/test-classes and added to the test classpath. + +You may remove this file once you understand the project structure and the purpose of this directory: this file also serves as a "empty directory" placeholder so CVS won't prune the directory. diff --git a/common-build/templates/projects/webapp/WEB-INF/classes/log4j.properties b/common-build/templates/projects/webapp/WEB-INF/classes/log4j.properties new file mode 100644 index 00000000..5223d5b0 --- /dev/null +++ b/common-build/templates/projects/webapp/WEB-INF/classes/log4j.properties @@ -0,0 +1,19 @@ +log4j.rootCategory=WARN, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=${@PROJECT_NAME@.root}/@PROJECT_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout + +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable debug interceptor +#log4j.category.org.springframework.aop.interceptor=DEBUG \ No newline at end of file diff --git a/common-build/templates/projects/webapp/WEB-INF/dispatcher-servlet-config.xml b/common-build/templates/projects/webapp/WEB-INF/dispatcher-servlet-config.xml new file mode 100644 index 00000000..d32bf1cd --- /dev/null +++ b/common-build/templates/projects/webapp/WEB-INF/dispatcher-servlet-config.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/common-build/templates/projects/webapp/WEB-INF/web.xml b/common-build/templates/projects/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..8c924b9e --- /dev/null +++ b/common-build/templates/projects/webapp/WEB-INF/web.xml @@ -0,0 +1,41 @@ + + + + + + + contextConfigLocation + + + + + + + + org.springframework.web.context.ContextLoaderListener + + + + @PROJECT_NAME@ + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + /WEB-INF/dispatcher-servlet-config.xml + + + + + @PROJECT_NAME@ + *.htm + + + \ No newline at end of file diff --git a/common-build/templates/projects/webapp/index.html b/common-build/templates/projects/webapp/index.html new file mode 100644 index 00000000..b37b2a3f --- /dev/null +++ b/common-build/templates/projects/webapp/index.html @@ -0,0 +1,10 @@ + + +Shell + + + + + + + diff --git a/common-build/templates/readme.txt b/common-build/templates/readme.txt new file mode 100644 index 00000000..cd23c818 --- /dev/null +++ b/common-build/templates/readme.txt @@ -0,0 +1,2 @@ +Contained in this directory are useful templates for projects, configuration files, +and Java artifacts for products such as Spring, Spring Web Flow, and log4j. \ No newline at end of file diff --git a/common-build/templates/spring-webflow/FlowExecutionTestTemplate.java b/common-build/templates/spring-webflow/FlowExecutionTestTemplate.java new file mode 100644 index 00000000..a387ed55 --- /dev/null +++ b/common-build/templates/spring-webflow/FlowExecutionTestTemplate.java @@ -0,0 +1,24 @@ +package webflow.templates; + +import java.util.Map; + +import org.springframework.webflow.definition.registry.FlowDefinitionResource; +import org.springframework.webflow.test.execution.AbstractXmlFlowExecutionTests; +import org.springframework.webflow.test.execution.MockFlowServiceLocator; + +public class FlowExecutionTestTemplate extends AbstractXmlFlowExecutionTests { + + public void testStartFlow() { + + } + + @Override + protected FlowDefinitionResource getFlowDefinitionResource() { + return createFlowDefinitionResource("/path/to/myflow.xml"); + } + + @Override + protected void registerMockServices(MockFlowServiceLocator serviceRegistry) { + + } +} \ No newline at end of file diff --git a/common-build/templates/spring-webflow/flow-definition-1.0-form.xml b/common-build/templates/spring-webflow/flow-definition-1.0-form.xml new file mode 100644 index 00000000..6435e161 --- /dev/null +++ b/common-build/templates/spring-webflow/flow-definition-1.0-form.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common-build/templates/spring-webflow/flow-definition-1.0-simplest.xml b/common-build/templates/spring-webflow/flow-definition-1.0-simplest.xml new file mode 100644 index 00000000..927a309d --- /dev/null +++ b/common-build/templates/spring-webflow/flow-definition-1.0-simplest.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/common-build/templates/spring-webflow/flow-definition-1.0.xml b/common-build/templates/spring-webflow/flow-definition-1.0.xml new file mode 100644 index 00000000..aa080bea --- /dev/null +++ b/common-build/templates/spring-webflow/flow-definition-1.0.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/common-build/templates/spring-webflow/webflow-config-1.0-spring-1.2.xml b/common-build/templates/spring-webflow/webflow-config-1.0-spring-1.2.xml new file mode 100644 index 00000000..77c48d22 --- /dev/null +++ b/common-build/templates/spring-webflow/webflow-config-1.0-spring-1.2.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common-build/templates/spring-webflow/webflow-config-1.0.xml b/common-build/templates/spring-webflow/webflow-config-1.0.xml new file mode 100644 index 00000000..0cb22d0e --- /dev/null +++ b/common-build/templates/spring-webflow/webflow-config-1.0.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/common-build/templates/spring-webflow/webflow-form.jsp b/common-build/templates/spring-webflow/webflow-form.jsp new file mode 100644 index 00000000..285f09ca --- /dev/null +++ b/common-build/templates/spring-webflow/webflow-form.jsp @@ -0,0 +1,18 @@ +<%@ include file="includeTop.jsp" %> + +
+
+ + + + +
+ + + + +
+
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/common-build/templates/spring/bean-definitions-1.2.xml b/common-build/templates/spring/bean-definitions-1.2.xml new file mode 100644 index 00000000..7be11e1c --- /dev/null +++ b/common-build/templates/spring/bean-definitions-1.2.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/common-build/templates/spring/bean-definitions-2.0.xml b/common-build/templates/spring/bean-definitions-2.0.xml new file mode 100644 index 00000000..5ae46de2 --- /dev/null +++ b/common-build/templates/spring/bean-definitions-2.0.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/common-build/templates/tomcat/context.xml b/common-build/templates/tomcat/context.xml new file mode 100644 index 00000000..0aa6e718 --- /dev/null +++ b/common-build/templates/tomcat/context.xml @@ -0,0 +1,4 @@ + + + diff --git a/common-build/tomcat-targets.xml b/common-build/tomcat-targets.xml new file mode 100644 index 00000000..ea1129a0 --- /dev/null +++ b/common-build/tomcat-targets.xml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Set tomcat.dir in your USER_HOME/build.properties file (create if it doesn't exist) + + + \ No newline at end of file diff --git a/common-build/version.txt b/common-build/version.txt new file mode 100644 index 00000000..27a8d2a9 --- /dev/null +++ b/common-build/version.txt @@ -0,0 +1,13 @@ +version 20060409-1 + +Changelog: +2006-08-25: Keith: updated project and file templates inside "templates" +2006-04-09: Colin: upgrade Ivy to 1.3.1 +2006-03-26: Colin: upgrade Ivy to snapshot 20060322, for transitive dep fix. +2006-03-08: Colin: upgrade Ivy to 1.3RC3 +2006-02-17: Colin: Add retrieve-to-repo to common-targets.xml, user for release procoess +2006-02-13: Arjen: Standard docbook images are now also copied in doc-targets.xml +2006-02-08: Colin: add dir="${basedir} to junit task so it runs ok from other dirs +2006-01-04: Colin: add description element to "retrieve" since it's not an internal target +2005-11-27: Update Spring version in templates +2005-11-20: Arjen: add gen java source dir functionality for test sources too diff --git a/readme.txt b/readme.txt new file mode 100644 index 00000000..af93624a --- /dev/null +++ b/readme.txt @@ -0,0 +1,36 @@ +Contained in this directory are the Spring Web Flow (SWF) related project sources. + +DIRECTORIES + +1. build-spring-webflow - Contains the build scripts needed to build all SWF projects. + To build all, simply execute the 'dist' ant target. + +2. spring-binding - the data binding and mapping project, a Spring Web Flow driven internal library. + +3. spring-webflow - The core Spring Web Flow project. + +4. spring-webflow-samples - The Spring Web Flow sample applications, illustrating the framework in action. + +ARCHITECTURE DOCUMENTS + +Also contained in this directory are two SonarJ files +1. webflow-architecture.xml +2. webflow-workspace.xml + +When opened from SonarJ these provide an architectural breakdown of the Spring Web Flow projects. +It is recommended that you view this breakdown to familiarize yourself with the Spring Web Flow system +architecture, including its layers, subsystems, dependencies, and various architectural metrics such +as total lines of code and average component dependency. + +To use SonarJ: +1. Download it from http://www.hello2morrow.com/en/sonarj/sonarj.php +2. Install it +3. Launch it +4. Point to license key +5. Open workspace (webflow-workspace.xml) +6. Open architecture template (webflow-architecture.xml) +7. Right click on Web Flow Workspace root and click "Run all operations" +8. Click the various tabs to see different views + - Architecture view is recommended + - Layers sub-tab shows layer diagram + - Subsytstem sub-tab shows breakdown by subsystem \ No newline at end of file diff --git a/spring-binding/.classpath b/spring-binding/.classpath new file mode 100644 index 00000000..7786252f --- /dev/null +++ b/spring-binding/.classpath @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/spring-binding/.cvsignore b/spring-binding/.cvsignore new file mode 100644 index 00000000..16b87eb5 --- /dev/null +++ b/spring-binding/.cvsignore @@ -0,0 +1,8 @@ +target +lib +bak +build.properties +*.log +*.jpx.local* +*.iws +*.tws diff --git a/spring-binding/.project b/spring-binding/.project new file mode 100644 index 00000000..b93b2f87 --- /dev/null +++ b/spring-binding/.project @@ -0,0 +1,19 @@ + + + spring-binding + + + common-build + repository + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/spring-binding/.settings/org.eclipse.jdt.core.prefs b/spring-binding/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..d96886eb --- /dev/null +++ b/spring-binding/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,74 @@ +#Fri Oct 06 15:22:02 CEST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.3 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.3 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.doc.comment.support=enabled +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.invalidJavadoc=warning +org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=disabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=protected +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingJavadocComments=ignore +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=public +org.eclipse.jdt.core.compiler.problem.missingJavadocTags=ignore +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=public +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullReference=ignore +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.3 diff --git a/spring-binding/.settings/org.eclipse.jdt.ui.prefs b/spring-binding/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..0e971266 --- /dev/null +++ b/spring-binding/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Wed Oct 04 14:36:37 EDT 2006 +eclipse.preferences.version=1 +internal.default.compliance=user diff --git a/spring-binding/.settings/org.eclipse.wst.validation.prefs b/spring-binding/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 00000000..68bb37d5 --- /dev/null +++ b/spring-binding/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,6 @@ +#Fri May 05 18:13:37 EDT 2006 +DELEGATES_PREFERENCE=delegateValidatorListorg.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator\=org.eclipse.wst.wsdl.validation.internal.eclipse.Validator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator\=org.eclipse.wst.xsd.core.internal.validation.eclipse.Validator; +USER_BUILD_PREFERENCE=enabledBuildValidatorListorg.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator; +USER_MANUAL_PREFERENCE=enabledManualValidatorListorg.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator; +USER_PREFERENCE=overrideGlobalPreferencesfalse +eclipse.preferences.version=1 diff --git a/spring-binding/build.xml b/spring-binding/build.xml new file mode 100644 index 00000000..a4bb7089 --- /dev/null +++ b/spring-binding/build.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-binding/ivy.xml b/spring-binding/ivy.xml new file mode 100644 index 00000000..22c7ed0f --- /dev/null +++ b/spring-binding/ivy.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-binding/pom.xml b/spring-binding/pom.xml new file mode 100644 index 00000000..adaedf84 --- /dev/null +++ b/spring-binding/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + org.springframework + spring-binding + Spring Binding + 1.0.1-SNAPSHOT + Spring Data Binding Framework + http://www.springframework.org + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + scm:svn:https://svn.sourceforge.net/svnroot/springframework/spring-projects + scm:svn:https://svn.sourceforge.net/svnroot/springframework/spring-projects + 1.0 + http://svn.sourceforge.net/viewcvs.cgi/springframework/spring-projects + + + Spring Framework + http://www.springframework.org/ + + + + + commons-logging + commons-logging + 1.0.4 + + + ognl + ognl + 2.6.9 + + + + org.springframework + spring-beans + 2.0 + + + org.springframework + spring-context + 2.0 + + + org.springframework + spring-core + 2.0 + + + + log4j + log4j + 1.2.13 + runtime + true + + + + easymock + easymock + 2.2 + test + + + junit + junit + 3.8.1 + test + + + \ No newline at end of file diff --git a/spring-binding/project.properties b/spring-binding/project.properties new file mode 100644 index 00000000..3f811539 --- /dev/null +++ b/spring-binding/project.properties @@ -0,0 +1,11 @@ +# properties defined in this file are overridable by a local build.properties in this project dir + +# The location of the common build system +common.build.dir=${basedir}/../../common-build + +project.base.version=1.0.1 +#project.version=${project.base.version} +#ivy.status=release + +javac.source=1.3 +javac.target=1.3 diff --git a/spring-binding/src/etc/filter.properties b/spring-binding/src/etc/filter.properties new file mode 100644 index 00000000..c54e5880 --- /dev/null +++ b/spring-binding/src/etc/filter.properties @@ -0,0 +1,33 @@ +# $Header$ + +# Contains filterable project settings. Setting placeholders in filterable project text +# files will be replaced with these values when the 'statics' build target is run. +# +# You may add static settings directly to this source file in the format: +# setting=value e.g MY_SETTING=myvalue +# This is appropriate usage if you know the setting value will never change. +# +# At build time this source file is copied to the ${target.dir} where additional +# dynamic settings may be appended using the task. Use this approach +# when a setting value depends on the build or the local user's environment. +# +# An example of this approach is shown below: +# +# build.xml +# +# +# +# +# +# +# +# +# This allows for dynamic replacement values that are sourced from local properties files to facilitate +# local user settings. +# +# To refer to filterable settings within project source files like config files, JSPs, or +# other text files use the standard ant placeholder format: +# @SETTING_NAME@ e.g, @MY_SETTING@ and @MY_LOCAL_SETTING@ +# +# Your settings: diff --git a/spring-binding/src/etc/test-resources/log4j.properties b/spring-binding/src/etc/test-resources/log4j.properties new file mode 100644 index 00000000..4e8192fa --- /dev/null +++ b/spring-binding/src/etc/test-resources/log4j.properties @@ -0,0 +1,17 @@ +log4j.rootCategory=WARN, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=@TEST_RESULTS_DIR@/@PROJECT_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/collection/CompositeIterator.java b/spring-binding/src/main/java/org/springframework/binding/collection/CompositeIterator.java new file mode 100644 index 00000000..66b3ed68 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/collection/CompositeIterator.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.collection; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.NoSuchElementException; + +import org.springframework.util.Assert; + +/** + * Iterator that combines multiple other iterators. This is a simple implementation + * that just maintains a list of iterators which are invoked in sequence untill + * all iterators are exhausted. + * + * @author Erwin Vervaet + */ +public class CompositeIterator implements Iterator { + + private List iterators = new LinkedList(); + + private boolean inUse = false; + + /** + * Create a new composite iterator. Add iterators using the {@link #add(Iterator)} method. + */ + public CompositeIterator() { + } + + /** + * Add given iterator to this composite. + */ + public void add(Iterator iterator) { + Assert.state(!inUse, "You can no longer add iterator to a composite iterator that's already in use"); + if (iterators.contains(iterator)) { + throw new IllegalArgumentException("You cannot add the same iterator twice"); + } + iterators.add(iterator); + } + + public boolean hasNext() { + inUse = true; + for (Iterator it = iterators.iterator(); it.hasNext(); ) { + if (((Iterator)it.next()).hasNext()) { + return true; + } + } + return false; + } + + public Object next() { + inUse = true; + for (Iterator it = iterators.iterator(); it.hasNext(); ) { + Iterator iterator = (Iterator)it.next(); + if (iterator.hasNext()) { + return iterator.next(); + } + } + throw new NoSuchElementException("Exhaused all iterators"); + } + + public void remove() { + throw new UnsupportedOperationException("Remove is not supported"); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/collection/MapAccessor.java b/spring-binding/src/main/java/org/springframework/binding/collection/MapAccessor.java new file mode 100644 index 00000000..d574ca20 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/collection/MapAccessor.java @@ -0,0 +1,463 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.collection; + +import java.util.Collection; +import java.util.Map; + +import org.springframework.util.Assert; + +/** + * A simple, generic decorator for getting attributes out of a map. May be + * instantiated directly or used as a base class as a convenience. + * + * @author Keith Donald + */ +public class MapAccessor implements MapAdaptable { + + /** + * The target map. + */ + private Map map; + + /** + * Creates a new attribute map accessor. + * @param map the map + */ + public MapAccessor(Map map) { + Assert.notNull(map, "The map to decorate is required"); + this.map = map; + } + + // implementing MapAdaptable + + public Map asMap() { + return map; + } + + /** + * Returns a value in the map, returning the defaultValue if no value was + * found. + * @param key the key + * @param defaultValue the default + * @return the attribute value + */ + public Object get(Object key, Object defaultValue) { + if (!map.containsKey(key)) { + return defaultValue; + } + return map.get(key); + } + + /** + * Returns a value in the map, asserting it is of the required type if + * present and returning null if not found. + * @param key the key + * @param requiredType the required type + * @return the value + * @throws IllegalArgumentException if the key is present but the value is + * not of the required type + */ + public Object get(Object key, Class requiredType) throws IllegalArgumentException { + return get(key, requiredType, null); + } + + /** + * Returns a value in the map of the specified type, returning the + * defaultValue if no value is found. + * @param key the key + * @param requiredType the required type + * @param defaultValue the default + * @return the attribute value + * @throws IllegalArgumentException if the key is present but the value is + * not of the required type + */ + public Object get(Object key, Class requiredType, Object defaultValue) { + if (!map.containsKey(key)) { + return defaultValue; + } + return assertKeyValueOfType(key, requiredType); + } + + /** + * Returns a value in the map, throwing an exception if the attribute is not + * present and of the correct type. + * @param key the key + * @return the value + */ + public Object getRequired(Object key) throws IllegalArgumentException { + assertContainsKey(key); + return map.get(key); + } + + /** + * Returns an value in the map, asserting it is present and of the required + * type. + * @param key the key + * @param requiredType the required type + * @return the value + */ + public Object getRequired(Object key, Class requiredType) throws IllegalArgumentException { + assertContainsKey(key); + return assertKeyValueOfType(key, requiredType); + } + + /** + * Returns a string value in the map, returning null if no + * value was found. + * @param key the key + * @return the string value + * @throws IllegalArgumentException if the key is present but the value is + * not a string + */ + public String getString(Object key) throws IllegalArgumentException { + return getString(key, null); + } + + /** + * Returns a string value in the map, returning the defaultValue if no value + * was found. + * @param key the key + * @param defaultValue the default + * @return the string value + * @throws IllegalArgumentException if the key is present but the value is + * not a string + */ + public String getString(Object key, String defaultValue) throws IllegalArgumentException { + if (!map.containsKey(key)) { + return defaultValue; + } + return (String)assertKeyValueOfType(key, String.class); + } + + /** + * Returns a string value in the map, throwing an exception if the attribute + * is not present and of the correct type. + * @param key the key + * @return the string value + * @throws IllegalArgumentException if the key is not present or present but + * the value is not a string + */ + public String getRequiredString(Object key) throws IllegalArgumentException { + assertContainsKey(key); + return (String)assertKeyValueOfType(key, String.class); + } + + /** + * Returns a collection value in the map, returning null if + * no value was found. + * @param key the key + * @return the collection value + * @throws IllegalArgumentException if the key is present but the value is + * not a collection + */ + public Collection getCollection(Object key) throws IllegalArgumentException { + if (!map.containsKey(key)) { + return null; + } + return (Collection)assertKeyValueOfType(key, Collection.class); + } + + /** + * Returns a collection value in the map, asserting it is of the required + * type if present and returning null if not found. + * @param key the key + * @return the collection value + * @throws IllegalArgumentException if the key is present but the value is + * not a collection + */ + public Collection getCollection(Object key, Class requiredType) throws IllegalArgumentException { + if (!map.containsKey(key)) { + return null; + } + assertAssignableTo(Collection.class, requiredType); + return (Collection)assertKeyValueOfType(key, requiredType); + } + + /** + * Returns a collection value in the map, throwing an exception if not + * found. + * @param key the key + * @return the collection value + * @throws IllegalArgumentException if the key is not present or present but + * the value is not a collection + */ + public Collection getRequiredCollection(Object key) throws IllegalArgumentException { + assertContainsKey(key); + return (Collection)assertKeyValueOfType(key, Collection.class); + } + + /** + * Returns a collection value in the map, asserting it is of the required + * type if present and throwing an exception if not found. + * @param key the key + * @return the collection value + * @throws IllegalArgumentException if the key is not present or present but + * the value is not a collection of the required type + */ + public Collection getRequiredCollection(Object key, Class requiredType) throws IllegalArgumentException { + assertContainsKey(key); + assertAssignableTo(Collection.class, requiredType); + return (Collection)assertKeyValueOfType(key, requiredType); + } + + /** + * Returns a array value in the map, asserting it is of the required type if + * present and returning null if not found. + * @param key the key + * @return the array value + * @throws IllegalArgumentException if the key is present but the value is + * not an array of the required type + */ + public Object[] getArray(Object key, Class requiredType) throws IllegalArgumentException { + assertAssignableTo(Object[].class, requiredType); + if (!map.containsKey(key)) { + return null; + } + return (Object[])assertKeyValueOfType(key, requiredType); + } + + /** + * Returns an array value in the map, asserting it is of the required type + * if present and throwing an exception if not found. + * @param key the key + * @return the array value + * @throws IllegalArgumentException if the key is not present or present but + * the value is not a array of the required type + */ + public Object[] getRequiredArray(Object key, Class requiredType) throws IllegalArgumentException { + assertContainsKey(key); + assertAssignableTo(Object[].class, requiredType); + return (Object[])assertKeyValueOfType(key, requiredType); + } + + /** + * Returns a number value in the map that is of the specified type, + * returning null if no value was found. + * @param key the key + * @param requiredType the required number type + * @return the numbervalue + * @throws IllegalArgumentException if the key is present but the value is + * not a number of the required type + */ + public Number getNumber(Object key, Class requiredType) throws IllegalArgumentException { + return getNumber(key, requiredType, null); + } + + /** + * Returns a number attribute value in the map of the specified type, + * returning the defaultValue if no value was found. + * @param key the attribute name + * @return the number value + * @param defaultValue the default + * @throws IllegalArgumentException if the key is present but the value is + * not a number of the required type + */ + public Number getNumber(Object key, Class requiredType, Number defaultValue) throws IllegalArgumentException { + if (!map.containsKey(key)) { + return defaultValue; + } + assertAssignableTo(Number.class, requiredType); + return (Number)assertKeyValueOfType(key, requiredType); + } + + /** + * Returns a number value in the map, throwing an exception if the attribute + * is not present and of the correct type. + * @param key the key + * @return the number value + * @throws IllegalArgumentException if the key is not present or present but + * the value is not a number of the required type + */ + public Number getRequiredNumber(Object key, Class requiredType) throws IllegalArgumentException { + assertContainsKey(key); + return (Number)assertKeyValueOfType(key, requiredType); + } + + /** + * Returns an integer value in the map, returning null if no + * value was found. + * @param key the key + * @return the integer value + * @throws IllegalArgumentException if the key is present but the value is + * not an integer + */ + public Integer getInteger(Object key) throws IllegalArgumentException { + return getInteger(key, null); + } + + /** + * Returns an integer value in the map, returning the defaultValue if no + * value was found. + * @param key the key + * @param defaultValue the default + * @return the integer value + * @throws IllegalArgumentException if the key is present but the value is + * not an integer + */ + public Integer getInteger(Object key, Integer defaultValue) throws IllegalArgumentException { + return (Integer)getNumber(key, Integer.class, defaultValue); + } + + /** + * Returns an integer value in the map, throwing an exception if the value + * is not present and of the correct type. + * @param key the attribute name + * @return the integer attribute value + * @throws IllegalArgumentException if the key is not present or present but + * the value is not an integer + */ + public Integer getRequiredInteger(Object key) throws IllegalArgumentException { + return (Integer)getRequiredNumber(key, Integer.class); + } + + /** + * Returns a long value in the map, returning null if no + * value was found. + * @param key the key + * @return the long value + * @throws IllegalArgumentException if the key is present but not a long + */ + public Long getLong(Object key) throws IllegalArgumentException { + return getLong(key, null); + } + + /** + * Returns a long value in the map, returning the defaultValue if no value + * was found. + * @param key the key + * @param defaultValue the default + * @return the long attribute value + * @throws IllegalArgumentException if the key is present but the value is + * not a long + */ + public Long getLong(Object key, Long defaultValue) throws IllegalArgumentException { + return (Long)getNumber(key, Long.class, defaultValue); + } + + /** + * Returns a long value in the map, throwing an exception if the value is + * not present and of the correct type. + * @param key the key + * @return the long attribute value + * @throws IllegalArgumentException if the key is not present or present but + * the value is not a long + */ + public Long getRequiredLong(Object key) throws IllegalArgumentException { + return (Long)getRequiredNumber(key, Long.class); + } + + /** + * Returns a boolean value in the map, returning null if no + * value was found. + * @param key the key + * @return the boolean value + * @throws IllegalArgumentException if the key is present but the value is + * not a boolean + */ + public Boolean getBoolean(Object key) throws IllegalArgumentException { + return getBoolean(key, null); + } + + /** + * Returns a boolean value in the map, returning the defaultValue if no + * value was found. + * @param key the key + * @param defaultValue the default + * @return the boolean value + * @throws IllegalArgumentException if the key is present but the value is + * not a boolean + */ + public Boolean getBoolean(Object key, Boolean defaultValue) throws IllegalArgumentException { + if (!map.containsKey(key)) { + return defaultValue; + } + return (Boolean)assertKeyValueOfType(key, Boolean.class); + } + + /** + * Returns a boolean value in the map, throwing an exception if the value is + * not present and of the correct type. + * @param key the attribute + * @return the boolean value + * @throws IllegalArgumentException if the key is not present or present but + * the value is not a boolean + */ + public Boolean getRequiredBoolean(Object key) throws IllegalArgumentException { + assertContainsKey(key); + return (Boolean)assertKeyValueOfType(key, Boolean.class); + } + + /** + * Asserts that the attribute is present in the attribute map. + * @param key the key + * @throws IllegalArgumentException if the key is not present + */ + public void assertContainsKey(Object key) throws IllegalArgumentException { + if (!map.containsKey(key)) { + throw new IllegalArgumentException("Required attribute '" + key + + "' is not present in map; attributes present are [" + asMap() + "]"); + } + } + + /** + * Indicates if the attribute is present in the attribute map and of the + * required type. + * @param key the attribute name + * @return true if present and of the required type, false if not present. + */ + public boolean containsKey(Object key, Class requiredType) throws IllegalArgumentException { + if (map.containsKey(key)) { + assertKeyValueOfType(key, requiredType); + return true; + } + else { + return false; + } + } + + /** + * Assert that value of the mak key is of the required type. + * @param key the attribute name + * @param requiredType the required attribute value type + * @return the attribute value + */ + public Object assertKeyValueOfType(Object key, Class requiredType) { + return assertKeyValueInstanceOf(key, map.get(key), requiredType); + } + + /** + * Assert that the key value is an instance of the required type. + * @param key the key + * @param value the value + * @param requiredType the required type + * @return the value + */ + public Object assertKeyValueInstanceOf(Object key, Object value, Class requiredType) { + Assert.notNull(requiredType, "The required type to assert is required"); + if (!requiredType.isInstance(value)) { + throw new IllegalArgumentException("Map key '" + key + "' has value [" + value + + "] that is not of expected type [" + requiredType + "], instead it is of type [" + + (value != null ? value.getClass().getName() : "null") + "]"); + } + return value; + } + + private void assertAssignableTo(Class clazz, Class requiredType) { + Assert.isTrue(clazz.isAssignableFrom(requiredType), "The provided required type must be assignable to [" + + clazz + "]"); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/collection/MapAdaptable.java b/spring-binding/src/main/java/org/springframework/binding/collection/MapAdaptable.java new file mode 100644 index 00000000..e4409452 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/collection/MapAdaptable.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.collection; + +import java.util.Map; + +/** + * An object whose contents are capable of being exposed as an unmodifiable map. + * + * @author Keith Donald + */ +public interface MapAdaptable { + + /** + * Returns this object's contents as a {@link Map}. The returned map may or + * may not be modifiable depending on this implementation. + *

+ * Warning: this operation may be called frequently; if so care should be + * taken so that the map contents (if calculated) be cached as appropriate. + * @return the object's contents as a map + */ + public Map asMap(); + +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/collection/SharedMap.java b/spring-binding/src/main/java/org/springframework/binding/collection/SharedMap.java new file mode 100644 index 00000000..385cbeec --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/collection/SharedMap.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.collection; + +import java.util.Map; + +/** + * A simple subinterface of {@link Map} that exposes a mutex that + * application code can synchronize on. + *

+ * Expected to be implemented by Maps that are backed by shared objects that + * require synchronization between multiple threads. An example would be the + * HTTP session map. + * + * @author Keith Donald + */ +public interface SharedMap extends Map { + + /** + * Returns the shared mutex that may be synchronized on using a + * synchronized block. The returned mutex is guaranteed to be non-null. + * + * Example usage: + * + *

+	 * synchronized (sharedMap.getMutex()) {
+	 * 	// do synchronized work
+	 * }
+	 * 
+ * + * @return the mutex + */ + public Object getMutex(); +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/collection/SharedMapDecorator.java b/spring-binding/src/main/java/org/springframework/binding/collection/SharedMapDecorator.java new file mode 100644 index 00000000..451afe56 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/collection/SharedMapDecorator.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.springframework.binding.collection; + +import java.io.Serializable; +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import org.springframework.core.style.ToStringCreator; + +/** + * A map decorator that implements SharedMap. By default, simply + * returns the map itself as the mutex. Subclasses may override to return a + * different mutex object. + * + * @author Keith Donald + */ +public class SharedMapDecorator implements SharedMap, Serializable { + + /** + * The wrapped, target map. + */ + private Map map; + + /** + * Creates a new shared map decorator. + * @param map the map that is shared by multiple threads, to be synced + */ + public SharedMapDecorator(Map map) { + this.map = map; + } + + // implementing Map + + public void clear() { + map.clear(); + } + + public boolean containsKey(Object key) { + return map.containsKey(key); + } + + public boolean containsValue(Object value) { + return map.containsValue(value); + } + + public Set entrySet() { + return map.entrySet(); + } + + public Object get(Object key) { + return map.get(key); + } + + public boolean isEmpty() { + return map.isEmpty(); + } + + public Set keySet() { + return map.keySet(); + } + + public Object put(Object key, Object value) { + return map.put(key, value); + } + + public void putAll(Map map) { + this.map.putAll(map); + } + + public Object remove(Object key) { + return map.remove(key); + } + + public int size() { + return map.size(); + } + + public Collection values() { + return map.values(); + } + + // implementing SharedMap + + public Object getMutex() { + return map; + } + + public String toString() { + return new ToStringCreator(this).append("map", map).append("mutex", getMutex()).toString(); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/collection/StringKeyedMapAdapter.java b/spring-binding/src/main/java/org/springframework/binding/collection/StringKeyedMapAdapter.java new file mode 100644 index 00000000..45ab7d65 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/collection/StringKeyedMapAdapter.java @@ -0,0 +1,285 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.collection; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + * Base class for map adapters whose keys are String values. Concrete + * classes need only implement the abstract hook methods defined by this class. + * + * @author Keith Donald + */ +public abstract class StringKeyedMapAdapter implements Map { + + private Set keySet; + + private Collection values; + + private Set entrySet; + + // implementing Map + + public void clear() { + for (Iterator it = getAttributeNames(); it.hasNext();) { + removeAttribute((String)it.next()); + } + } + + public boolean containsKey(Object key) { + return getAttribute(key.toString()) != null; + } + + public boolean containsValue(Object value) { + if (value == null) { + return false; + } + for (Iterator it = getAttributeNames(); it.hasNext();) { + Object aValue = getAttribute((String)it.next()); + if (value.equals(aValue)) { + return true; + } + } + return false; + } + + public Set entrySet() { + return (entrySet != null) ? entrySet : (entrySet = new EntrySet()); + } + + public Object get(Object key) { + return getAttribute(key.toString()); + } + + public boolean isEmpty() { + return !getAttributeNames().hasNext(); + } + + public Set keySet() { + return (keySet != null) ? keySet : (keySet = new KeySet()); + } + + public Object put(Object key, Object value) { + String stringKey = String.valueOf(key); + Object previousValue = getAttribute(stringKey); + setAttribute(stringKey, value); + return previousValue; + } + + public void putAll(Map map) { + for (Iterator it = map.entrySet().iterator(); it.hasNext();) { + Entry entry = (Entry)it.next(); + setAttribute(entry.getKey().toString(), entry.getValue()); + } + } + + public Object remove(Object key) { + String stringKey = key.toString(); + Object retval = getAttribute(stringKey); + removeAttribute(stringKey); + return retval; + } + + public int size() { + int size = 0; + for (Iterator it = getAttributeNames(); it.hasNext();) { + size++; + it.next(); + } + return size; + } + + public Collection values() { + return (values != null) ? values : (values = new Values()); + } + + // hook methods + + /** + * Hook method that needs to be implemented by concrete subclasses. + * Gets a value associated with a key. + * @param key the key to lookup + * @return the associated value, or null if none + */ + protected abstract Object getAttribute(String key); + + /** + * Hook method that needs to be implemented by concrete subclasses. + * Puts a key-value pair in the map, overwriting any possible earlier + * value associated with the same key. + * @param key the key to associate the value with + * @param value the value to associate with the key + */ + protected abstract void setAttribute(String key, Object value); + + /** + * Hook method that needs to be implemented by concrete subclasses. + * Removes a key and its associated value from the map. + * @param key the key to remove + */ + protected abstract void removeAttribute(String key); + + /** + * Hook method that needs to be implemented by concrete subclasses. + * Returns an enumeration listing all keys known to the map. + * @return the key enumeration + */ + protected abstract Iterator getAttributeNames(); + + // internal helper classes + + private abstract class AbstractSet extends java.util.AbstractSet { + public boolean isEmpty() { + return StringKeyedMapAdapter.this.isEmpty(); + } + + public int size() { + return StringKeyedMapAdapter.this.size(); + } + + public void clear() { + StringKeyedMapAdapter.this.clear(); + } + } + + private class KeySet extends AbstractSet { + public Iterator iterator() { + return new KeyIterator(); + } + + public boolean contains(Object o) { + return StringKeyedMapAdapter.this.containsKey(o); + } + + public boolean remove(Object o) { + return StringKeyedMapAdapter.this.remove(o) != null; + } + } + + private class KeyIterator implements Iterator { + protected final Iterator it = getAttributeNames(); + + protected Object currentKey; + + public void remove() { + if (currentKey == null) { + throw new NoSuchElementException("You must call next() at least once"); + } + StringKeyedMapAdapter.this.remove(currentKey); + } + + public boolean hasNext() { + return it.hasNext(); + } + + public Object next() { + return currentKey = it.next(); + } + } + + private class Values extends AbstractSet { + public Iterator iterator() { + return new ValuesIterator(); + } + + public boolean contains(Object o) { + return StringKeyedMapAdapter.this.containsValue(o); + } + + public boolean remove(Object o) { + if (o == null) { + return false; + } + for (Iterator it = iterator(); it.hasNext();) { + if (o.equals(it.next())) { + it.remove(); + return true; + } + } + return false; + } + } + + private class ValuesIterator extends KeyIterator { + public Object next() { + super.next(); + return StringKeyedMapAdapter.this.get(currentKey); + } + } + + private class EntrySet extends AbstractSet { + public Iterator iterator() { + return new EntryIterator(); + } + + public boolean contains(Object o) { + if (!(o instanceof Entry)) { + return false; + } + Entry entry = (Entry)o; + Object key = entry.getKey(); + Object value = entry.getValue(); + if (key == null || value == null) { + return false; + } + return value.equals(StringKeyedMapAdapter.this.get(key)); + } + + public boolean remove(Object o) { + if (!(o instanceof Entry)) { + return false; + } + Entry entry = (Entry)o; + Object key = entry.getKey(); + Object value = entry.getValue(); + if (key == null || value == null || !value.equals(StringKeyedMapAdapter.this.get(key))) { + return false; + } + return StringKeyedMapAdapter.this.remove(((Entry)o).getKey()) != null; + } + } + + private class EntryIterator extends KeyIterator { + public Object next() { + super.next(); + return new EntrySetEntry(currentKey); + } + } + + private class EntrySetEntry implements Entry { + private final Object currentKey; + + public EntrySetEntry(Object currentKey) { + this.currentKey = currentKey; + } + + public Object getKey() { + return currentKey; + } + + public Object getValue() { + return StringKeyedMapAdapter.this.get(currentKey); + } + + public Object setValue(Object value) { + return StringKeyedMapAdapter.this.put(currentKey, value); + } + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/collection/package.html b/spring-binding/src/main/java/org/springframework/binding/collection/package.html new file mode 100644 index 00000000..63c34d2b --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/collection/package.html @@ -0,0 +1,7 @@ + + +

+Collection related classes usable by other packages and systems. +

+ + \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/ConversionContext.java b/spring-binding/src/main/java/org/springframework/binding/convert/ConversionContext.java new file mode 100644 index 00000000..a79a179c --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/ConversionContext.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert; + +/** + * A context object with two main responsibities: + *
    + *
  1. Exposing information to a converter to influence + * a type conversion attempt. + *
  2. Providing operations for recording progress or + * errors during the type conversion process. + *
+ * Empty for now; subclasses may define their own custom context behavior + * accessible by a converter with a downcast. + * + * @author Keith Donald + */ +public interface ConversionContext { + +} diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/ConversionException.java b/spring-binding/src/main/java/org/springframework/binding/convert/ConversionException.java new file mode 100644 index 00000000..549747b7 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/ConversionException.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert; + +import org.springframework.core.NestedRuntimeException; + +/** + * Base class for exceptions thrown by the type conversion system. + * + * @author Keith Donald + */ +public class ConversionException extends NestedRuntimeException { + + /** + * The source type we tried to convert from + */ + private Class sourceClass; + + /** + * The value we tried to convert. + */ + private Object value; + + /** + * The target type we tried to convert to. + */ + private Class targetClass; + + /** + * Creates a new conversion exception. + * @param value the value we tried to convert + * @param targetClass the target type + */ + public ConversionException(Object value, Class targetClass) { + super("Unable to convert value '" + value + "' of type '" + (value != null ? value.getClass().getName() : null) + + "' to class '" + targetClass.getName() + "'"); + this.value = value; + this.targetClass = targetClass; + } + + /** + * Creates a new conversion exception. + * @param value the value we tried to convert + * @param targetClass the target type + * @param cause underlying cause of this exception + */ + public ConversionException(Object value, Class targetClass, Throwable cause) { + super("Unable to convert value '" + value + "' of type '" + (value != null ? value.getClass().getName() : null) + + "' to class '" + targetClass.getName() + "'", cause); + this.value = value; + this.targetClass = targetClass; + } + + /** + * Creates a new conversion exception. + * @param value the value we tried to convert + * @param targetClass the target type + * @param message a descriptive message + * @param cause underlying cause of this exception + */ + public ConversionException(Object value, Class targetClass, String message, Throwable cause) { + super(message, cause); + this.value = value; + this.targetClass = targetClass; + } + + /** + * Creates a new conversion exception. + * @param sourceClass the source type + * @param targetClass the target type + * @param message a descriptive message + */ + public ConversionException(Class sourceClass, Class targetClass, String message) { + super(message); + this.sourceClass = sourceClass; + this.value = null; // not available + this.targetClass = targetClass; + } + + /** + * Creates a new conversion exception. + * @param sourceClass the source type + * @param message a descriptive message + */ + public ConversionException(Class sourceClass, String message) { + super(message); + this.sourceClass = sourceClass; + this.value = null; // not available + this.targetClass = null; // not available + } + + /** + * Returns the source type. + */ + public Class getSourceClass() { + return sourceClass; + } + + /** + * Returns the value we tried to convert. + */ + public Object getValue() { + return value; + } + + /** + * Returns the target type. + */ + public Class getTargetClass() { + return targetClass; + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/ConversionExecutor.java b/spring-binding/src/main/java/org/springframework/binding/convert/ConversionExecutor.java new file mode 100644 index 00000000..3fd96d38 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/ConversionExecutor.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert; + +import java.io.Serializable; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; + +/** + * A command object that is parameterized with the information necessary to + * perform a conversion of a source input to a target output. + *

+ * Specifically, encapsulates knowledge about how to convert source objects to a + * specific target type using a specific converter. + * + * @author Keith Donald + */ +public class ConversionExecutor implements Serializable { + + /** + * The source value type this executor will attempt to convert from. + */ + private final Class sourceClass; + + /** + * The target value type this executor will attempt to convert to. + */ + private final Class targetClass; + + /** + * The converter that will perform the conversion. + */ + private final Converter converter; + + /** + * Creates a conversion executor. + * @param sourceClass the source type that the converter will convert from + * @param targetClass the target type that the converter will convert to + * @param converter the converter that will perform the conversion + */ + public ConversionExecutor(Class sourceClass, Class targetClass, Converter converter) { + Assert.notNull(sourceClass, "The source class is required"); + Assert.notNull(targetClass, "The target class is required"); + Assert.notNull(converter, "The converter is required"); + this.sourceClass = sourceClass; + this.targetClass = targetClass; + this.converter = converter; + } + + /** + * Returns the source class of conversions performed by this executor. + * @return the source class + */ + public Class getSourceClass() { + return sourceClass; + } + + /** + * Returns the target class of conversions performed by this executor. + * @return the target class + */ + public Class getTargetClass() { + return targetClass; + } + + /** + * Returns the converter that will perform the conversion. + * @return the converter + */ + public Converter getConverter() { + return converter; + } + + /** + * Execute the conversion for the provided source object. + * @param source the source object to convert + */ + public Object execute(Object source) throws ConversionException { + return execute(source, null); + } + + /** + * Execute the conversion for the provided source object. + * @param source the source object to convert + * @param context the conversion context, useful for influencing the + * behavior of the converter + */ + public Object execute(Object source, ConversionContext context) throws ConversionException { + if (source != null) { + Assert.isInstanceOf(sourceClass, source, "Not of source type: "); + } + return converter.convert(source, targetClass, context); + } + + public boolean equals(Object o) { + if (!(o instanceof ConversionExecutor)) { + return false; + } + ConversionExecutor other = (ConversionExecutor)o; + return sourceClass.equals(other.sourceClass) && targetClass.equals(other.targetClass); + } + + public int hashCode() { + return sourceClass.hashCode() + targetClass.hashCode(); + } + + public String toString() { + return new ToStringCreator(this).append("sourceClass", sourceClass).append("targetClass", targetClass) + .toString(); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/ConversionService.java b/spring-binding/src/main/java/org/springframework/binding/convert/ConversionService.java new file mode 100644 index 00000000..beaaf8d1 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/ConversionService.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert; + +/** + * A service interface for retrieving type conversion executors. The returned + * command objects are thread-safe and may be safely cached for use by client + * code. + * + * @author Keith Donald + */ +public interface ConversionService { + + /** + * Return a conversion executor command object capable of converting source + * objects of the specified sourceClass to instances of the + * targetClass. + *

+ * The returned ConversionExecutor is thread-safe and may safely be cached + * for use in client code. + * @param sourceClass the source class to convert from + * @param targetClass the target class to convert to + * @return the executor that can execute instance conversion, never null + * @throws ConversionException an exception occured retrieving a converter + * for the source-to-target pair + */ + public ConversionExecutor getConversionExecutor(Class sourceClass, Class targetClass) + throws ConversionException; + + /** + * Return a conversion executor command object capable of converting source + * objects of the specified sourceClass to target objects of + * the type associated with the specified alias. + * @param sourceClass the sourceClass + * @param targetAlias the target alias + * @return the conversion executor, or null if the alias cannot be found + * @throws ConversionException an exception occured retrieving a converter + * for the source-to-target pair + */ + public ConversionExecutor getConversionExecutorByTargetAlias(Class sourceClass, String targetAlias) + throws ConversionException; + + /** + * Return all conversion executors capable of converting source objects of + * the the specified sourceClass. + * @param sourceClass the source class to convert from + * @return the matching conversion executors + * @throws ConversionException an exception occured retrieving the converters + */ + public ConversionExecutor[] getConversionExecutorsForSource(Class sourceClass) + throws ConversionException; + + /** + * Return the class with the specified alias. + * @param alias the class alias + * @return the class, or null if not aliased + * @throws ConversionException when an error occurs looking up the class by alias + */ + public Class getClassByAlias(String alias) throws ConversionException; + +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/Converter.java b/spring-binding/src/main/java/org/springframework/binding/convert/Converter.java new file mode 100644 index 00000000..ee43a3d5 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/Converter.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert; + +/** + * A type converter converts objects from one type to another. They may support + * conversion of multiple source types to multiple target types. + *

+ * Implementations of this interface are thread-safe. + * + * @author Keith Donald + */ +public interface Converter { + + /** + * The source classes this converter can convert from. + * @return the supported source classes + */ + public Class[] getSourceClasses(); + + /** + * The target classes this converter can convert to. + * @return the supported target classes + */ + public Class[] getTargetClasses(); + + /** + * Convert the provided source object argument to an instance of the + * specified target class. + * @param source the source object to convert, its class must be one of the + * supported sourceClasses + * @param targetClass the target class to convert the source to, must be one + * of the supported targetClasses + * @param context an optional conversion context that may be used to + * influence the conversion process + * @return the converted object, an instance of the target type + * @throws ConversionException an exception occured during the conversion + */ + public Object convert(Object source, Class targetClass, ConversionContext context) throws ConversionException; + +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/package.html b/spring-binding/src/main/java/org/springframework/binding/convert/package.html new file mode 100644 index 00000000..b26447a3 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/package.html @@ -0,0 +1,7 @@ + + +

+Core services for converting objects from one type to another. +

+ + \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/AbstractConverter.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/AbstractConverter.java new file mode 100644 index 00000000..614756fb --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/AbstractConverter.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import org.springframework.binding.convert.ConversionContext; +import org.springframework.binding.convert.ConversionException; +import org.springframework.binding.convert.Converter; + +/** + * Base class for converters provided as a convenience to implementors. + * + * @author Keith Donald + */ +public abstract class AbstractConverter implements Converter { + + /** + * Convenience convert method that converts the provided source to the first + * target object supported by this converter. Useful when a converter only + * supports conversion to a single target. + * @param source the source to convert + * @return the converted object + * @throws ConversionException an exception occured converting the source + * value + */ + public Object convert(Object source) throws ConversionException { + return convert(source, getTargetClasses()[0], null); + } + + /** + * Convenience convert method that converts the provided source to the + * target class specified with an empty conversion context. + * @param source the source to convert + * @param targetClass the target class to convert the source to, must be one + * of the supported targetClasses + * @return the converted object + * @throws ConversionException an exception occured converting the source + * value + */ + public Object convert(Object source, Class targetClass) throws ConversionException { + return convert(source, targetClass, null); + } + + /** + * Convenience convert method that converts the provided source to the first + * target object supported by this converter. Useful when a converter only + * supports conversion to a single target. + * @param source the source to convert + * @param context the conversion context, useful for influencing the + * behavior of the converter + * @return the converted object + * @throws ConversionException an exception occured converting the source + * value + */ + public Object convert(Object source, ConversionContext context) throws ConversionException { + return convert(source, getTargetClasses()[0], context); + } + + public Object convert(Object source, Class targetClass, ConversionContext context) throws ConversionException { + try { + return doConvert(source, targetClass, context); + } + catch (ConversionException e) { + throw e; + } + catch (Throwable e) { + // wrap in a ConversionException + if (targetClass == null) { + targetClass = getTargetClasses()[0]; + } + throw new ConversionException(source, targetClass, e); + } + } + + /** + * Template method subclasses should override to actually perform the type + * conversion. + * @param source the source to convert from + * @param targetClass the target type to convert to + * @param context an optional conversion context that may be used to + * influence the conversion process, could be null + * @return the converted source value + * @throws Exception an exception occured, will be wrapped in a conversion + * exception if necessary + */ + protected abstract Object doConvert(Object source, Class targetClass, ConversionContext context) throws Exception; + +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/AbstractFormattingConverter.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/AbstractFormattingConverter.java new file mode 100644 index 00000000..83508c86 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/AbstractFormattingConverter.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import org.springframework.binding.format.FormatterFactory; + +/** + * A converter that delegates to a formatter to perform the conversion. + * Formatters are typically not thread safe, so we use a FormatterFactory that + * is expected to provide us with thread-safe instances as necessary. + * + * @author Keith Donald + */ +public abstract class AbstractFormattingConverter extends AbstractConverter { + + /** + * The formatter factory. + */ + private FormatterFactory formatterFactory; + + /** + * Creates a new converter that delegates to a formatter. + * @param formatterFactory the factory to use + */ + protected AbstractFormattingConverter(FormatterFactory formatterFactory) { + setFormatterFactory(formatterFactory); + } + + protected FormatterFactory getFormatterFactory() { + return formatterFactory; + } + + public void setFormatterFactory(FormatterFactory formatterSource) { + this.formatterFactory = formatterSource; + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/CompositeConversionService.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/CompositeConversionService.java new file mode 100644 index 00000000..7ba6b28f --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/CompositeConversionService.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.springframework.binding.convert.ConversionException; +import org.springframework.binding.convert.ConversionExecutor; +import org.springframework.binding.convert.ConversionService; +import org.springframework.util.Assert; + +/** + * A conversion service that delegates to an ordered chain of other conversion + * services. The first correct reply received from a conversion service in + * the chain is returned to the caller. + * + * @author Erwin Vervaet + */ +public class CompositeConversionService implements ConversionService { + + private ConversionService[] chain; + + /** + * Create a new composite conversion service. + * @param conversionServices the conversion services in the chain + */ + public CompositeConversionService(ConversionService[] conversionServices) { + Assert.notNull(conversionServices, "The conversion services chain is required"); + this.chain = conversionServices; + } + + /** + * Returns the conversion services in the chain managed by this + * composite conversion service. + */ + public ConversionService[] getConversionServices() { + return chain; + } + + public ConversionExecutor getConversionExecutor(Class sourceClass, Class targetClass) + throws ConversionException { + for (int i = 0; i < chain.length; i++) { + try { + return chain[i].getConversionExecutor(sourceClass, targetClass); + } + catch (ConversionException e) { + // ignore and try the next conversion service in the chain + } + } + throw new ConversionException(sourceClass, targetClass, + "No converter registered to convert from sourceClass '" + sourceClass + + "' to target class '" + targetClass + "'"); + } + + public ConversionExecutor getConversionExecutorByTargetAlias(Class sourceClass, String targetAlias) + throws ConversionException { + boolean exceptionThrown = false; + for (int i = 0; i < chain.length; i++) { + try { + ConversionExecutor res = chain[i].getConversionExecutorByTargetAlias(sourceClass, targetAlias); + if (res != null) { + return res; + } + } + catch (ConversionException e) { + exceptionThrown = true; + } + } + if (exceptionThrown) { + throw new ConversionException(sourceClass, + "No converter registered to convert from sourceClass '" + sourceClass + + "' to aliased target type '" + targetAlias + "'"); + } + else { + // alias was not recognized by any conversion service in the chain + return null; + } + } + + public ConversionExecutor[] getConversionExecutorsForSource(Class sourceClass) + throws ConversionException { + Set executors = new HashSet(); + for (int i = 0; i < chain.length; i++) { + executors.addAll(Arrays.asList(chain[i].getConversionExecutorsForSource(sourceClass))); + } + return (ConversionExecutor[])executors.toArray(new ConversionExecutor[executors.size()]); + } + + public Class getClassByAlias(String alias) throws ConversionException { + for (int i = 0; i < chain.length; i++) { + try { + Class res = chain[i].getClassByAlias(alias); + if (res != null) { + return res; + } + } + catch (ConversionException e) { + // ignore and try the next conversion service in the chain + } + } + return null; + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/ConversionServiceAware.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/ConversionServiceAware.java new file mode 100644 index 00000000..4971924e --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/ConversionServiceAware.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import org.springframework.binding.convert.ConversionService; + +/** + * Marker interface that denotes an object has a dependency on a conversion + * service that is expected to be fulfilled. + * + * @author Keith Donald + */ +public interface ConversionServiceAware { + + /** + * Set the conversion service this object should be made aware of (as it + * presumably depends on it). + * + * @param conversionService the conversion service + */ + public void setConversionService(ConversionService conversionService); +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/ConversionServiceAwareConverter.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/ConversionServiceAwareConverter.java new file mode 100644 index 00000000..eeb270b6 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/ConversionServiceAwareConverter.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import org.springframework.binding.convert.ConversionExecutor; +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.expression.Expression; + +/** + * Base class for converters that use other converters to convert things, thus + * they are conversion-service aware. + * + * @author Keith Donald + */ +public abstract class ConversionServiceAwareConverter extends AbstractConverter implements ConversionServiceAware { + + /** + * The conversion service this converter is aware of. + */ + private ConversionService conversionService; + + /** + * Default constructor, expectes to conversion service to be injected + * using {@link #setConversionService(ConversionService)}. + */ + protected ConversionServiceAwareConverter() { + } + + /** + * Create a converter using given conversion service. + */ + protected ConversionServiceAwareConverter(ConversionService conversionService) { + setConversionService(conversionService); + } + + /** + * Returns the conversion service used. + */ + public ConversionService getConversionService() { + if (conversionService == null) { + throw new IllegalStateException("Conversion service not yet set: set it first before calling this method"); + } + return conversionService; + } + + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * Returns a conversion executor capable of converting string objects to the + * specified target class. + * @param targetClass the target class + * @return the conversion executor, never null + */ + protected ConversionExecutor fromStringTo(Class targetClass) { + return getConversionService().getConversionExecutor(String.class, targetClass); + } + + /** + * Returns a conversion executor capable of converting string objects to the + * target class aliased by the provided alias. + * @param targetAlias the target class alias, e.g "long" or "float" + * @return the conversion executor, or null if no suitable + * converter exists for alias + */ + protected ConversionExecutor fromStringToAliased(String targetAlias) { + return getConversionService().getConversionExecutorByTargetAlias(String.class, targetAlias); + } + + /** + * Returns a conversion executor capable of converting objects from one + * class to another. + * @param sourceClass the source class to convert from + * @param targetClass the target class to convert to + * @return the conversion executor, never null + */ + protected ConversionExecutor converterFor(Class sourceClass, Class targetClass) { + return getConversionService().getConversionExecutor(sourceClass, targetClass); + } + + /** + * Helper that parsers the given expression string into an expression, using + * the installed String->Expression converter. + * @param expressionString the expression string to parse + * @return the parsed, evaluatable expression + */ + protected Expression parseExpression(String expressionString) { + return (Expression)fromStringTo(Expression.class).execute(expressionString); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/ConverterPropertyEditorAdapter.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/ConverterPropertyEditorAdapter.java new file mode 100644 index 00000000..db1260aa --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/ConverterPropertyEditorAdapter.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import java.beans.PropertyEditorSupport; + +import org.springframework.binding.convert.ConversionExecutor; +import org.springframework.util.Assert; + +/** + * Adapts a Converter to the PropertyEditor interface. + *

+ * Note: with a converter, only forward conversion from-string-to-value is + * supported. Value-to-string conversion is not supported. If you need this + * capability, use a Formatter with a FormatterPropertyEditor adapter. + * + * @see org.springframework.binding.format.Formatter + * @see org.springframework.binding.format.support.FormatterPropertyEditor + * + * @author Keith Donald + */ +public class ConverterPropertyEditorAdapter extends PropertyEditorSupport { + + private ConversionExecutor conversionExecutor; + + /** + * Adapt given conversion executor to the PropertyEditor contract. + */ + public ConverterPropertyEditorAdapter(ConversionExecutor conversionExecutor) { + Assert.notNull(conversionExecutor, "A conversion executor is required"); + Assert.isTrue(conversionExecutor.getSourceClass().equals(String.class), + "A string conversion executor is required"); + this.conversionExecutor = conversionExecutor; + } + + /** + * Returns the type strings will be converted to. + */ + public Class getTargetClass() { + return conversionExecutor.getTargetClass(); + } + + public void setAsText(String text) throws IllegalArgumentException { + setValue(conversionExecutor.execute(text)); + } + + public String getAsText() { + throw new UnsupportedOperationException(); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/CustomConverterConfigurer.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/CustomConverterConfigurer.java new file mode 100644 index 00000000..9a12d174 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/CustomConverterConfigurer.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2005 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.binding.convert.ConversionExecutor; +import org.springframework.binding.convert.ConversionService; +import org.springframework.util.Assert; + +/** + * Registers all 'from string' converters known to a conversion service with + * a Spring bean factory. + *

+ * Acts as bean factory post processor, registering property editor adapters for + * each supported conversion with a java.lang.String sourceClass. + * This makes for very convenient use with the Spring container. + * + * @author Keith Donald + */ +public class CustomConverterConfigurer implements BeanFactoryPostProcessor, InitializingBean { + + private ConversionService conversionService; + + /** + * Create a new configurer. + * @param conversionService the conversion service to take converters from + */ + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + public void afterPropertiesSet() throws Exception { + Assert.notNull(conversionService, "The conversion service is required"); + } + + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + ConversionExecutor[] executors = conversionService.getConversionExecutorsForSource(String.class); + for (int i = 0; i < executors.length; i++) { + ConverterPropertyEditorAdapter editor = new ConverterPropertyEditorAdapter(executors[i]); + beanFactory.registerCustomEditor(editor.getTargetClass(), editor); + } + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/DefaultConversionService.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/DefaultConversionService.java new file mode 100644 index 00000000..e4e62d0f --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/DefaultConversionService.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.binding.format.support.SimpleFormatterFactory; +import org.springframework.core.enums.LabeledEnum; + +/** + * Default, local implementation of a conversion service. Will automatically + * register from string converters for a number of standard Java + * types like Class, Number, Boolean and so on. + * + * @author Keith Donald + */ +public class DefaultConversionService extends GenericConversionService { + + /** + * Creates a new default conversion service, installing the default + * converters. + */ + public DefaultConversionService() { + addDefaultConverters(); + } + + /** + * Add all default converters to the conversion service. + */ + protected void addDefaultConverters() { + addConverter(new TextToClass()); + addConverter(new TextToNumber(new SimpleFormatterFactory())); + addConverter(new TextToBoolean()); + addConverter(new TextToLabeledEnum()); + addDefaultAlias(String.class); + addDefaultAlias(Short.class); + addDefaultAlias(Integer.class); + addAlias("int", Integer.class); + addDefaultAlias(Byte.class); + addDefaultAlias(Long.class); + addDefaultAlias(Float.class); + addDefaultAlias(Double.class); + addDefaultAlias(BigInteger.class); + addDefaultAlias(BigDecimal.class); + addDefaultAlias(Boolean.class); + addDefaultAlias(Class.class); + addDefaultAlias(LabeledEnum.class); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/GenericConversionService.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/GenericConversionService.java new file mode 100644 index 00000000..d248013c --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/GenericConversionService.java @@ -0,0 +1,300 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.Set; + +import org.springframework.binding.convert.ConversionException; +import org.springframework.binding.convert.ConversionExecutor; +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.convert.Converter; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Base implementation of a conversion service. Initially empty, e.g. no converters + * are registered by default. + * + * @author Keith Donald + */ +public class GenericConversionService implements ConversionService { + + /** + * An indexed map of converters. Each entry key is a source class that can + * be converted from, and each entry value is a map of target classes that + * can be convertered to, ultimately mapping to a specific converter that + * can perform the source->target conversion. + */ + private Map sourceClassConverters = new HashMap(); + + /** + * A map of string aliases to convertible classes. Allows lookup of + * converters by alias. + */ + private Map aliasMap = new HashMap(); + + /** + * An optional parent conversion service. + */ + private ConversionService parent; + + /** + * Returns the parent of this conversion service. Could be null. + */ + public ConversionService getParent() { + return parent; + } + + /** + * Set the parent of this conversion service. This is optional. + */ + public void setParent(ConversionService parent) { + this.parent = parent; + } + + /** + * Add given converter to this conversion service. If the converter is + * {@link ConversionServiceAware}, it will get the conversion service + * injected. + */ + public void addConverter(Converter converter) { + Class[] sourceClasses = converter.getSourceClasses(); + Class[] targetClasses = converter.getTargetClasses(); + for (int i = 0; i < sourceClasses.length; i++) { + Class sourceClass = sourceClasses[i]; + Map sourceMap = (Map)sourceClassConverters.get(sourceClass); + if (sourceMap == null) { + sourceMap = new HashMap(); + sourceClassConverters.put(sourceClass, sourceMap); + } + for (int j = 0; j < targetClasses.length; j++) { + Class targetClass = targetClasses[j]; + sourceMap.put(targetClass, converter); + } + } + if (converter instanceof ConversionServiceAware) { + ((ConversionServiceAware)converter).setConversionService(this); + } + } + + /** + * Add all given converters. If the converters are + * {@link ConversionServiceAware}, they will get the conversion service + * injected. + */ + public void addConverters(Converter[] converters) { + for (int i = 0; i < converters.length; i++) { + addConverter(converters[i]); + } + } + + /** + * Add given converter with an alias to the conversion service. If the + * converter is {@link ConversionServiceAware}, it will get the conversion + * service injected. + */ + public void addConverter(Converter converter, String alias) { + aliasMap.put(alias, converter); + addConverter(converter); + } + + /** + * Add an alias for given target type. + */ + public void addAlias(String alias, Class targetType) { + aliasMap.put(alias, targetType); + } + + /** + * Generate a conventions based alias for given target type. For instance, + * "java.lang.Boolean" will get the "boolean" alias. + */ + public void addDefaultAlias(Class targetType) { + addAlias(StringUtils.uncapitalize(ClassUtils.getShortName(targetType)), targetType); + } + + public ConversionExecutor getConversionExecutor(Class sourceClass, Class targetClass) throws ConversionException { + Assert.notNull(sourceClass, "The source class to convert from is required"); + Assert.notNull(targetClass, "The target class to convert to is required"); + if (this.sourceClassConverters == null || this.sourceClassConverters.isEmpty()) { + throw new IllegalStateException("No converters have been added to this service's registry"); + } + if (sourceClass.equals(targetClass)) { + return new ConversionExecutor(sourceClass, targetClass, new NoOpConverter(sourceClass, targetClass)); + } + Map sourceTargetConverters = findConvertersForSource(sourceClass); + Converter converter = findTargetConverter(sourceTargetConverters, targetClass); + if (converter != null) { + // we found a converter + return new ConversionExecutor(sourceClass, targetClass, converter); + } + else { + if (parent != null) { + // try the parent + return parent.getConversionExecutor(sourceClass, targetClass); + } + else { + throw new ConversionException(sourceClass, targetClass, + "No converter registered to convert from sourceClass '" + sourceClass + + "' to target class '" + targetClass + "'"); + } + } + } + + public ConversionExecutor getConversionExecutorByTargetAlias(Class sourceClass, String alias) + throws IllegalArgumentException { + Assert.notNull(sourceClass, "The source class to convert from is required"); + Assert.hasText(alias, "The target alias is required and must either be a type alias (e.g 'boolean') " + + "or a generic converter alias (e.g. 'bean') "); + Object targetType = aliasMap.get(alias); + if (targetType == null) { + if (parent != null) { + // try the parent + return parent.getConversionExecutorByTargetAlias(sourceClass, alias); + } + else { + // not aliased + return null; + } + } + else if (targetType instanceof Class) { + return getConversionExecutor(sourceClass, (Class)targetType); + } + else { + Assert.isInstanceOf(Converter.class, targetType, "Not a converter: "); + Converter conv = (Converter)targetType; + return new ConversionExecutor(sourceClass, Object.class, conv); + } + } + + public ConversionExecutor[] getConversionExecutorsForSource(Class sourceClass) { + Assert.notNull(sourceClass, "The source class to convert from is required"); + Map sourceTargetConverters = findConvertersForSource(sourceClass); + if (sourceTargetConverters.isEmpty()) { + if (parent != null) { + // use the parent + return parent.getConversionExecutorsForSource(sourceClass); + } + else { + // no converters for source class + return new ConversionExecutor[0]; + } + } + else { + Set executors = new HashSet(); + if (parent != null) { + executors.addAll(Arrays.asList(parent.getConversionExecutorsForSource(sourceClass))); + } + Iterator it = sourceTargetConverters.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = (Map.Entry)it.next(); + executors.add(new ConversionExecutor(sourceClass, (Class)entry.getKey(), (Converter)entry.getValue())); + } + return (ConversionExecutor[])executors.toArray(new ConversionExecutor[executors.size()]); + } + } + + public Class getClassByAlias(String alias) { + Assert.hasText(alias, "The alias is required and must be a type alias (e.g 'boolean')"); + Object clazz = aliasMap.get(alias); + if (clazz != null) { + Assert.isInstanceOf(Class.class, clazz, "Not a Class alias '" + alias + "': "); + return (Class)clazz; + } + else { + if (parent != null) { + // try parent service + return parent.getClassByAlias(alias); + } + else { + // alias does not index a class, return null + return null; + } + } + } + + // internal helpers + + private Map findConvertersForSource(Class sourceClass) { + LinkedList classQueue = new LinkedList(); + classQueue.addFirst(sourceClass); + while (!classQueue.isEmpty()) { + sourceClass = (Class)classQueue.removeLast(); + Map sourceTargetConverters = (Map)sourceClassConverters.get(sourceClass); + if (sourceTargetConverters != null && !sourceTargetConverters.isEmpty()) { + return sourceTargetConverters; + } + if (!sourceClass.isInterface() && (sourceClass.getSuperclass() != null)) { + classQueue.addFirst(sourceClass.getSuperclass()); + } + // queue up source class's implemented interfaces. + Class[] interfaces = sourceClass.getInterfaces(); + for (int i = 0; i < interfaces.length; i++) { + classQueue.addFirst(interfaces[i]); + } + } + return Collections.EMPTY_MAP; + } + + private Converter findTargetConverter(Map sourceTargetConverters, Class targetClass) { + LinkedList classQueue = new LinkedList(); + classQueue.addFirst(targetClass); + while (!classQueue.isEmpty()) { + targetClass = (Class)classQueue.removeLast(); + Converter converter = (Converter)sourceTargetConverters.get(targetClass); + if (converter != null) { + return converter; + } + if (!targetClass.isInterface() && (targetClass.getSuperclass() != null)) { + classQueue.addFirst(targetClass.getSuperclass()); + } + // queue up target class's implemented interfaces. + Class[] interfaces = targetClass.getInterfaces(); + for (int i = 0; i < interfaces.length; i++) { + classQueue.addFirst(interfaces[i]); + } + } + return null; + } + + // subclassing support + + /** + * Returns an indexed map of converters. Each entry key is a source class that + * can be converted from, and each entry value is a map of target classes that + * can be convertered to, ultimately mapping to a specific converter that can + * perform the source->target conversion. + */ + protected Map getSourceClassConverters() { + return sourceClassConverters; + } + + /** + * Returns a map of known aliases. Each entry key is a String alias and the + * associated value is either a target class or a converter. + */ + protected Map getAliasMap() { + return aliasMap; + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/NoOpConverter.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/NoOpConverter.java new file mode 100644 index 00000000..b17e510f --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/NoOpConverter.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import org.springframework.binding.convert.ConversionContext; + +/** + * Package private converter that is a "no op". + * + * @author Keith Donald + */ +class NoOpConverter extends AbstractConverter { + + private Class sourceClass; + + private Class targetClass; + + /** + * Create a "no op" converter from given source to given target class. + */ + public NoOpConverter(Class sourceClass, Class targetClass) { + this.sourceClass = sourceClass; + this.targetClass = targetClass; + } + + protected Object doConvert(Object source, Class targetClass, ConversionContext context) throws Exception { + return source; + } + + public Class[] getSourceClasses() { + return new Class[] { sourceClass }; + } + + public Class[] getTargetClasses() { + return new Class[] { targetClass }; + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToBoolean.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToBoolean.java new file mode 100644 index 00000000..2bd641e6 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToBoolean.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import org.springframework.binding.convert.ConversionContext; +import org.springframework.util.StringUtils; + +/** + * Converts a textual representation of a boolean object to a Boolean + * instance. + * + * @author Keith Donald + */ +public class TextToBoolean extends AbstractConverter { + + private static final String VALUE_TRUE = "true"; + + private static final String VALUE_FALSE = "false"; + + private static final String VALUE_ON = "on"; + + private static final String VALUE_OFF = "off"; + + private static final String VALUE_YES = "yes"; + + private static final String VALUE_NO = "no"; + + private static final String VALUE_1 = "1"; + + private static final String VALUE_0 = "0"; + + private String trueString; + + private String falseString; + + /** + * Default constructor. No special true or false strings are considered. + */ + public TextToBoolean() { + this(null, null); + } + + /** + * Create a text to boolean converter. Take given special string representations + * of true and false into account. + * @param trueString special true string to consider + * @param falseString special false string to consider + */ + public TextToBoolean(String trueString, String falseString) { + this.trueString = trueString; + this.falseString = falseString; + } + + public Class[] getSourceClasses() { + return new Class[] { String.class }; + } + + public Class[] getTargetClasses() { + return new Class[] { Boolean.class }; + } + + protected Object doConvert(Object source, Class targetClass, ConversionContext context) throws Exception { + String text = (String)source; + if (!StringUtils.hasText(text)) { + return null; + } + else if (this.trueString != null && text.equalsIgnoreCase(this.trueString)) { + return Boolean.TRUE; + } + else if (this.falseString != null && text.equalsIgnoreCase(this.falseString)) { + return Boolean.FALSE; + } + else if (this.trueString == null + && (text.equalsIgnoreCase(VALUE_TRUE) || text.equalsIgnoreCase(VALUE_ON) + || text.equalsIgnoreCase(VALUE_YES) || text.equals(VALUE_1))) { + return Boolean.TRUE; + } + else if (this.falseString == null + && (text.equalsIgnoreCase(VALUE_FALSE) || text.equalsIgnoreCase(VALUE_OFF) + || text.equalsIgnoreCase(VALUE_NO) || text.equals(VALUE_0))) { + return Boolean.FALSE; + } + else { + throw new IllegalArgumentException("Invalid boolean value [" + text + "]"); + } + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToClass.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToClass.java new file mode 100644 index 00000000..e0e8d838 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToClass.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import org.springframework.binding.convert.ConversionContext; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * Converts a textual representation of a class object to a Class + * instance. + * + * @author Keith Donald + */ +public class TextToClass extends ConversionServiceAwareConverter { + + private static final String ALIAS_PREFIX = "type:"; + + private static final String CLASS_PREFIX = "class:"; + + public Class[] getSourceClasses() { + return new Class[] { String.class }; + } + + public Class[] getTargetClasses() { + return new Class[] { Class.class }; + } + + protected Object doConvert(Object source, Class targetClass, ConversionContext context) throws Exception { + String text = (String)source; + if (StringUtils.hasText(text)) { + String classNameOrAlias = text.trim(); + if (classNameOrAlias.startsWith(CLASS_PREFIX)) { + return ClassUtils.forName(text.substring(CLASS_PREFIX.length())); + } + else if (classNameOrAlias.startsWith(ALIAS_PREFIX)) { + String alias = text.substring(ALIAS_PREFIX.length()); + Class clazz = getConversionService().getClassByAlias(alias); + Assert.notNull(clazz, "No class found associated with type alias '" + alias + "'"); + return clazz; + } + else { + // try first an aliased based lookup + if (getConversionService() != null) { + Class aliasedClass = getConversionService().getClassByAlias(text); + if (aliasedClass != null) { + return aliasedClass; + } + } + // treat as a class name + return ClassUtils.forName(classNameOrAlias); + } + } + else { + return null; + } + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToExpression.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToExpression.java new file mode 100644 index 00000000..d08b3bee --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToExpression.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import org.springframework.binding.convert.ConversionContext; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.binding.expression.SettableExpression; +import org.springframework.binding.expression.support.StaticExpression; +import org.springframework.util.Assert; + +/** + * Converter that converts a String into an Expression object. + * + * @see org.springframework.binding.expression.Expression + * @see org.springframework.binding.expression.SettableExpression + * + * @author Erwin Vervaet + */ +public class TextToExpression extends AbstractConverter { + + /** + * The expression string parser. + */ + private ExpressionParser expressionParser; + + /** + * Creates a new string-to-expression converter. + * @param expressionParser the expression string parser + */ + public TextToExpression(ExpressionParser expressionParser) { + Assert.notNull(expressionParser, "The expression parser is required"); + this.expressionParser = expressionParser; + } + + /** + * Returns the expression parser used by this converter. + */ + public ExpressionParser getExpressionParser() { + return expressionParser; + } + + public Class[] getSourceClasses() { + return new Class[] { String.class }; + } + + public Class[] getTargetClasses() { + return new Class[] { Expression.class, SettableExpression.class }; + } + + protected Object doConvert(Object source, Class targetClass, ConversionContext context) throws Exception { + String expressionString = (String)source; + if (getExpressionParser().isDelimitedExpression(expressionString)) { + return getExpressionParser().parseExpression((String)source); + } + else { + return new StaticExpression(expressionString); + } + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToLabeledEnum.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToLabeledEnum.java new file mode 100644 index 00000000..99d7d236 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToLabeledEnum.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import org.springframework.binding.convert.ConversionContext; +import org.springframework.binding.format.support.LabeledEnumFormatter; +import org.springframework.core.enums.LabeledEnum; + +/** + * Converter that converts textual representations of enum + * instances to a specific instance of LabeledEnum. + * + * @author Keith Donald + */ +public class TextToLabeledEnum extends AbstractConverter { + + private LabeledEnumFormatter labeledEnumFormatter = new LabeledEnumFormatter(); + + public Class[] getSourceClasses() { + return new Class[] { String.class }; + } + + public Class[] getTargetClasses() { + return new Class[] { LabeledEnum.class }; + } + + protected Object doConvert(Object source, Class targetClass, ConversionContext context) throws Exception { + return labeledEnumFormatter.parseValue((String)source, targetClass); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToNumber.java b/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToNumber.java new file mode 100644 index 00000000..c13c314f --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/TextToNumber.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import java.math.BigDecimal; +import java.math.BigInteger; + +import org.springframework.binding.convert.ConversionContext; +import org.springframework.binding.format.FormatterFactory; +import org.springframework.binding.format.support.SimpleFormatterFactory; + +/** + * Converts textual representations of numbers to a Number + * specialization. Delegates to a synchronized formatter to parse text strings. + * + * @author Keith Donald + */ +public class TextToNumber extends AbstractFormattingConverter { + + /** + * Default constructor that uses a {@link SimpleFormatterFactory}. + */ + public TextToNumber() { + super(new SimpleFormatterFactory()); + } + + /** + * Create a string to number converter using given formatter factory. + * @param formatterFactory the factory to use + */ + public TextToNumber(FormatterFactory formatterFactory) { + super(formatterFactory); + } + + public Class[] getSourceClasses() { + return new Class[] { String.class }; + } + + public Class[] getTargetClasses() { + return new Class[] { Integer.class, Short.class, Byte.class, Long.class, Float.class, Double.class, + BigInteger.class, BigDecimal.class }; + } + + protected Object doConvert(Object source, Class targetClass, ConversionContext context) throws Exception { + return getFormatterFactory().getNumberFormatter(targetClass).parseValue((String)source, targetClass); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/convert/support/package.html b/spring-binding/src/main/java/org/springframework/binding/convert/support/package.html new file mode 100644 index 00000000..17e7746e --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/convert/support/package.html @@ -0,0 +1,7 @@ + + +

+Supporting type converter implementations that are generically applicable and frequently used. +

+ + \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/EvaluationAttempt.java b/spring-binding/src/main/java/org/springframework/binding/expression/EvaluationAttempt.java new file mode 100644 index 00000000..e1c73ce5 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/EvaluationAttempt.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression; + +import java.io.Serializable; + +import org.springframework.core.style.ToStringCreator; + +/** + * A simple holder for information about an evaluation attempt. + * + * @author Keith Donald + */ +public class EvaluationAttempt implements Serializable { + + /** + * The expression that attempted to evaluate. + */ + private Expression expression; + + /** + * The target object being evaluated. + */ + private Object target; + + /** + * The evaluation context. + */ + private EvaluationContext context; + + /** + * Create an evaluation attempt. + * @param expression the expression that failed to evaluate + * @param target the target of the expression + * @param context the context attributes that might have affected evaluation behavior + */ + public EvaluationAttempt(Expression expression, Object target, EvaluationContext context) { + this.expression = expression; + this.target = target; + this.context = context; + } + + /** + * Returns the expression that attempted to evaluate. + */ + public Expression getExpression() { + return expression; + } + + /** + * Returns the target object upon which evaluation was attempted. + */ + public Object getTarget() { + return target; + } + + /** + * Returns context attributes that may have influenced the evaluation process. + */ + public EvaluationContext getContext() { + return context; + } + + public String toString() { + return createToString(new ToStringCreator(this)).toString(); + } + + protected ToStringCreator createToString(ToStringCreator creator) { + return creator.append("expression", expression).append("target", target).append("context", + context); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/EvaluationContext.java b/spring-binding/src/main/java/org/springframework/binding/expression/EvaluationContext.java new file mode 100644 index 00000000..33ebfdcb --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/EvaluationContext.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression; + +import java.util.Map; + +/** + * A context object with two main responsibities: + *
    + *
  1. Exposing information to an expression to influence + * an evaluation attempt. + *
  2. Providing operations for recording progress or + * errors during the expression evaluation process. + *
+ * + * @author Keith Donald + */ +public interface EvaluationContext { + + /** + * Returns a map of attributes that can be used to influence expression evaluation. + * @return the evaluation attributes + */ + public Map getAttributes(); + +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/EvaluationException.java b/spring-binding/src/main/java/org/springframework/binding/expression/EvaluationException.java new file mode 100644 index 00000000..ecb3ee4f --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/EvaluationException.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression; + +import org.springframework.core.NestedRuntimeException; + +/** + * Indicates an expression evaluation failed. + * + * @author Keith Donald + */ +public class EvaluationException extends NestedRuntimeException { + + /** + * The evaluation attempt that failed. + */ + private EvaluationAttempt evaluationAttempt; + + /** + * Creates a new evaluation exception. + * @param evaluationAttempt the evaluation attempt that failed + * @param cause the underlying cause of this exception + */ + public EvaluationException(EvaluationAttempt evaluationAttempt, Throwable cause) { + super("Expression " + evaluationAttempt + + " failed - make sure the expression is evaluatable on the target object", cause); + this.evaluationAttempt = evaluationAttempt; + } + + /** + * Returns the evaluation attempt that failed. + */ + public EvaluationAttempt getEvaluationAttempt() { + return evaluationAttempt; + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/Expression.java b/spring-binding/src/main/java/org/springframework/binding/expression/Expression.java new file mode 100644 index 00000000..3f4a1765 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/Expression.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression; + +/** + * Evaluates a single parsed expression on the provided input object in the + * specified context. This provides a common abstraction for expression + * evaluation independent of any language like OGNL or Spring's BeanWrapper. + * + * @author Keith Donald + */ +public interface Expression { + + /** + * Evaluate the expression encapsulated by this evaluator against the + * provided target object and return the result of the evaluation. + * @param target the target of the expression + * @param context the expression evaluation context + * @return the evaluation result + * @throws EvaluationException an exception occured during evaluation + */ + public Object evaluate(Object target, EvaluationContext context) throws EvaluationException; +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/ExpressionParser.java b/spring-binding/src/main/java/org/springframework/binding/expression/ExpressionParser.java new file mode 100644 index 00000000..620021a6 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/ExpressionParser.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression; + +/** + * Parses expression strings, returing a configured evaluator instance capable + * of performing parsed expression evaluation in a thread safe way. + * + * @author Keith Donald + */ +public interface ExpressionParser { + + /** + * Is this expression string delimited in a manner that indicates it is a + * parseable expression? For example "${expression}". + * @param expressionString the proposed expression string + * @return true if yes, false if not + */ + public boolean isDelimitedExpression(String expressionString); + + /** + * Parse the provided expression string, returning an evaluator capable of + * evaluating it against input. + * @param expressionString the parseable expression string + * @return the evaluator for the parsed expression + * @throws ParserException an exception occured during parsing + */ + public Expression parseExpression(String expressionString) throws ParserException; + + /** + * Parse the provided settable expression string, returning an evaluator + * capable of evaluating its value as well as setting its value. + * @param expressionString the parseable expression string + * @return the evaluator for the parsed expression + * @throws ParserException an exception occured during parsing + * @throws UnsupportedOperationException this parser does not support + * settable expressions + */ + public SettableExpression parseSettableExpression(String expressionString) throws ParserException, + UnsupportedOperationException; + +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/ParserException.java b/spring-binding/src/main/java/org/springframework/binding/expression/ParserException.java new file mode 100644 index 00000000..56590127 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/ParserException.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression; + +import org.springframework.core.NestedRuntimeException; + +/** + * Base class for exceptions thrown during expression parsing. + * + * @author Keith Donald + */ +public class ParserException extends NestedRuntimeException { + + /** + * The expression string that could not be parsed. + */ + private String expressionString; + + /** + * Creates a new expression parsing exception. + * @param expressionString the expression string that could not be parsed + * @param cause the underlying cause of this exception + */ + public ParserException(String expressionString, Throwable cause) { + this(expressionString, "Unable to parse expression string '" + expressionString + "'", cause); + } + + /** + * Creates a new expression parsing exception. + * @param expressionString the expression string that could not be parsed + * @param message a descriptive message + * @param cause the underlying cause of this exception + */ + public ParserException(String expressionString, String message, Throwable cause) { + super(message, cause); + this.expressionString = expressionString; + } + + /** + * Returns the expression string that could not be parsed. + */ + public Object getExpressionString() { + return expressionString; + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/SetValueAttempt.java b/spring-binding/src/main/java/org/springframework/binding/expression/SetValueAttempt.java new file mode 100644 index 00000000..8aabbafb --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/SetValueAttempt.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression; + +import org.springframework.core.style.ToStringCreator; + +/** + * Records an attempt to set an expression value. + * + * @author Keith Donald + */ +public class SetValueAttempt extends EvaluationAttempt { + + /** + * The new value. + */ + private Object value; + + /** + * Creates a new set attempt. + * @param expression the settable expression + * @param target the target of the expression + * @param value the value that was attempted to be set + * @param context context attributes that may have influenced the evaluation and set process + */ + public SetValueAttempt(SettableExpression expression, Object target, Object value, EvaluationContext context) { + super(expression, target, context); + this.value = value; + } + + /** + * Returns the value that was attempted to be set. + */ + public Object getValue() { + return value; + } + + protected ToStringCreator createToString(ToStringCreator creator) { + return super.createToString(creator).append("value", value); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/SettableExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/SettableExpression.java new file mode 100644 index 00000000..f11b1dd2 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/SettableExpression.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression; + +/** + * An evaluator that is capable of setting a value on a target object at the + * path defined by this expression. + * + * @author Keith Donald + */ +public interface SettableExpression extends Expression { + + /** + * Evaluate this expression against the target object to set its value to + * the value provided. + * @param target the target object + * @param value the new value to be set + * @param context the evaluation context + * @throws EvaluationException an exception occured during evaluation + */ + public void evaluateToSet(Object target, Object value, EvaluationContext context) throws EvaluationException; +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/package.html b/spring-binding/src/main/java/org/springframework/binding/expression/package.html new file mode 100644 index 00000000..6c863f90 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/package.html @@ -0,0 +1,7 @@ + + +

+Core expression language abstraction for parsing and evaluating expressions. +

+ + \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/AbstractExpressionParser.java b/spring-binding/src/main/java/org/springframework/binding/expression/support/AbstractExpressionParser.java new file mode 100644 index 00000000..3c734dea --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/AbstractExpressionParser.java @@ -0,0 +1,185 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression.support; + +import java.util.LinkedList; +import java.util.List; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.binding.expression.ParserException; +import org.springframework.binding.expression.SettableExpression; +import org.springframework.util.StringUtils; + +/** + * Abstract base class for expression parsers. + * + * @author Keith Donald + */ +public abstract class AbstractExpressionParser implements ExpressionParser { + + /** + * The expression prefix. + */ + private static final String DEFAULT_EXPRESSION_PREFIX = "${"; + + /** + * The expression suffix. + */ + private static final String DEFAULT_EXPRESSION_SUFFIX = "}"; + + /** + * The marked expression delimter prefix. + */ + private String expressionPrefix = DEFAULT_EXPRESSION_PREFIX; + + /** + * The marked expression delimiter suffix. + */ + private String expressionSuffix = DEFAULT_EXPRESSION_SUFFIX; + + /** + * Returns the configured expression delimiter prefix. Defaults to "${". + */ + public String getExpressionPrefix() { + return expressionPrefix; + } + + /** + * Sets the expression delimiter prefix. + */ + public void setExpressionPrefix(String expressionPrefix) { + this.expressionPrefix = expressionPrefix; + } + + /** + * Returns the expression delimiter suffix. Defaults to "}". + */ + public String getExpressionSuffix() { + return expressionSuffix; + } + + /** + * Sets the expression delimiter suffix. + */ + public void setExpressionSuffix(String expressionSuffix) { + this.expressionSuffix = expressionSuffix; + } + + /** + * Check whether or not given criteria are expressed as an expression. + */ + public boolean isDelimitedExpression(String expressionString) { + int prefixIndex = expressionString.indexOf(getExpressionPrefix()); + if (prefixIndex == -1) { + return false; + } + int suffixIndex = expressionString.indexOf(getExpressionSuffix(), prefixIndex); + if (suffixIndex == -1) { + return false; + } + else { + if (suffixIndex == prefixIndex + getExpressionPrefix().length()) { + return false; + } + else { + return true; + } + } + } + + public final Expression parseExpression(String expressionString) throws ParserException { + Expression[] expressions = parseExpressions(expressionString); + if (expressions.length == 1) { + return expressions[0]; + } + else { + return new CompositeStringExpression(expressions); + } + } + + public abstract SettableExpression parseSettableExpression(String expressionString) throws ParserException, + UnsupportedOperationException; + + /** + * Helper that parses given expression string using the configured parser. + * The expression string can contain any number of expressions all contained + * in "${...}" markers. For instance: "foo${expr0}bar${expr1}". The static + * pieces of text will also be returned as Expressions that just return that + * static piece of text. As a result, evaluating all returned expressions + * and concating the results produces the complete evaluated string. + * @param expressionString the expression string + * @return the parsed expressions + * @throws ParserException when the expressions cannot be parsed + */ + private Expression[] parseExpressions(String expressionString) throws ParserException { + List expressions = new LinkedList(); + if (StringUtils.hasText(expressionString)) { + int startIdx = 0; + while (startIdx < expressionString.length()) { + int prefixIndex = expressionString.indexOf(getExpressionPrefix(), startIdx); + if (prefixIndex >= startIdx) { + // an expression was found + if (prefixIndex > startIdx) { + expressions.add(new StaticExpression(expressionString.substring(startIdx, prefixIndex))); + startIdx = prefixIndex; + } + int suffixIndex = expressionString.indexOf(getExpressionSuffix(), prefixIndex); + if (suffixIndex == -1) { + throw new ParserException(expressionString, "No ending suffix '" + getExpressionSuffix() + + "' for expression starting at character " + prefixIndex + ": " + + expressionString.substring(prefixIndex), null); + } + else if (suffixIndex == prefixIndex + getExpressionPrefix().length()) { + throw new ParserException(expressionString, "No expression defined within delimiter '" + + getExpressionPrefix() + getExpressionSuffix() + "' at character " + prefixIndex, + null); + } + else { + String expr = expressionString.substring(prefixIndex + getExpressionPrefix().length(), + suffixIndex); + expressions.add(doParseExpression(expr)); + startIdx = suffixIndex + 1; + } + } + else { + if (startIdx == 0) { + // treat entire string as one expression + expressions.add(doParseExpression(expressionString)); + } + else { + // no more ${expressions} found in string + expressions.add(new StaticExpression(expressionString.substring(startIdx))); + } + startIdx = expressionString.length(); + } + } + } + else { + expressions.add(new StaticExpression(expressionString)); + } + return (Expression[]) expressions.toArray(new Expression[expressions.size()]); + } + + /** + * Template method for parsing a filtered expression string. Subclasses should + * override. + * @param expressionString the expression string + * @return the parsed expression + */ + protected abstract Expression doParseExpression(String expressionString); + +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/BeanWrapperExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/support/BeanWrapperExpression.java new file mode 100644 index 00000000..a03b9fd4 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/BeanWrapperExpression.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression.support; + +import org.springframework.beans.BeanWrapperImpl; +import org.springframework.beans.BeansException; +import org.springframework.binding.expression.EvaluationAttempt; +import org.springframework.binding.expression.EvaluationContext; +import org.springframework.binding.expression.EvaluationException; +import org.springframework.binding.expression.SetValueAttempt; +import org.springframework.binding.expression.SettableExpression; +import org.springframework.util.Assert; + +/** + * An expression evaluator that uses the Spring bean wrapper. + * + * @author Keith Donald + */ +class BeanWrapperExpression implements SettableExpression { + + /** + * The expression. + */ + private String expression; + + public BeanWrapperExpression(String expression) { + this.expression = expression; + } + + public int hashCode() { + return expression.hashCode(); + } + + public boolean equals(Object o) { + if (!(o instanceof BeanWrapperExpression)) { + return false; + } + BeanWrapperExpression other = (BeanWrapperExpression)o; + return expression.equals(other.expression); + } + + public Object evaluate(Object target, EvaluationContext context) throws EvaluationException { + try { + return new BeanWrapperImpl(target).getPropertyValue(expression); + } + catch (BeansException e) { + throw new EvaluationException(new EvaluationAttempt(this, target, context), e); + } + } + + public void evaluateToSet(Object target, Object value, EvaluationContext context) throws EvaluationException { + try { + Assert.notNull(target, "The target object to evaluate is required"); + new BeanWrapperImpl(target).setPropertyValue(expression, value); + } + catch (BeansException e) { + throw new EvaluationException(new SetValueAttempt(this, target, value, context), e); + } + } + + public String toString() { + return expression; + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/BeanWrapperExpressionParser.java b/spring-binding/src/main/java/org/springframework/binding/expression/support/BeanWrapperExpressionParser.java new file mode 100644 index 00000000..e77c94fc --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/BeanWrapperExpressionParser.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression.support; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ParserException; +import org.springframework.binding.expression.SettableExpression; + +/** + * An expression parser that parses bean wrapper expressions. + * + * @author Keith + */ +public class BeanWrapperExpressionParser extends AbstractExpressionParser { + + protected Expression doParseExpression(String expressionString) throws ParserException { + return parseSettableExpression(expressionString); + } + + public SettableExpression parseSettableExpression(String expressionString) throws ParserException { + return new BeanWrapperExpression(expressionString); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/CollectionAddingExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/support/CollectionAddingExpression.java new file mode 100644 index 00000000..aae505ea --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/CollectionAddingExpression.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression.support; + +import java.util.Collection; + +import org.springframework.binding.expression.EvaluationContext; +import org.springframework.binding.expression.EvaluationException; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.SetValueAttempt; +import org.springframework.binding.expression.SettableExpression; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; + +/** + * A settable expression that adds non-null values to a collection. + * + * @author Keith Donald + */ +public class CollectionAddingExpression implements SettableExpression { + + /** + * The expression that resolves a mutable collection reference. + */ + private Expression collectionExpression; + + /** + * Creates a collection adding property expression. + * @param collectionExpression the collection expression + */ + public CollectionAddingExpression(Expression collectionExpression) { + this.collectionExpression = collectionExpression; + } + + public Object evaluate(Object target, EvaluationContext context) throws EvaluationException { + return collectionExpression.evaluate(target, context); + } + + public void evaluateToSet(Object target, Object value, EvaluationContext context) throws EvaluationException { + Object result = evaluate(target, context); + if (result == null) { + throw new EvaluationException(new SetValueAttempt(this, target, value, null), + new IllegalArgumentException("The collection expression evaluated to a [null] reference")); + } + Assert.isInstanceOf(Collection.class, result, "Not a collection: "); + if (value != null) { + // add the value to the collection + ((Collection)result).add(value); + } + } + + public String toString() { + return new ToStringCreator(this).append("collectionExpression", collectionExpression).toString(); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/CompositeStringExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/support/CompositeStringExpression.java new file mode 100644 index 00000000..d9a47b35 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/CompositeStringExpression.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression.support; + +import org.springframework.binding.expression.EvaluationContext; +import org.springframework.binding.expression.EvaluationException; +import org.springframework.binding.expression.Expression; +import org.springframework.core.style.ToStringCreator; + +/** + * Evaluates an array of expressions to build a concatenated string. + * + * @author Keith Donald + */ +public class CompositeStringExpression implements Expression { + + /** + * The expression array. + */ + private Expression[] expressions; + + /** + * Creates a new composite string expression. + * @param expressions the ordered set of expressions that when evaluated + * will have their results stringed together to build the composite string + */ + public CompositeStringExpression(Expression[] expressions) { + this.expressions = expressions; + } + + public Object evaluate(Object target, EvaluationContext evaluationContext) throws EvaluationException { + StringBuffer buffer = new StringBuffer(128); + for (int i = 0; i < expressions.length; i++) { + buffer.append(expressions[i].evaluate(target, evaluationContext)); + } + return buffer.toString(); + } + + public String toString() { + return new ToStringCreator(this).append("expressions", expressions).toString(); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/OgnlExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/support/OgnlExpression.java new file mode 100644 index 00000000..74e18cfb --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/OgnlExpression.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression.support; + +import java.util.Collections; +import java.util.Map; + +import ognl.Ognl; +import ognl.OgnlException; + +import org.springframework.binding.expression.EvaluationAttempt; +import org.springframework.binding.expression.EvaluationContext; +import org.springframework.binding.expression.EvaluationException; +import org.springframework.binding.expression.SetValueAttempt; +import org.springframework.binding.expression.SettableExpression; +import org.springframework.util.Assert; + +/** + * Evaluates a parsed Ognl expression. + *

+ * IMPLEMENTATION NOTE: Ognl 2.6.7 expression objects do not respect equality + * properly, so the equality operations defined within this class do not + * function properly. + * + * @author Keith Donald + */ +class OgnlExpression implements SettableExpression { + + /** + * The expression. + */ + private Object expression; + + /** + * Creates a new OGNL expression. + * @param expression the parsed expression + */ + public OgnlExpression(Object expression) { + this.expression = expression; + } + + public int hashCode() { + return expression.hashCode(); + } + + public boolean equals(Object o) { + if (!(o instanceof OgnlExpression)) { + return false; + } + // as late as Ognl 2.6.7, their expression objects don't implement + // equals + // so this always returns false + OgnlExpression other = (OgnlExpression) o; + return expression.equals(other.expression); + } + + public Object evaluate(Object target, EvaluationContext context) throws EvaluationException { + Assert.notNull(target, "The target object to evaluate is required"); + Map contextAttributes = (context != null ? context.getAttributes() : Collections.EMPTY_MAP); + try { + return Ognl.getValue(expression, contextAttributes, target); + } + catch (OgnlException e) { + throw new EvaluationException(new EvaluationAttempt(this, target, context), e); + } + } + + public void evaluateToSet(Object target, Object value, EvaluationContext context) { + Assert.notNull(target, "The target object to evaluate is required"); + Map contextAttributes = (context != null ? context.getAttributes() : Collections.EMPTY_MAP); + try { + Ognl.setValue(expression, contextAttributes, target, value); + } + catch (OgnlException e) { + throw new EvaluationException(new SetValueAttempt(this, target, value, context), e); + } + } + + public String toString() { + return expression.toString(); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/OgnlExpressionParser.java b/spring-binding/src/main/java/org/springframework/binding/expression/support/OgnlExpressionParser.java new file mode 100644 index 00000000..9cae20c2 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/OgnlExpressionParser.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression.support; + +import ognl.Ognl; +import ognl.OgnlException; +import ognl.OgnlRuntime; +import ognl.PropertyAccessor; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ParserException; +import org.springframework.binding.expression.SettableExpression; + +/** + * An expression parser that parses Ognl expressions. + * + * @author Keith Donald + */ +public class OgnlExpressionParser extends AbstractExpressionParser { + + protected Expression doParseExpression(String expressionString) throws ParserException { + return parseSettableExpression(expressionString); + } + + public SettableExpression parseSettableExpression(String expressionString) throws ParserException { + try { + return new OgnlExpression(Ognl.parseExpression(expressionString)); + } + catch (OgnlException e) { + throw new ParserException(expressionString, e); + } + } + + /** + * Add a property access strategy for the given class. + * @param clazz the class that contains properties needing access + * @param propertyAccessor the property access strategy + */ + public void addPropertyAccessor(Class clazz, PropertyAccessor propertyAccessor) { + OgnlRuntime.setPropertyAccessor(clazz, propertyAccessor); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/StaticExpression.java b/spring-binding/src/main/java/org/springframework/binding/expression/support/StaticExpression.java new file mode 100644 index 00000000..cb0de892 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/StaticExpression.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression.support; + +import org.springframework.binding.expression.EvaluationContext; +import org.springframework.binding.expression.EvaluationException; +import org.springframework.binding.expression.Expression; +import org.springframework.util.ObjectUtils; + +/** + * A simple expression evaluator that just returns a fixed result on each + * evaluation. + * + * @author Keith Donald + */ +public class StaticExpression implements Expression { + + /** + * The value expression. + */ + private Object value; + + /** + * Create a static evaluator for the given value. + * @param value the value + */ + public StaticExpression(Object value) { + this.value = value; + } + + public int hashCode() { + if (value == null) { + return 0; + } + else { + return value.hashCode(); + } + } + + public boolean equals(Object o) { + if (!(o instanceof StaticExpression)) { + return false; + } + StaticExpression other = (StaticExpression)o; + return ObjectUtils.nullSafeEquals(value, other.value); + } + + public Object evaluate(Object target, EvaluationContext context) throws EvaluationException { + return value; + } + + public String toString() { + return String.valueOf(value); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/expression/support/package.html b/spring-binding/src/main/java/org/springframework/binding/expression/support/package.html new file mode 100644 index 00000000..4c7960ec --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/expression/support/package.html @@ -0,0 +1,7 @@ + + +

+Expression abstraction implementations, integrated with BeanWrapper and OGNL. +

+ + \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/format/Formatter.java b/spring-binding/src/main/java/org/springframework/binding/format/Formatter.java new file mode 100644 index 00000000..b7192c35 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/format/Formatter.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.format; + +/** + * A lightweight interface for formatting a value and parsing a value from its + * formatted form. + * + * @author Keith Donald + */ +public interface Formatter { + + /** + * Format the value. + * @param value the value to format + * @return the formatted string, fit for display in a UI + * @throws IllegalArgumentException the value could not be formatted + */ + public String formatValue(Object value) throws IllegalArgumentException; + + /** + * Parse the formatted string representation of a value, restoring the + * value. + * @param formattedString the formatted string representation + * @param targetClass the target class to convert the formatted value to + * @return the parsed value + * @throws InvalidFormatException the string was in an invalid form + */ + public Object parseValue(String formattedString, Class targetClass) throws InvalidFormatException; + +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/format/FormatterFactory.java b/spring-binding/src/main/java/org/springframework/binding/format/FormatterFactory.java new file mode 100644 index 00000000..dac25528 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/format/FormatterFactory.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.format; + +import java.text.Format; + +/** + * Source for shared and commonly used Formatters. + *

+ * Formatters are typically not thread safe as Format objects + * aren't thread safe: so implementations of this service should take care to + * synchronize them as neccessary. + * + * @see Format + * + * @author Keith Donald + */ +public interface FormatterFactory { + + /** + * Returns a date formatter for the encoded date format. + * @param encodedFormat the format + * @return the formatter + */ + public Formatter getDateFormatter(String encodedFormat); + + /** + * Returns the default date format for the current locale. + * @return the date formatter + */ + public Formatter getDateFormatter(); + + /** + * Returns the date format with the specified style for the current locale. + * @param style the style + * @return the formatter + */ + public Formatter getDateFormatter(Style style); + + /** + * Returns the default date/time format for the current locale. + * @return the date/time formatter + */ + public Formatter getDateTimeFormatter(); + + /** + * Returns the date format with the specified styles for the current locale. + * @param dateStyle the date style + * @param timeStyle the time style + * @return the formatter + */ + public Formatter getDateTimeFormatter(Style dateStyle, Style timeStyle); + + /** + * Returns the default time format for the current locale. + * @return the time formatter + */ + public Formatter getTimeFormatter(); + + /** + * Returns the time format with the specified style for the current locale. + * @param style the style + * @return the formatter + */ + public Formatter getTimeFormatter(Style style); + + /** + * Returns a number formatter for the specified class. + * @param numberClass the number class + * @return the number formatter + */ + public Formatter getNumberFormatter(Class numberClass); + + /** + * Returns a percent number formatter. + * @return the percent formatter + */ + public Formatter getPercentFormatter(); + + /** + * Returns a currency number formatter. + * @return the currency formatter + */ + public Formatter getCurrencyFormatter(); + +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/format/InvalidFormatException.java b/spring-binding/src/main/java/org/springframework/binding/format/InvalidFormatException.java new file mode 100644 index 00000000..800ef5bc --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/format/InvalidFormatException.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.format; + +import org.springframework.core.NestedRuntimeException; + +/** + * Thrown when a formatted value is of the wrong form. + * + * @author Keith Donald + */ +public class InvalidFormatException extends NestedRuntimeException { + + private String invalidValue; + + private String expectedFormat; + + /** + * Create a new invalid format exception + * @param invalidValue the invalid value + * @param expectedFormat the expected format + */ + public InvalidFormatException(String invalidValue, String expectedFormat) { + this(invalidValue, expectedFormat, (Throwable)null); + } + + /** + * Create a new invalid format exception + * @param invalidValue the invalid value + * @param expectedFormat the expected format + * @param cause the underlying cause of this exception + */ + public InvalidFormatException(String invalidValue, String expectedFormat, Throwable cause) { + super("Invalid format for value " + invalidValue + "; the expected format was '" + expectedFormat + "'", cause); + this.invalidValue = invalidValue; + this.expectedFormat = expectedFormat; + } + + /** + * Create a new invalid format exception + * @param invalidValue the invalid value + * @param expectedFormat the expected format + * @param message a descriptive message + * @param cause the underlying cause of this exception + */ + public InvalidFormatException(String invalidValue, String expectedFormat, String message, Throwable cause) { + super(message, cause); + this.invalidValue = invalidValue; + this.expectedFormat = expectedFormat; + } + + /** + * Returns the invalid value. + */ + public String getInvalidValue() { + return invalidValue; + } + + /** + * Returns the expected format. + */ + public String getExpectedFormat() { + return expectedFormat; + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/format/Style.java b/spring-binding/src/main/java/org/springframework/binding/format/Style.java new file mode 100644 index 00000000..8b937513 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/format/Style.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.format; + +import org.springframework.core.enums.StaticLabeledEnum; + +/** + * Format styles. + * @author Keith Donald + */ +public class Style extends StaticLabeledEnum { + + public static final Style FULL = new Style(0, "Full"); + + public static final Style LONG = new Style(1, "Long"); + + public static final Style MEDIUM = new Style(2, "Medium"); + + public static final Style SHORT = new Style(3, "Short"); + + /** + * Private constructor since this is a type-safe enum. + */ + private Style(int code, String label) { + super(code, label); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/format/package.html b/spring-binding/src/main/java/org/springframework/binding/format/package.html new file mode 100644 index 00000000..b17d1908 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/format/package.html @@ -0,0 +1,7 @@ + + +

+Core services for formatting objects in string form. +

+ + \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/format/support/AbstractFormatter.java b/spring-binding/src/main/java/org/springframework/binding/format/support/AbstractFormatter.java new file mode 100644 index 00000000..c15320cc --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/format/support/AbstractFormatter.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.format.support; + +import java.text.ParseException; + +import org.springframework.binding.format.Formatter; +import org.springframework.binding.format.InvalidFormatException; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Abstract base class for all formatters. + * + * @author Keith Donald + */ +public abstract class AbstractFormatter implements Formatter { + + /** + * Does this formatter allow empty values? + */ + private boolean allowEmpty = true; + + /** + * Constructs a formatter. + */ + protected AbstractFormatter() { + } + + /** + * Constructs a formatter. + * @param allowEmpty allow formatting of empty (null or blank) values? + */ + protected AbstractFormatter(boolean allowEmpty) { + this.allowEmpty = allowEmpty; + } + + /** + * Allow formatting of empty (null or blank) values? + */ + public boolean isAllowEmpty() { + return allowEmpty; + } + + public final String formatValue(Object value) { + if (allowEmpty && isEmpty(value)) { + return getEmptyFormattedValue(); + } + Assert.isTrue(!isEmpty(value), "Object to format cannot be empty"); + return doFormatValue(value); + } + + /** + * Template method subclasses should override to encapsulate formatting + * logic. + * @param value the value to format + * @return the formatted string representation + */ + protected abstract String doFormatValue(Object value); + + /** + * Returns the formatted form of an empty value. Default implementation + * just returns the empty string. + */ + protected String getEmptyFormattedValue() { + return ""; + } + + public final Object parseValue(String formattedString, Class targetClass) throws InvalidFormatException { + try { + if (allowEmpty && isEmpty(formattedString)) { + return getEmptyValue(); + } + return doParseValue(formattedString, targetClass); + } + catch (ParseException ex) { + throw new InvalidFormatException(formattedString, getExpectedFormat(targetClass), ex); + } + } + + /** + * Template method subclasses should override to encapsulate parsing logic. + * @param formattedString the formatted string to parse + * @return the parsed value + * @throws InvalidFormatException an exception occured parsing + * @throws ParseException when parse exceptions occur + */ + protected abstract Object doParseValue(String formattedString, Class targetClass) throws InvalidFormatException, + ParseException; + + /** + * Returns the empty value (resulting from parsing an empty input string). + * This default implementation just returns null. + */ + protected Object getEmptyValue() { + return null; + } + + /** + * Returns the expected string format for the given target class. + * The default implementation just returns null. + */ + protected String getExpectedFormat(Class targetClass) { + return null; + } + + /** + * Is given object empty (null or empty string)? + */ + protected boolean isEmpty(Object o) { + if (o == null) { + return true; + } + else if (o instanceof String) { + return !StringUtils.hasText((String)o); + } + else { + return false; + } + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/format/support/AbstractFormatterFactory.java b/spring-binding/src/main/java/org/springframework/binding/format/support/AbstractFormatterFactory.java new file mode 100644 index 00000000..f6ce4d50 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/format/support/AbstractFormatterFactory.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.format.support; + +import java.util.Locale; + +import org.springframework.binding.format.Formatter; +import org.springframework.binding.format.FormatterFactory; +import org.springframework.binding.format.Style; +import org.springframework.context.i18n.LocaleContext; +import org.springframework.context.i18n.SimpleLocaleContext; + +/** + * Base class for formatter factories. + * + * @author Keith Donald + */ +public abstract class AbstractFormatterFactory implements FormatterFactory { + + private LocaleContext localeContext = new SimpleLocaleContext(Locale.getDefault()); + + private Style defaultDateStyle = Style.MEDIUM; + + private Style defaultTimeStyle = Style.MEDIUM; + + /** + * Set's the locale context used. Defaults to a SimpleLocaleContext holding + * the system default locale. + */ + public void setLocaleContext(LocaleContext localeContext) { + this.localeContext = localeContext; + } + + /** + * Returns the locale in use. + */ + protected Locale getLocale() { + return localeContext.getLocale(); + } + + /** + * Returns the default date style. Defaults to {@link Style#MEDIUM}. + */ + protected Style getDefaultDateStyle() { + return defaultDateStyle; + } + + /** + * Set the default date style. + */ + public void setDefaultDateStyle(Style defaultDateStyle) { + this.defaultDateStyle = defaultDateStyle; + } + + /** + * Returns the default time style. Defaults to {@link Style#MEDIUM}. + */ + public Style getDefaultTimeStyle() { + return defaultTimeStyle; + } + + /** + * Set the default time style. + */ + public void setDefaultTimeStyle(Style defaultTimeStyle) { + this.defaultTimeStyle = defaultTimeStyle; + } + + public Formatter getDateFormatter() { + return getDateFormatter(getDefaultDateStyle()); + } + + public Formatter getDateTimeFormatter() { + return getDateTimeFormatter(getDefaultDateStyle(), getDefaultTimeStyle()); + } + + public Formatter getTimeFormatter() { + return getTimeFormatter(getDefaultTimeStyle()); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/format/support/DateFormatter.java b/spring-binding/src/main/java/org/springframework/binding/format/support/DateFormatter.java new file mode 100644 index 00000000..0e0ce519 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/format/support/DateFormatter.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.format.support; + +import java.text.DateFormat; +import java.text.ParseException; +import java.util.Date; + +import org.springframework.binding.format.InvalidFormatException; + +/** + * Formatter that formats date objects. + * + * @author Keith Donald + */ +public class DateFormatter extends AbstractFormatter { + + private DateFormat dateFormat; + + /** + * Constructs a date formatter that will delegate to the specified date + * format. + * @param dateFormat the date format to use + */ + public DateFormatter(DateFormat dateFormat) { + this.dateFormat = dateFormat; + } + + /** + * Constructs a date formatter that will delegate to the specified date + * format. + * @param dateFormat the date format to use + * @param allowEmpty should this formatter allow empty input arguments? + */ + public DateFormatter(DateFormat dateFormat, boolean allowEmpty) { + super(allowEmpty); + this.dateFormat = dateFormat; + } + + // convert from date to string + protected String doFormatValue(Object date) { + return dateFormat.format((Date)date); + } + + // convert back from string to date + protected Object doParseValue(String formattedString, Class targetClass) throws ParseException { + return dateFormat.parse(formattedString); + } + + /** + * Convenience method to parse a date. + */ + public Date parseDate(String formattedString) throws InvalidFormatException { + return (Date)parseValue(formattedString, Date.class); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/format/support/FormatterPropertyEditor.java b/spring-binding/src/main/java/org/springframework/binding/format/support/FormatterPropertyEditor.java new file mode 100644 index 00000000..341cddab --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/format/support/FormatterPropertyEditor.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.format.support; + +import java.beans.PropertyEditorSupport; + +import org.springframework.binding.format.Formatter; + +/** + * Adapts a formatter to the property editor contract. + * + * @author Keith Donald + */ +public class FormatterPropertyEditor extends PropertyEditorSupport { + + /** + * The formatter + */ + private Formatter formatter; + + /** + * The target value class (may be null). + */ + private Class targetClass; + + /** + * Creates a formatter property editor. + * @param formatter the formatter to adapt + */ + public FormatterPropertyEditor(Formatter formatter) { + this.formatter = formatter; + } + + /** + * Creates a formatter property editor. + * @param formatter the formatter to adapt + * @param targetClass the target class for "setAsText" conversions + */ + public FormatterPropertyEditor(Formatter formatter, Class targetClass) { + this.formatter = formatter; + this.targetClass = targetClass; + } + + public String getAsText() { + return formatter.formatValue(getValue()); + } + + public void setAsText(String text) throws IllegalArgumentException { + setValue(formatter.parseValue(text, targetClass)); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/format/support/LabeledEnumFormatter.java b/spring-binding/src/main/java/org/springframework/binding/format/support/LabeledEnumFormatter.java new file mode 100644 index 00000000..bde4e3c1 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/format/support/LabeledEnumFormatter.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.format.support; + +import org.springframework.binding.format.InvalidFormatException; +import org.springframework.core.enums.LabeledEnum; +import org.springframework.core.enums.LabeledEnumResolver; +import org.springframework.core.enums.StaticLabeledEnumResolver; +import org.springframework.util.Assert; + +/** + * Converts from string to a LabeledEnum instance and back. + * + * @author Keith Donald + */ +public class LabeledEnumFormatter extends AbstractFormatter { + + private LabeledEnumResolver labeledEnumResolver = StaticLabeledEnumResolver.instance(); + + /** + * Default constructor. + */ + public LabeledEnumFormatter() { + } + + /** + * Create a new LabeledEnum formatter. + * @param allowEmpty should this formatter allow empty input arguments? + */ + public LabeledEnumFormatter(boolean allowEmpty) { + super(allowEmpty); + } + + /** + * Set the LabeledEnumResolver used. Defaults to {@link StaticLabeledEnumResolver}. + */ + public void setLabeledEnumResolver(LabeledEnumResolver labeledEnumResolver) { + Assert.notNull(labeledEnumResolver, "The labeled enum resolver is required"); + this.labeledEnumResolver = labeledEnumResolver; + } + + protected String doFormatValue(Object value) { + LabeledEnum labeledEnum = (LabeledEnum)value; + return labeledEnum.getLabel(); + } + + protected Object doParseValue(String formattedString, Class targetClass) throws IllegalArgumentException { + LabeledEnum labeledEnum = labeledEnumResolver.getLabeledEnumByLabel(targetClass, formattedString); + if (!isAllowEmpty()) { + Assert.notNull(labeledEnum, "The label '" + formattedString + + "' did not map to a valid enum instance for type " + targetClass); + Assert.isInstanceOf(targetClass, labeledEnum); + } + return labeledEnum; + } + + /** + * Convenience method to parse a LabeledEnum. + */ + public LabeledEnum parseLabeledEnum(String formattedString, Class enumClass) throws InvalidFormatException { + return (LabeledEnum)parseValue(formattedString, enumClass); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/format/support/NumberFormatter.java b/spring-binding/src/main/java/org/springframework/binding/format/support/NumberFormatter.java new file mode 100644 index 00000000..30a22783 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/format/support/NumberFormatter.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.format.support; + +import java.math.BigInteger; +import java.text.NumberFormat; + +import org.springframework.binding.format.InvalidFormatException; +import org.springframework.util.NumberUtils; + +/** + * Converts from various + * Number specializations to String and back. + * + * @author Keith Donald + */ +public class NumberFormatter extends AbstractFormatter { + + private NumberFormat numberFormat; + + /** + * Default constructor. + */ + public NumberFormatter() { + } + + /** + * Create a new number formatter. + * @param numberFormat the number format to use + */ + public NumberFormatter(NumberFormat numberFormat) { + this.numberFormat = numberFormat; + } + + /** + * Create a new number formatter. + * @param numberFormat the number format to use + * @param allowEmpty should this formatter allow empty input arguments? + */ + public NumberFormatter(NumberFormat numberFormat, boolean allowEmpty) { + super(allowEmpty); + this.numberFormat = numberFormat; + } + + protected String doFormatValue(Object number) { + if (this.numberFormat != null) { + // use NumberFormat for rendering value + return this.numberFormat.format(number); + } + else { + // use toString method for rendering value + return number.toString(); + } + } + + protected Object doParseValue(String text, Class targetClass) throws IllegalArgumentException { + // use given NumberFormat for parsing text + if (this.numberFormat != null) { + return NumberUtils.parseNumber(text, targetClass, this.numberFormat); + } + // use default valueOf methods for parsing text + else { + return NumberUtils.parseNumber(text, targetClass); + } + } + + // convenience methods + + public Short parseShort(String formattedString) throws InvalidFormatException { + return (Short)parseValue(formattedString, Short.class); + } + + public Integer parseInteger(String formattedString) throws InvalidFormatException { + return (Integer)parseValue(formattedString, Integer.class); + } + + public Long parseLong(String formattedString) throws InvalidFormatException { + return (Long)parseValue(formattedString, Long.class); + } + + public Double parseDouble(String formattedString) throws InvalidFormatException { + return (Double)parseValue(formattedString, Double.class); + } + + public Float parseFloat(String formattedString) throws InvalidFormatException { + return (Float)parseValue(formattedString, Float.class); + } + + public BigInteger parseBigInteger(String formattedString) throws InvalidFormatException { + return (BigInteger)parseValue(formattedString, BigInteger.class); + } + + public Byte parseByte(String formattedString) throws InvalidFormatException { + return (Byte)parseValue(formattedString, Byte.class); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/format/support/PropertyEditorFormatter.java b/spring-binding/src/main/java/org/springframework/binding/format/support/PropertyEditorFormatter.java new file mode 100644 index 00000000..807bfa96 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/format/support/PropertyEditorFormatter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.format.support; + +import java.beans.PropertyEditor; + +import org.springframework.util.Assert; + +/** + * Adapts a property editor to the formatter interface. + * + * @author Keith Donald + */ +public class PropertyEditorFormatter extends AbstractFormatter { + + private PropertyEditor propertyEditor; + + /** + * Wrap given property editor in a formatter. + */ + public PropertyEditorFormatter(PropertyEditor propertyEditor) { + Assert.notNull(propertyEditor, "Property editor is required"); + this.propertyEditor = propertyEditor; + } + + /** + * Returns the wrapped property editor. + */ + public PropertyEditor getPropertyEditor() { + return propertyEditor; + } + + protected String doFormatValue(Object value) { + propertyEditor.setValue(value); + return propertyEditor.getAsText(); + } + + protected Object doParseValue(String formattedValue, Class targetClass) { + propertyEditor.setAsText(formattedValue); + return propertyEditor.getValue(); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/format/support/SimpleFormatterFactory.java b/spring-binding/src/main/java/org/springframework/binding/format/support/SimpleFormatterFactory.java new file mode 100644 index 00000000..851b2f8d --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/format/support/SimpleFormatterFactory.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.format.support; + +import java.text.NumberFormat; +import java.text.SimpleDateFormat; + +import org.springframework.binding.format.Formatter; +import org.springframework.binding.format.Style; + +/** + * Simple FormatterFactory implementation. + * + * @author Keith Donald + */ +public class SimpleFormatterFactory extends AbstractFormatterFactory { + + public SimpleFormatterFactory() { + } + + public Formatter getDateFormatter(Style style) { + return new DateFormatter(SimpleDateFormat.getDateInstance(style.shortValue(), getLocale())); + } + + public Formatter getDateTimeFormatter(Style dateStyle, Style timeStyle) { + return new DateFormatter(SimpleDateFormat.getDateTimeInstance(dateStyle.shortValue(), timeStyle.shortValue(), + getLocale())); + } + + public Formatter getTimeFormatter(Style style) { + return new DateFormatter(SimpleDateFormat.getTimeInstance(style.shortValue(), getLocale())); + } + + public Formatter getNumberFormatter(Class numberClass) { + return new NumberFormatter(NumberFormat.getNumberInstance(getLocale())); + } + + public Formatter getCurrencyFormatter() { + return new NumberFormatter(NumberFormat.getCurrencyInstance(getLocale())); + } + + public Formatter getDateFormatter(String encodedFormat) { + return new DateFormatter(new SimpleDateFormat(encodedFormat)); + } + + public Formatter getPercentFormatter() { + return new NumberFormatter(NumberFormat.getPercentInstance(getLocale())); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/format/support/package.html b/spring-binding/src/main/java/org/springframework/binding/format/support/package.html new file mode 100644 index 00000000..0c067b5d --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/format/support/package.html @@ -0,0 +1,7 @@ + + +

+Supporting formatter implementations that are generically applicable and frequently used. +

+ + \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/mapping/AttributeMapper.java b/spring-binding/src/main/java/org/springframework/binding/mapping/AttributeMapper.java new file mode 100644 index 00000000..2f920306 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/mapping/AttributeMapper.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.mapping; + +/** + * A lightweight service interface for mapping between two attribute sources. + *

+ * Implementations of this interface are expected to encapsulate the mapping + * configuration information as well as the logic to act on it to perform + * mapping between a given source and target attribute source. + * + * @author Keith Donald + */ +public interface AttributeMapper { + + /** + * Map data from a source object to a target object. + * @param source the source + * @param target the target + * @param context the mapping context + */ + public void map(Object source, Object target, MappingContext context); +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/mapping/DefaultAttributeMapper.java b/spring-binding/src/main/java/org/springframework/binding/mapping/DefaultAttributeMapper.java new file mode 100644 index 00000000..63762ac1 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/mapping/DefaultAttributeMapper.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.mapping; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import org.springframework.core.style.ToStringCreator; + +/** + * Generic attributes mapper implementation that allows mappings to be + * configured programatically. + * + * @author Erwin Vervaet + * @author Keith Donald + * @author Colin Sampaleanu + */ +public class DefaultAttributeMapper implements AttributeMapper { + + /** + * The ordered list of mappings to apply. + */ + private List mappings = new LinkedList(); + + /** + * Add a mapping to this mapper. + * @param mapping the mapping to add (as an AttributeMapper) + * @return this, to support convenient call chaining + */ + public DefaultAttributeMapper addMapping(AttributeMapper mapping) { + mappings.add(mapping); + return this; + } + + /** + * Add a set of mappings. + * @param mappings the mappings + */ + public void addMappings(AttributeMapper[] mappings) { + if (mappings == null) { + return; + } + this.mappings.addAll(Arrays.asList(mappings)); + } + + /** + * Returns this mapper's list of mappings. + * @return the list of mappings + */ + public AttributeMapper[] getMappings() { + return (AttributeMapper[])mappings.toArray(new AttributeMapper[mappings.size()]); + } + + public void map(Object source, Object target, MappingContext context) { + if (mappings != null) { + Iterator it = mappings.iterator(); + while (it.hasNext()) { + AttributeMapper mapping = (AttributeMapper)it.next(); + mapping.map(source, target, context); + } + } + } + + public String toString() { + return new ToStringCreator(this).append("mappings", mappings).toString(); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/mapping/Mapping.java b/spring-binding/src/main/java/org/springframework/binding/mapping/Mapping.java new file mode 100644 index 00000000..5a568f80 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/mapping/Mapping.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.mapping; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.binding.convert.ConversionExecutor; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.SettableExpression; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; + +/** + * A single mapping definition, encapulating the information neccessary to map + * the result of evaluating an expression on a source object to a property on a + * target object, optionally applying a type conversion during the mapping + * process. + * + * @author Keith Donald + */ +public class Mapping implements AttributeMapper { + + private static final Log logger = LogFactory.getLog(Mapping.class); + + /** + * The source expression to evaluate against a source object to map from. + */ + private final Expression sourceExpression; + + /** + * The target expression to set on a target object to map to. + */ + private final SettableExpression targetExpression; + + /** + * A type converter to apply during the mapping process. + */ + private final ConversionExecutor typeConverter; + + /** + * Whether or not this is a required mapping; if true, the source expression + * must return a non-null value. + */ + private boolean required; + + /** + * Creates a new mapping. + * @param sourceExpression the source expression + * @param targetExpression the target expression + * @param typeConverter a type converter + */ + public Mapping(Expression sourceExpression, SettableExpression targetExpression, + ConversionExecutor typeConverter) { + this(sourceExpression, targetExpression, typeConverter, false); + } + + /** + * Creates a new mapping. + * @param sourceExpression the source expression + * @param targetExpression the target expression + * @param typeConverter a type converter + * @param required whether or not this mapping is required + */ + protected Mapping(Expression sourceExpression, SettableExpression targetExpression, + ConversionExecutor typeConverter, boolean required) { + Assert.notNull(sourceExpression, "The source expression is required"); + Assert.notNull(targetExpression, "The target expression is required"); + this.sourceExpression = sourceExpression; + this.targetExpression = targetExpression; + this.typeConverter = typeConverter; + this.required = required; + } + + /** + * Map the sourceAttribute in to the + * targetAttribute target map, performing type conversion if + * necessary. + * @param source The source data structure + * @param target The target data structure + */ + public void map(Object source, Object target, MappingContext context) { + // get source value + Object sourceValue = sourceExpression.evaluate(source, null); + if (sourceValue == null) { + if (required) { + throw new RequiredMappingException("This mapping is required; evaluation of expression '" + + sourceExpression + "' against source of type [" + source.getClass() + + "] must return a non-null value"); + } + else { + // source expression returned no value, simply abort mapping + return; + } + } + Object targetValue = sourceValue; + if (typeConverter != null) { + targetValue = typeConverter.execute(sourceValue); + } + // set target value + if (logger.isDebugEnabled()) { + logger.debug("Mapping '" + sourceExpression + "' value [" + sourceValue + "] to target property '" + + targetExpression + "'; setting property value to [" + targetValue + "]"); + } + targetExpression.evaluateToSet(target, targetValue, null); + } + + public boolean equals(Object o) { + if (!(o instanceof Mapping)) { + return false; + } + Mapping other = (Mapping)o; + return sourceExpression.equals(other.sourceExpression) + && targetExpression.equals(other.targetExpression); + } + + public int hashCode() { + return sourceExpression.hashCode() + targetExpression.hashCode(); + } + + public String toString() { + return new ToStringCreator(this).append(sourceExpression + " -> " + targetExpression).toString(); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/mapping/MappingBuilder.java b/spring-binding/src/main/java/org/springframework/binding/mapping/MappingBuilder.java new file mode 100644 index 00000000..377d479e --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/mapping/MappingBuilder.java @@ -0,0 +1,187 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.mapping; + +import org.springframework.binding.convert.ConversionExecutor; +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.convert.support.DefaultConversionService; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.binding.expression.SettableExpression; +import org.springframework.util.Assert; + +/** + * A stateful builder that builds {@link Mapping} objects. Designed for + * convenience to build mappings in a clear, readable manner. + *

+ * Example usage: + * + *

+ * MappingBuilder mapping = new MappingBuilder();
+ * Mapping result = mapping.source("foo").target("bar").from(String.class).to(Long.class).value();
+ * 
+ * + * Calling the {@link #value()} result method clears out this builder's state so + * it can be reused to build another mapping. + * + * @author Keith Donald + */ +public class MappingBuilder { + + /** + * The expression string parser. + */ + private ExpressionParser expressionParser; + + /** + * The conversion service for applying type conversions. + */ + private ConversionService conversionService = new DefaultConversionService(); + + /** + * The source mapping expression. + */ + private Expression sourceExpression; + + /** + * The target mapping settable expression. + */ + private SettableExpression targetExpression; + + /** + * The type of the object returned by evaluating the source expression. + */ + private Class sourceType; + + /** + * The type of the property settable by the target expression. + */ + private Class targetType; + + /** + * Whether or not the built mapping is a required mapping. + */ + private boolean required; + + /** + * Creates a mapping builder that uses the expression parser to parse + * attribute mapping expressions. + * @param expressionParser the expression parser + */ + public MappingBuilder(ExpressionParser expressionParser) { + Assert.notNull(expressionParser, "The expression parser is required"); + this.expressionParser = expressionParser; + } + + /** + * Sets the conversion service that will convert the object returned by + * evaluating the source expression to the {@link #to(Class)} type if + * necessary. + * @param conversionService the conversion service + */ + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * Sets the source expression of the mapping built by this builder. + * @param expressionString the expression string + * @return this, to support call-chaining + */ + public MappingBuilder source(String expressionString) { + sourceExpression = expressionParser.parseExpression(expressionString); + return this; + } + + /** + * Sets the target property expression of the mapping built by this builder. + * @param expressionString the expression string + * @return this, to support call-chaining + */ + public MappingBuilder target(String expressionString) { + targetExpression = (SettableExpression)expressionParser.parseExpression(expressionString); + return this; + } + + /** + * Sets the expected type of the object returned by evaluating the source + * expression. Used in conjunction with {@link #to(Class)} to perform a type + * conversion during the mapping process. + * @param sourceType the source type + * @return this, to support call-chaining + */ + public MappingBuilder from(Class sourceType) { + this.sourceType = sourceType; + return this; + } + + /** + * Sets the target type of the property writeable by the target expression. + * @param targetType the target type + * @return this, to support call-chaining + */ + public MappingBuilder to(Class targetType) { + this.targetType = targetType; + return this; + } + + /** + * Marks the mapping to be built a "required" mapping. + * @return this, to support call-chaining + */ + public MappingBuilder required() { + this.required = true; + return this; + } + + /** + * The logical GoF builder getResult method, returning a fully constructed + * Mapping from the configured pieces. Once called, the state of this + * builder is nulled out to support building a new mapping object again. + * @return the mapping result + */ + public Mapping value() { + Assert.notNull(sourceExpression, "The source expression must be set at a minimum"); + if (targetExpression == null) { + targetExpression = (SettableExpression)sourceExpression; + } + ConversionExecutor typeConverter = null; + if (sourceType != null) { + Assert.notNull(targetType, "The target type is required when the source type is specified"); + typeConverter = conversionService.getConversionExecutor(sourceType, targetType); + } + Mapping result; + if (required) { + result = new RequiredMapping(sourceExpression, targetExpression, typeConverter); + } + else { + result = new Mapping(sourceExpression, targetExpression, typeConverter); + } + reset(); + return result; + } + + /** + * Reset this mapping builder. + */ + public void reset() { + sourceExpression = null; + targetExpression = null; + sourceType = null; + targetType = null; + required = false; + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/mapping/MappingContext.java b/spring-binding/src/main/java/org/springframework/binding/mapping/MappingContext.java new file mode 100644 index 00000000..44252b65 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/mapping/MappingContext.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.mapping; + +/** + * A context object with two main responsibities: + *
    + *
  1. Exposing information to a mapper to influence + * a mapping attempt. + *
  2. Providing operations for recording progress or + * errors during the mapping process. + *
+ * Empty for now; subclasses may define their own custom context behavior + * accessible by a mapper with a downcast. + * + * @author Keith Donald + */ +public interface MappingContext { + +} diff --git a/spring-binding/src/main/java/org/springframework/binding/mapping/RequiredMapping.java b/spring-binding/src/main/java/org/springframework/binding/mapping/RequiredMapping.java new file mode 100644 index 00000000..0005b3e4 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/mapping/RequiredMapping.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.mapping; + +import org.springframework.binding.convert.ConversionExecutor; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.SettableExpression; + +/** + * A mapping that is required. + * + * @author Keith Donald + */ +public class RequiredMapping extends Mapping { + + /** + * Creates a required mapping. + * @param sourceExpression the source mapping expression + * @param targetPropertyExpression the target property expression + * @param typeConverter a type converter + */ + public RequiredMapping(Expression sourceExpression, SettableExpression targetPropertyExpression, + ConversionExecutor typeConverter) { + super(sourceExpression, targetPropertyExpression, typeConverter, true); + } +} diff --git a/spring-binding/src/main/java/org/springframework/binding/mapping/RequiredMappingException.java b/spring-binding/src/main/java/org/springframework/binding/mapping/RequiredMappingException.java new file mode 100644 index 00000000..68b54520 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/mapping/RequiredMappingException.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.mapping; + +/** + * Thrown when a required mapping could not be performed. + * + * @author Keith Donald + */ +public class RequiredMappingException extends IllegalStateException { + + /** + * Create a new required mapping exception. + * @param message a descriptive message + */ + public RequiredMappingException(String message) { + super(message); + } +} diff --git a/spring-binding/src/main/java/org/springframework/binding/mapping/package.html b/spring-binding/src/main/java/org/springframework/binding/mapping/package.html new file mode 100644 index 00000000..24886f4b --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/mapping/package.html @@ -0,0 +1,7 @@ + + +

+Support for mapping attribute values between data structures. +

+ + \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/method/InvalidMethodKeyException.java b/spring-binding/src/main/java/org/springframework/binding/method/InvalidMethodKeyException.java new file mode 100644 index 00000000..99dfa1f8 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/method/InvalidMethodKeyException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.method; + +import org.springframework.core.NestedRuntimeException; + +/** + * Thrown when a method key could not be resolved to an invokable java Method on + * a Class. + * + * @author Keith Donald + */ +public class InvalidMethodKeyException extends NestedRuntimeException { + + /** + * The method key that could not be resolved. + */ + private MethodKey methodKey; + + /** + * Creates an exception signaling an invalid method signature. + * @param methodKey the class method key + * @param cause the cause + */ + public InvalidMethodKeyException(MethodKey methodKey, Exception cause) { + super("Could not resolve method with key " + methodKey, cause); + this.methodKey = methodKey; + } + + /** + * Returns the invalid method key. + * @return the method key. + */ + public MethodKey getMethodKey() { + return methodKey; + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/method/MethodInvocationException.java b/spring-binding/src/main/java/org/springframework/binding/method/MethodInvocationException.java new file mode 100644 index 00000000..04044461 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/method/MethodInvocationException.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.method; + +import java.lang.reflect.InvocationTargetException; + +import org.springframework.core.NestedRuntimeException; +import org.springframework.core.style.StylerUtils; + +/** + * Base class for exceptions that report a method invocation failure. + * + * @author Keith Donald + */ +public class MethodInvocationException extends NestedRuntimeException { + + /** + * The method signature. + */ + private MethodSignature methodSignature; + + /** + * The method invocation argument values. + */ + private Object[] arguments; + + /** + * Signals that the method with the specified signature could not be invoked + * with the provided arguments. + * @param methodSignature the method signature + * @param arguments the arguments + * @param cause the root cause + */ + public MethodInvocationException(MethodSignature methodSignature, Object[] arguments, Exception cause) { + super("Unable to invoke method " + methodSignature + " with arguments " + StylerUtils.style(arguments), cause); + } + + /** + * Returns the invoked method's signature. + */ + public MethodSignature getMethodSignature() { + return methodSignature; + } + + /** + * Returns the method invocation arguments. + */ + public Object[] getArguments() { + return arguments; + } + + /** + * Returns the target root cause exception of the method invocation failure. + * @return the target throwable + */ + public Throwable getTargetException() { + if (getCause() instanceof InvocationTargetException) { + return ((InvocationTargetException)getCause()).getTargetException(); + } + else { + return getCause(); + } + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/method/MethodInvoker.java b/spring-binding/src/main/java/org/springframework/binding/method/MethodInvoker.java new file mode 100644 index 00000000..2ebf245c --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/method/MethodInvoker.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.method; + +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.convert.support.DefaultConversionService; +import org.springframework.core.style.StylerUtils; +import org.springframework.util.CachingMapDecorator; + +/** + * A helper for invoking typed methods on abritrary objects, with support for + * argument value type conversion from values retrieved from a argument + * attribute source. + * + * @author Keith Donald + */ +public class MethodInvoker { + + private static final Log logger = LogFactory.getLog(MethodInvoker.class); + + /** + * Conversion service for converting arguments to the neccessary type if + * required. + */ + private ConversionService conversionService = new DefaultConversionService(); + + /** + * A cache of invoked bean methods, keyed weakly. + */ + private CachingMapDecorator methodCache = new CachingMapDecorator(true) { + public Object create(Object key) { + return ((MethodKey) key).getMethod(); + } + }; + + /** + * Sets the conversion service to convert argument values as needed. + */ + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + /** + * Invoke the method on the bean provided. Argument values are pulled from + * the provided argument source. + * @param signature the definition of the method to invoke, including the + * method name and the method argument types + * @param bean the bean to invoke + * @param argumentSource the source for method arguments + * @return the invoked method's return value + * @throws MethodInvocationException the method could not be invoked + */ + public Object invoke(MethodSignature signature, Object bean, Object argumentSource) + throws MethodInvocationException { + Parameters parameters = signature.getParameters(); + Object[] arguments = new Object[parameters.size()]; + for (int i = 0; i < parameters.size(); i++) { + Parameter parameter = parameters.getParameter(i); + Object argument = parameter.evaluateArgument(argumentSource, null); + arguments[i] = applyTypeConversion(argument, parameter.getType()); + } + Class[] parameterTypes = parameters.getTypesArray(); + for (int i = 0; i < parameterTypes.length; i++) { + if (parameterTypes[i] == null) { + Object argument = arguments[i]; + if (argument != null) { + parameterTypes[i] = argument.getClass(); + } + } + } + MethodKey key = new MethodKey(bean.getClass(), signature.getMethodName(), parameterTypes); + try { + Method method = (Method) methodCache.get(key); + if (logger.isDebugEnabled()) { + logger.debug("Invoking method with signature [" + key + "] with arguments " + + StylerUtils.style(arguments) + " on bean [" + bean + "]"); + + } + Object returnValue = method.invoke(bean, arguments); + if (logger.isDebugEnabled()) { + logger.debug("Invoked method with signature [" + key + "] returned value [" + returnValue + "]"); + } + return returnValue; + } + catch (Exception e) { + throw new MethodInvocationException(signature, arguments, e); + } + } + + /** + * Apply type conversion on the event parameter if neccessary + * + * @param parameterValue the raw argument value + * @param targetType the target type for the matching method argument + * @return the converted method argument + */ + protected Object applyTypeConversion(Object parameterValue, Class targetType) { + if (parameterValue == null || targetType == null) { + return parameterValue; + } + return conversionService.getConversionExecutor(parameterValue.getClass(), targetType).execute(parameterValue); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/method/MethodKey.java b/spring-binding/src/main/java/org/springframework/binding/method/MethodKey.java new file mode 100644 index 00000000..fe69b9c4 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/method/MethodKey.java @@ -0,0 +1,251 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.method; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * A helper for resolving and caching a Java method by reflection. + * + * @author Keith Donald + */ +public class MethodKey implements Serializable { + + /** + * The class the method is a member of. + */ + private Class declaredType; + + /** + * The method name. + */ + private String methodName; + + /** + * The method's actual parameter types. + */ + private Class[] parameterTypes; + + /** + * A cached handle to the resolved method (may be null). + */ + private transient Method method; + + /** + * Create a new method key. + * @param declaredType the class the method is a member of + * @param methodName the method name + * @param parameterTypes the method's parameter types, or null + * if the method has no parameters + */ + public MethodKey(Class declaredType, String methodName, Class[] parameterTypes) { + Assert.notNull(declaredType, "The method's declared type is required"); + Assert.notNull(methodName, "The method name is required"); + this.declaredType = declaredType; + this.methodName = methodName; + this.parameterTypes = parameterTypes; + } + + /** + * Return the class the method is a member of. + */ + public Class getDeclaredType() { + return declaredType; + } + + /** + * Returns the method name. + */ + public String getMethodName() { + return methodName; + } + + /** + * Returns the method parameter types. + */ + public Class[] getParameterTypes() { + return parameterTypes; + } + + /** + * Returns the keyed method, resolving it if necessary via reflection. + */ + public Method getMethod() throws InvalidMethodKeyException { + if (method == null) { + method = resolveMethod(); + } + return method; + } + + // internal helpers + + /** + * Resolve the keyed method. + */ + protected Method resolveMethod() throws InvalidMethodKeyException { + try { + return declaredType.getMethod(methodName, getParameterTypes()); + } + catch (NoSuchMethodException e) { + Method method = findMethodConsiderAssignableParameterTypes(); + if (method != null) { + return method; + } + else { + throw new InvalidMethodKeyException(this, e); + } + } + } + + /** + * Find the keyed method using 'relaxed' typing. + */ + protected Method findMethodConsiderAssignableParameterTypes() { + Method[] candidateMethods = getDeclaredType().getMethods(); + for (int i = 0; i < candidateMethods.length; i++) { + if (candidateMethods[i].getName().equals(methodName)) { + // Check if the method has the correct number of parameters. + Class[] candidateParameterTypes = candidateMethods[i].getParameterTypes(); + if (candidateParameterTypes.length == getParameterTypes().length) { + int numberOfCorrectArguments = 0; + for (int j = 0; j < candidateParameterTypes.length; j++) { + // Check if the candidate type is assignable to the sig + // parameter type. + Class candidateType = candidateParameterTypes[j]; + Class parameterType = parameterTypes[j]; + if (parameterType != null) { + if (isAssignable(candidateType, parameterType)) { + numberOfCorrectArguments++; + } + } + else { + // just match on a null param type (effectively + // 'any') + numberOfCorrectArguments++; + } + } + if (numberOfCorrectArguments == parameterTypes.length) { + return candidateMethods[i]; + } + } + } + } + return null; + } + + public boolean equals(Object obj) { + if (!(obj instanceof MethodKey)) { + return false; + } + MethodKey other = (MethodKey) obj; + return declaredType.equals(other.declaredType) && methodName.equals(other.methodName) + && parameterTypesEqual(other.parameterTypes); + } + + private boolean parameterTypesEqual(Class[] other) { + if (parameterTypes == other) { + return true; + } + if (parameterTypes.length != other.length) { + return false; + } + for (int i = 0; i < this.parameterTypes.length; i++) { + if (!ObjectUtils.nullSafeEquals(parameterTypes[i], other[i])) { + return false; + } + } + return true; + } + + public int hashCode() { + return declaredType.hashCode() + methodName.hashCode() + parameterTypesHash(); + } + + private int parameterTypesHash() { + if (parameterTypes == null) { + return 0; + } + int hash = 0; + for (int i = 0; i < parameterTypes.length; i++) { + Class parameterType = parameterTypes[i]; + if (parameterType != null) { + hash += parameterTypes[i].hashCode(); + } + } + return hash; + } + + // internal helpers + + /** + * Determine if the given target type is assignable from the given value + * type, assuming setting by reflection. Considers primitive wrapper classes + * as assignable to the corresponding primitive types.

NOTE: Pulled from + * ClassUtils in Spring 2.0 for 1.2.8 compatability. Should be collapsed + * when 1.2.9 is released. + * @param targetType the target type + * @param valueType the value type that should be assigned to the target + * type + * @return if the target type is assignable from the value type + */ + private static boolean isAssignable(Class targetType, Class valueType) { + return (targetType.isAssignableFrom(valueType) || targetType.equals(primitiveWrapperTypeMap.get(valueType))); + } + + /** + * Map with primitive wrapper type as key and corresponding primitive type + * as value, for example: Integer.class -> int.class. + */ + private static final Map primitiveWrapperTypeMap = new HashMap(8); + + static { + primitiveWrapperTypeMap.put(Boolean.class, boolean.class); + primitiveWrapperTypeMap.put(Byte.class, byte.class); + primitiveWrapperTypeMap.put(Character.class, char.class); + primitiveWrapperTypeMap.put(Double.class, double.class); + primitiveWrapperTypeMap.put(Float.class, float.class); + primitiveWrapperTypeMap.put(Integer.class, int.class); + primitiveWrapperTypeMap.put(Long.class, long.class); + primitiveWrapperTypeMap.put(Short.class, short.class); + } + + public String toString() { + return methodName + "(" + parameterTypesString() + ")"; + } + + /** + * Convenience method that returns the parameter types describing the + * signature of the method as a string. + */ + private String parameterTypesString() { + StringBuffer parameterTypesString = new StringBuffer(); + for (int i = 0; i < parameterTypes.length; i++) { + parameterTypesString.append(ClassUtils.getShortName(parameterTypes[i])); + if (i < parameterTypes.length - 1) { + parameterTypesString.append(','); + } + } + return parameterTypesString.toString(); + } + +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/method/MethodSignature.java b/spring-binding/src/main/java/org/springframework/binding/method/MethodSignature.java new file mode 100644 index 00000000..08edeae9 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/method/MethodSignature.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.method; + +import java.io.Serializable; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; + +/** + * A specification for a method consisting of the methodName and an optional set + * of named arguments. This class provides the ability to resolve a method with + * parameters and evaluate its argument values as part of a + * {@link MethodInvoker method invoker attempt}. + * + * @author Keith Donald + */ +public class MethodSignature implements Serializable { + + /** + * The name of the method, e.g "execute". + */ + private String methodName; + + /** + * The parameter types of the method, e.g "int param1". + */ + private Parameters parameters; + + /** + * Creates a method signature with no parameters. + * @param methodName the name of the method + */ + public MethodSignature(String methodName) { + this(methodName, Parameters.NONE); + } + + /** + * Creates a method signature with a single parameter. + * @param methodName the name of the method + * @param parameter the method parameter + */ + public MethodSignature(String methodName, Parameter parameter) { + this(methodName, new Parameters(parameter)); + } + + /** + * Creates a method signature with a list of parameters. + * @param methodName the name of the method + * @param parameters the method parameters + */ + public MethodSignature(String methodName, Parameters parameters) { + Assert.notNull(methodName, "The method name is required"); + Assert.notNull(parameters, "The parameters are required"); + this.methodName = methodName; + this.parameters = parameters; + } + + /** + * Returns the method name. + */ + public String getMethodName() { + return methodName; + } + + /** + * Returns the method parameters. + */ + public Parameters getParameters() { + return parameters; + } + + public boolean equals(Object obj) { + if (!(obj instanceof MethodSignature)) { + return false; + } + MethodSignature other = (MethodSignature) obj; + return methodName.equals(methodName) && parameters.equals(other.parameters); + } + + public int hashCode() { + return methodName.hashCode() + parameters.hashCode(); + } + + public String toString() { + return new ToStringCreator(this).append("methodName", methodName).append("parameters", parameters).toString(); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/method/Parameter.java b/spring-binding/src/main/java/org/springframework/binding/method/Parameter.java new file mode 100644 index 00000000..6aba7830 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/method/Parameter.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.method; + +import java.io.Serializable; + +import org.springframework.binding.expression.EvaluationContext; +import org.springframework.binding.expression.Expression; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * A named method parameter. Each parameter has an identifying name and is of a + * specified type (class). + * + * @author Keith Donald + */ +public class Parameter implements Serializable { + + /** + * The class of the parameter, e.g "springbank.AccountNumber". + */ + private Class type; + + /** + * The name of the parameter as an evaluatable expression, e.g + * "accountNumber". + */ + private Expression name; + + /** + * Create a new named parameter definition. Named parameters are capable of resolving + * parameter values (arguments) from argument sources. + * @param type the type the parameter type, may be null + * @param name the name the method argument expression (required) + */ + public Parameter(Class type, Expression name) { + Assert.notNull(name, "The parameter name expression is required"); + this.type = type; + this.name = name; + } + + /** + * Returns the parameter type. + */ + public Class getType() { + return type; + } + + /** + * Returns the method name. + */ + public Expression getName() { + return name; + } + + /** + * Evaluate this method parameter against the provided argument source, + * returning a single method argument value. + * @param argumentSource the meyhod argument source + * @param context the evaluation context + * @return the method argument value + */ + public Object evaluateArgument(Object argumentSource, EvaluationContext context) { + return name.evaluate(argumentSource, context); + } + + public boolean equals(Object obj) { + if (!(obj instanceof Parameter)) { + return false; + } + Parameter other = (Parameter) obj; + return ObjectUtils.nullSafeEquals(type, other.type) && name.equals(other.name); + } + + public int hashCode() { + return (type != null ? type.hashCode() : 0) + name.hashCode(); + } + + public String toString() { + return new ToStringCreator(this).append("type", type).append("name", name).toString(); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/method/Parameters.java b/spring-binding/src/main/java/org/springframework/binding/method/Parameters.java new file mode 100644 index 00000000..10abf010 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/method/Parameters.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.method; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +/** + * An ordered list of method parameters. + * + * @author Keith + */ +public class Parameters implements Serializable { + + /** + * Canonical instance for an empty parameters list. + */ + public static final Parameters NONE = new Parameters(0); + + /** + * The list. + */ + private List parameters; + + /** + * Create a parameter list of the default size (3 elements). + */ + public Parameters() { + this(3); + } + + /** + * Create a parameter list with the specified size. + * @param size the size + */ + public Parameters(int size) { + this.parameters = new ArrayList(size); + } + + /** + * Create a parameter list with one parameter. + * @param parameter the single parameter + */ + public Parameters(Parameter parameter) { + this.parameters = new ArrayList(1); + add(parameter); + } + + /** + * Create a parameter list from the parameter array. + * @param parameters the parameters + */ + public Parameters(Parameter[] parameters) { + this.parameters = new ArrayList(parameters.length); + addAll(parameters); + } + + /** + * Add a new parameter to this list. + * @param parameter the parameter + */ + public boolean add(Parameter parameter) { + return this.parameters.add(parameter); + } + + /** + * Add new parameters to this list. + * @param parameters the parameters + */ + public boolean addAll(Parameter[] parameters) { + return this.parameters.addAll(Arrays.asList(parameters)); + } + + /** + * Return a parameter iterator. + * @return the iterator + */ + public Iterator iterator() { + return parameters.iterator(); + } + + /** + * Get an array containing each parameter type. + * @return the types + */ + public Class[] getTypesArray() { + int i = 0; + Class[] types = new Class[parameters.size()]; + for (Iterator it = parameters.iterator(); it.hasNext();) { + Parameter param = (Parameter)it.next(); + types[i] = param.getType(); + i++; + } + return types; + } + + /** + * Returns the number of parameters in this list. + * @return the size + */ + public int size() { + return parameters.size(); + } + + /** + * Return the parameter at the provided index. + * @param index the parameter index + * @return the parameter at that index + * @throws IndexOutOfBoundsException if the provided index is out of bounds + */ + public Parameter getParameter(int index) throws IndexOutOfBoundsException { + return (Parameter)parameters.get(index); + } + + public boolean equals(Object obj) { + if (!(obj instanceof Parameters)) { + return false; + } + Parameters other = (Parameters)obj; + return parameters.equals(other.parameters); + } + + public int hashCode() { + return parameters.hashCode(); + } + + public String toString() { + return parameters.toString(); + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/method/TextToMethodSignature.java b/spring-binding/src/main/java/org/springframework/binding/method/TextToMethodSignature.java new file mode 100644 index 00000000..4fa9dc37 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/method/TextToMethodSignature.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.method; + +import org.springframework.binding.convert.ConversionContext; +import org.springframework.binding.convert.ConversionException; +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.convert.support.ConversionServiceAwareConverter; +import org.springframework.util.StringUtils; + +/** + * Converter that takes an encoded string representation and produces a + * corresponding MethodSignature object. + *

+ * This converter supports the following encoded forms: + *

    + *
  • "methodName" - the name of the method to invoke, where the method is + * expected to have no arguments.
  • + *
  • "methodName(param1Type param1Name, paramNType paramNName)" - the name of + * the method to invoke, where the method is expected to have parameters + * delimited by a comma. In this example, the method has two parameters. The + * type is either the fully-qualified class of the argument OR a known type + * alias. The name is the logical name of the argument, which is used during + * data binding to retrieve the argument value.
  • + *
+ * + * @see MethodSignature + * + * @author Keith Donald + */ +public class TextToMethodSignature extends ConversionServiceAwareConverter { + + /** + * Create a new converter that converts strings to MethodSignature objects. + */ + public TextToMethodSignature() { + } + + /** + * Create a new converter that converts strings to MethodSignature objects. + * @param conversionService the conversion service to use + */ + public TextToMethodSignature(ConversionService conversionService) { + super(conversionService); + } + + public Class[] getSourceClasses() { + return new Class[] { String.class }; + } + + public Class[] getTargetClasses() { + return new Class[] { MethodSignature.class }; + } + + protected Object doConvert(Object source, Class targetClass, ConversionContext context) throws Exception { + String encodedMethodKey = (String)source; + encodedMethodKey = encodedMethodKey.trim(); + int openParan = encodedMethodKey.indexOf('('); + if (openParan == -1) { + return new MethodSignature(encodedMethodKey); + } + else { + String methodName = encodedMethodKey.substring(0, openParan); + int closeParan = encodedMethodKey.lastIndexOf(')'); + if (closeParan == -1) { + throw new ConversionException(encodedMethodKey, MethodSignature.class, + "Syntax error: No close parenthesis specified for method parameter list", null); + } + String delimParamList = encodedMethodKey.substring(openParan + 1, closeParan); + String[] paramArray = StringUtils.commaDelimitedListToStringArray(delimParamList); + Parameters params = new Parameters(paramArray.length); + for (int i = 0; i < paramArray.length; i++) { + String param = paramArray[i].trim(); + String[] typeAndName = StringUtils.split(param, " "); + if (typeAndName != null && typeAndName.length == 2) { + Class type = (Class)converterFor(String.class, Class.class).execute(typeAndName[0]); + params.add(new Parameter(type, parseExpression(typeAndName[1].trim()))); + } + else { + params.add(new Parameter(null, parseExpression(param))); + } + } + return new MethodSignature(methodName, params); + } + } +} \ No newline at end of file diff --git a/spring-binding/src/main/java/org/springframework/binding/method/package.html b/spring-binding/src/main/java/org/springframework/binding/method/package.html new file mode 100644 index 00000000..836fdd08 --- /dev/null +++ b/spring-binding/src/main/java/org/springframework/binding/method/package.html @@ -0,0 +1,7 @@ + + +

+Method binding support for invoking abritrary methods on target beans. +

+ + \ No newline at end of file diff --git a/spring-binding/src/test/java/org/springframework/binding/collection/CompositeIteratorTests.java b/spring-binding/src/test/java/org/springframework/binding/collection/CompositeIteratorTests.java new file mode 100644 index 00000000..467027fa --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/collection/CompositeIteratorTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.collection; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + +import junit.framework.TestCase; + +/** + * Test case for {@link CompositeIterator}. + * + * @author Erwin Vervaet + */ +public class CompositeIteratorTests extends TestCase { + + public void testNoIterators() { + CompositeIterator it = new CompositeIterator(); + assertFalse(it.hasNext()); + try { + it.next(); + fail(); + } + catch (NoSuchElementException e) { + // expected + } + } + + public void testSingleIterator() { + CompositeIterator it = new CompositeIterator(); + it.add(Arrays.asList(new String[] { "0", "1" }).iterator()); + for (int i = 0; i < 2; i++) { + assertTrue(it.hasNext()); + assertEquals(String.valueOf(i), it.next()); + } + assertFalse(it.hasNext()); + try { + it.next(); + fail(); + } + catch (NoSuchElementException e) { + // expected + } + } + + public void testMultipleIterators() { + CompositeIterator it = new CompositeIterator(); + it.add(Arrays.asList(new String[] { "0", "1" }).iterator()); + it.add(Arrays.asList(new String[] { "2" }).iterator()); + it.add(Arrays.asList(new String[] { "3", "4" }).iterator()); + for (int i = 0; i < 5; i++) { + assertTrue(it.hasNext()); + assertEquals(String.valueOf(i), it.next()); + } + assertFalse(it.hasNext()); + try { + it.next(); + fail(); + } + catch (NoSuchElementException e) { + // expected + } + } + + public void testInUse() { + List list = Arrays.asList(new String[] { "0", "1" }); + CompositeIterator it = new CompositeIterator(); + it.add(list.iterator()); + it.hasNext(); + try { + it.add(list.iterator()); + fail(); + } + catch (IllegalStateException e) { + // expected + } + it = new CompositeIterator(); + it.add(list.iterator()); + it.next(); + try { + it.add(list.iterator()); + fail(); + } + catch (IllegalStateException e) { + // expected + } + } + + public void testDuplicateIterators() { + List list = Arrays.asList(new String[] { "0", "1" }); + Iterator iterator = list.iterator(); + CompositeIterator it = new CompositeIterator(); + it.add(iterator); + it.add(list.iterator()); + try { + it.add(iterator); + fail(); + } + catch (IllegalArgumentException e) { + // expected + } + } + +} diff --git a/spring-binding/src/test/java/org/springframework/binding/collection/SharedMapDecoratorTests.java b/spring-binding/src/test/java/org/springframework/binding/collection/SharedMapDecoratorTests.java new file mode 100644 index 00000000..12e4cad2 --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/collection/SharedMapDecoratorTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.collection; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link org.springframework.binding.collection.SharedMapDecorator}. + */ +public class SharedMapDecoratorTests extends TestCase { + + private SharedMapDecorator map = new SharedMapDecorator(new HashMap()); + + public void testGetPutRemove() { + assertTrue(map.size() == 0); + assertTrue(map.isEmpty()); + assertNull(map.get("foo")); + assertFalse(map.containsKey("foo")); + map.put("foo", "bar"); + assertTrue(map.size() == 1); + assertFalse(map.isEmpty()); + assertNotNull(map.get("foo")); + assertTrue(map.containsKey("foo")); + assertTrue(map.containsValue("bar")); + assertEquals("bar", map.get("foo")); + map.remove("foo"); + assertTrue(map.size() == 0); + assertNull(map.get("foo")); + } + + public void testPutAll() { + Map all = new HashMap(); + all.put("foo", "bar"); + all.put("bar", "baz"); + map.putAll(all); + assertTrue(map.size() == 2); + } + + public void testEntrySet() { + map.put("foo", "bar"); + map.put("bar", "baz"); + Set entrySet = map.entrySet(); + assertTrue(entrySet.size() == 2); + } + + public void testKeySet() { + map.put("foo", "bar"); + map.put("bar", "baz"); + Set keySet = map.keySet(); + assertTrue(keySet.size() == 2); + } + + public void testValues() { + map.put("foo", "bar"); + map.put("bar", "baz"); + Collection values = map.values(); + assertTrue(values.size() == 2); + } +} diff --git a/spring-binding/src/test/java/org/springframework/binding/collection/StringKeyedMapAdapterTests.java b/spring-binding/src/test/java/org/springframework/binding/collection/StringKeyedMapAdapterTests.java new file mode 100644 index 00000000..486d1b3c --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/collection/StringKeyedMapAdapterTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.collection; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link org.springframework.binding.collection.StringKeyedMapAdapter}. + */ +public class StringKeyedMapAdapterTests extends TestCase { + + private Map contents = new HashMap(); + + private StringKeyedMapAdapter map = new StringKeyedMapAdapter() { + + protected Object getAttribute(String key) { + return contents.get(key); + } + + protected Iterator getAttributeNames() { + return contents.keySet().iterator(); + } + + protected void removeAttribute(String key) { + contents.remove(key); + } + + protected void setAttribute(String key, Object value) { + contents.put(key, value); + } + }; + + public void testGetPutRemove() { + assertTrue(map.size() == 0); + assertTrue(map.isEmpty()); + assertNull(map.get("foo")); + assertFalse(map.containsKey("foo")); + map.put("foo", "bar"); + assertTrue(map.size() == 1); + assertFalse(map.isEmpty()); + assertNotNull(map.get("foo")); + assertTrue(map.containsKey("foo")); + assertTrue(map.containsValue("bar")); + assertEquals("bar", map.get("foo")); + map.remove("foo"); + assertTrue(map.size() == 0); + assertNull(map.get("foo")); + } + + public void testPutAll() { + Map all = new HashMap(); + all.put("foo", "bar"); + all.put("bar", "baz"); + map.putAll(all); + assertTrue(map.size() == 2); + } + + public void testEntrySet() { + map.put("foo", "bar"); + map.put("bar", "baz"); + Set entrySet = map.entrySet(); + assertTrue(entrySet.size() == 2); + } + + public void testKeySet() { + map.put("foo", "bar"); + map.put("bar", "baz"); + Set keySet = map.keySet(); + assertTrue(keySet.size() == 2); + } + + public void testValues() { + map.put("foo", "bar"); + map.put("bar", "baz"); + Collection values = map.values(); + assertTrue(values.size() == 2); + } +} diff --git a/spring-binding/src/test/java/org/springframework/binding/convert/support/CompositeConversionServiceTests.java b/spring-binding/src/test/java/org/springframework/binding/convert/support/CompositeConversionServiceTests.java new file mode 100644 index 00000000..b24902ea --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/convert/support/CompositeConversionServiceTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import java.util.Date; + +import junit.framework.TestCase; + +import org.springframework.binding.convert.ConversionException; +import org.springframework.binding.convert.ConversionExecutor; +import org.springframework.binding.convert.ConversionService; + +/** + * Test case for the {@link CompositeConversionService}. + * + * @author Erwin Vervaet + */ +public class CompositeConversionServiceTests extends TestCase { + + private CompositeConversionService service; + + protected void setUp() throws Exception { + GenericConversionService first = new GenericConversionService(); + first.addConverter(new TextToClass()); + first.addConverter(new TextToBoolean("ja", "nee")); + first.addDefaultAlias(Boolean.class); + GenericConversionService second = new GenericConversionService(); + second.addConverter(new TextToNumber()); + second.addConverter(new TextToBoolean()); + second.addDefaultAlias(Integer.class); + service = new CompositeConversionService(new ConversionService[] { first, second }); + } + + public void testGetConversionExecutor() { + assertNotNull(service.getConversionExecutor(String.class, Class.class)); + assertNotNull(service.getConversionExecutor(String.class, Boolean.class)); + assertEquals(Boolean.TRUE, service.getConversionExecutor(String.class, Boolean.class).execute("ja")); + assertNotNull(service.getConversionExecutor(String.class, Integer.class)); + try { + service.getConversionExecutor(String.class, Date.class); + fail(); + } + catch (ConversionException e) { + // expected + } + } + + public void testGetConversionExecutorByTargetAlias() { + assertNotNull(service.getConversionExecutorByTargetAlias(String.class, "boolean")); + assertEquals(Boolean.TRUE, service.getConversionExecutorByTargetAlias(String.class, "boolean").execute("ja")); + assertNotNull(service.getConversionExecutorByTargetAlias(String.class, "integer")); + assertNull(service.getConversionExecutorByTargetAlias(String.class, "class")); + } + + public void testGetConversionExecutorsForSource() { + assertEquals( + new TextToClass().getTargetClasses().length + + new TextToBoolean().getTargetClasses().length + + new TextToNumber().getTargetClasses().length, + service.getConversionExecutorsForSource(String.class).length); + assertEquals(0, service.getConversionExecutorsForSource(Date.class).length); + ConversionExecutor[] fromStringConversionExecutors = service.getConversionExecutorsForSource(String.class); + ConversionExecutor booleanConversionExecutor = null; + for (int i = 0; i < fromStringConversionExecutors.length; i++) { + if (fromStringConversionExecutors[i].getConverter() instanceof TextToBoolean) { + booleanConversionExecutor = fromStringConversionExecutors[i]; + } + } + assertEquals(Boolean.TRUE, booleanConversionExecutor.execute("ja")); + } + + public void testGetClassByAlias() { + assertEquals(Boolean.class, service.getClassByAlias("boolean")); + assertEquals(Integer.class, service.getClassByAlias("integer")); + assertNull(service.getClassByAlias("class")); + } +} diff --git a/spring-binding/src/test/java/org/springframework/binding/convert/support/DefaultConversionServiceTests.java b/spring-binding/src/test/java/org/springframework/binding/convert/support/DefaultConversionServiceTests.java new file mode 100644 index 00000000..7bd30a14 --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/convert/support/DefaultConversionServiceTests.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.convert.support; + +import java.util.HashMap; + +import junit.framework.TestCase; + +import org.springframework.binding.convert.ConversionException; +import org.springframework.binding.convert.ConversionExecutor; +import org.springframework.binding.convert.Converter; +import org.springframework.core.enums.ShortCodedLabeledEnum; + +/** + * Test case for the default conversion service. + * + * @author Keith Donald + */ +public class DefaultConversionServiceTests extends TestCase { + + public void testOverrideConverter() { + Converter customConverter = new TextToBoolean("ja", "nee"); + + DefaultConversionService service = new DefaultConversionService(); + + ConversionExecutor executor = service.getConversionExecutor(String.class, Boolean.class); + assertNotSame(customConverter, executor.getConverter()); + try { + executor.execute("ja"); + fail(); + } + catch (ConversionException e) { + // expected + } + + service.addConverter(customConverter); + + executor = service.getConversionExecutor(String.class, Boolean.class); + assertSame(customConverter, executor.getConverter()); + assertTrue(((Boolean)executor.execute("ja")).booleanValue()); + } + + public void testTargetClassNotSupported() { + DefaultConversionService service = new DefaultConversionService(); + try { + service.getConversionExecutor(String.class, HashMap.class); + fail("Should have thrown an exception"); + } + catch (ConversionException e) { + } + } + + public void testValidConversion() { + DefaultConversionService service = new DefaultConversionService(); + ConversionExecutor executor = service.getConversionExecutor(String.class, Integer.class); + Integer three = (Integer)executor.execute("3"); + assertEquals(3, three.intValue()); + } + + public void testLabeledEnumConversionNoSuchEnum() { + DefaultConversionService service = new DefaultConversionService(); + ConversionExecutor executor = service.getConversionExecutor(String.class, MyEnum.class); + try { + executor.execute("My Invalid Label"); + fail("Should have failed"); + } + catch (ConversionException e) { + } + } + + public void testValidLabeledEnumConversion() { + DefaultConversionService service = new DefaultConversionService(); + ConversionExecutor executor = service.getConversionExecutor(String.class, MyEnum.class); + MyEnum myEnum = (MyEnum)executor.execute("My Label 1"); + assertEquals(MyEnum.ONE, myEnum); + } + + public static class MyEnum extends ShortCodedLabeledEnum { + public static MyEnum ONE = new MyEnum(0, "My Label 1"); + + public static MyEnum TWO = new MyEnum(1, "My Label 2"); + + private MyEnum(int code, String label) { + super(code, label); + } + } +} \ No newline at end of file diff --git a/spring-binding/src/test/java/org/springframework/binding/expression/support/CollectionAddingExpressionTests.java b/spring-binding/src/test/java/org/springframework/binding/expression/support/CollectionAddingExpressionTests.java new file mode 100644 index 00000000..957c6815 --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/expression/support/CollectionAddingExpressionTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression.support; + +import java.util.ArrayList; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; + +/** + * Unit tests for {@link org.springframework.binding.expression.support.CollectionAddingExpression}. + */ +public class CollectionAddingExpressionTests extends TestCase { + + ExpressionParser parser = new BeanWrapperExpressionParser(); + + TestBean bean = new TestBean(); + + Expression exp = parser.parseExpression("list"); + + public void testEvaluation() { + ArrayList list = new ArrayList(); + bean.setList(list); + CollectionAddingExpression colExp = new CollectionAddingExpression(exp); + assertSame(list, colExp.evaluate(bean, null)); + } + + public void testAddToCollection() { + CollectionAddingExpression colExp = new CollectionAddingExpression(exp); + colExp.evaluateToSet(bean, "1", null); + colExp.evaluateToSet(bean, "2", null); + assertEquals("1", bean.getList().get(0)); + assertEquals("2", bean.getList().get(1)); + } + + public void testNotACollection() { + Expression exp = parser.parseExpression("flag"); + CollectionAddingExpression colExp = new CollectionAddingExpression(exp); + try { + colExp.evaluateToSet(bean, "1", null); + fail("not a collection"); + } + catch (IllegalArgumentException e) { + } + } + + public void testNoAddOnNullValue() { + CollectionAddingExpression colExp = new CollectionAddingExpression(exp); + colExp.evaluateToSet(bean, null, null); + colExp.evaluateToSet(bean, "2", null); + assertEquals("2", bean.getList().get(0)); + } +} \ No newline at end of file diff --git a/spring-binding/src/test/java/org/springframework/binding/expression/support/OgnlExpressionParserTests.java b/spring-binding/src/test/java/org/springframework/binding/expression/support/OgnlExpressionParserTests.java new file mode 100644 index 00000000..0e7ab1c8 --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/expression/support/OgnlExpressionParserTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression.support; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ParserException; + +/** + * Unit tests for {@link org.springframework.binding.expression.support.OgnlExpressionParser}. + */ +public class OgnlExpressionParserTests extends TestCase { + + private OgnlExpressionParser parser = new OgnlExpressionParser(); + + private TestBean bean = new TestBean(); + + public void testParseSimpleDelimited() { + String exp = "${flag}"; + Expression e = parser.parseExpression(exp); + assertNotNull(e); + Boolean b = (Boolean)e.evaluate(bean, null); + assertFalse(b.booleanValue()); + } + + public void testParseSimple() { + String exp = "flag"; + Expression e = parser.parseExpression(exp); + assertNotNull(e); + Boolean b = (Boolean)e.evaluate(bean, null); + assertFalse(b.booleanValue()); + } + + public void testParseNull() { + Expression e = parser.parseExpression(null); + assertNotNull(e); + assertNull(e.evaluate(bean, null)); + } + + public void testParseEmpty() { + Expression e = parser.parseExpression(""); + assertNotNull(e); + assertEquals("", e.evaluate(bean, null)); + } + + public void testParseComposite() { + String exp = "hello ${flag} ${flag} ${flag}"; + Expression e = parser.parseExpression(exp); + assertNotNull(e); + String str = (String)e.evaluate(bean, null); + assertEquals("hello false false false", str); + } + + public void testEnclosedCompositeNotSupported() { + String exp = "${hello ${flag} ${flag} ${flag}}"; + try { + parser.parseExpression(exp); + fail("Should've failed - not intended use"); + } + catch (ParserException e) { + } + } + + public void testSyntaxError1() { + try { + String exp = "hello ${flag} ${abcd defg"; + parser.parseExpression(exp); + fail("Should've failed - not intended use"); + } + catch (ParserException e) { + } + } + + public void testSyntaxError2() { + try { + String exp = "hello ${flag} ${}"; + parser.parseExpression(exp); + fail("Should've failed - not intended use"); + } + catch (ParserException e) { + } + } + + public void testIsDelimitedExpression() { + assertTrue(parser.isDelimitedExpression("${foo}")); + assertTrue(parser.isDelimitedExpression("${foo ${foo}}")); + assertTrue(parser.isDelimitedExpression("foo ${bar}")); + } + + public void testIsNotDelimitedExpression() { + assertFalse(parser.isDelimitedExpression("foo")); + assertFalse(parser.isDelimitedExpression("foo ${")); + assertFalse(parser.isDelimitedExpression("$foo}")); + assertFalse(parser.isDelimitedExpression("foo ${}")); + } +} \ No newline at end of file diff --git a/spring-binding/src/test/java/org/springframework/binding/expression/support/TestBean.java b/spring-binding/src/test/java/org/springframework/binding/expression/support/TestBean.java new file mode 100644 index 00000000..3a0b3916 --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/expression/support/TestBean.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.expression.support; + +import java.util.ArrayList; +import java.util.List; + +public class TestBean { + + private boolean flag; + + private List list = new ArrayList(); + + public boolean isFlag() { + return flag; + } + + public void setFlag(boolean flag) { + this.flag = flag; + } + + public List getList() { + return list; + } + + public void setList(List list) { + this.list = list; + } +} diff --git a/spring-binding/src/test/java/org/springframework/binding/format/support/NumberFormatterTests.java b/spring-binding/src/test/java/org/springframework/binding/format/support/NumberFormatterTests.java new file mode 100644 index 00000000..33a2ce66 --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/format/support/NumberFormatterTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.format.support; + +import java.math.BigDecimal; +import java.util.Locale; + +import org.springframework.binding.format.Formatter; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link NumberFormatterTests}. + * + * @author Erwin Vervaet + */ +public class NumberFormatterTests extends TestCase { + + private Locale systemDefaultLocale; + + protected void setUp() throws Exception { + systemDefaultLocale = Locale.getDefault(); + } + + protected void tearDown() throws Exception { + // restore default + Locale.setDefault(systemDefaultLocale); + } + + public void testParseBigDecimalInUs() { + Locale.setDefault(Locale.US); + Formatter formatter = new SimpleFormatterFactory().getNumberFormatter(BigDecimal.class); + assertEquals(new BigDecimal("123.45"), formatter.parseValue("123.45", BigDecimal.class)); + } + + public void testParseBigDecimalInGermany() { + Locale.setDefault(Locale.GERMANY); + Formatter formatter = new SimpleFormatterFactory().getNumberFormatter(BigDecimal.class); + assertEquals(new BigDecimal("123.45"), formatter.parseValue("123.45", BigDecimal.class)); + } +} diff --git a/spring-binding/src/test/java/org/springframework/binding/mapping/RequiredMappingTests.java b/spring-binding/src/test/java/org/springframework/binding/mapping/RequiredMappingTests.java new file mode 100644 index 00000000..3ad544f7 --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/mapping/RequiredMappingTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.mapping; + +import java.util.HashMap; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.support.OgnlExpressionParser; + +/** + * Unit tests for the {@link org.springframework.binding.mapping.RequiredMapping}. + */ +public class RequiredMappingTests extends TestCase { + + public void testRequired() { + MappingBuilder builder = new MappingBuilder(new OgnlExpressionParser()); + Mapping mapping = builder.source("foo").target("bar").required().value(); + HashMap source = new HashMap(); + source.put("foo", "baz"); + HashMap target = new HashMap(); + mapping.map(source, target, null); + assertEquals("baz", target.get("bar")); + } + + public void testRequiredExceptionOnNull() { + MappingBuilder builder = new MappingBuilder(new OgnlExpressionParser()); + Mapping mapping = builder.source("foo").target("bar").required().value(); + HashMap source = new HashMap(); + source.put("foo", null); + HashMap target = new HashMap(); + try { + mapping.map(source, target, null); + } + catch (RequiredMappingException e) { + } + } + + public void testRequiredExceptionOnNoKey() { + MappingBuilder builder = new MappingBuilder(new OgnlExpressionParser()); + Mapping mapping = builder.source("foo").target("bar").required().value(); + HashMap source = new HashMap(); + HashMap target = new HashMap(); + try { + mapping.map(source, target, null); + } + catch (RequiredMappingException e) { + } + } + +} diff --git a/spring-binding/src/test/java/org/springframework/binding/method/MethodInvokerTests.java b/spring-binding/src/test/java/org/springframework/binding/method/MethodInvokerTests.java new file mode 100644 index 00000000..49b9dc23 --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/method/MethodInvokerTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.method; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.support.StaticExpression; + +/** + * Unit tests for {@link org.springframework.binding.method.MethodInvoker}. + * + * @author Erwin Vervaet + */ +public class MethodInvokerTests extends TestCase { + + private MethodInvoker methodInvoker; + + protected void setUp() throws Exception { + this.methodInvoker = new MethodInvoker(); + } + + public void testInvocationTargetException() { + try { + methodInvoker.invoke(new MethodSignature("test"), new TestObject(), null); + fail(); + } + catch (MethodInvocationException e) { + assertTrue(e.getTargetException() instanceof IllegalArgumentException); + assertEquals("just testing", e.getTargetException().getMessage()); + } + } + + public void testInvalidMethod() { + try { + methodInvoker.invoke(new MethodSignature("bogus"), new TestObject(), null); + fail(); + } + catch (MethodInvocationException e) { + assertTrue(e.getTargetException() instanceof InvalidMethodKeyException); + } + } + + public void testBeanArg() { + Parameters parameters = new Parameters(); + Bean bean = new Bean(); + parameters.add(new Parameter(Bean.class, new StaticExpression(bean))); + MethodSignature method = new MethodSignature("testBeanArg", parameters); + assertSame(bean, methodInvoker.invoke(method, new TestObject(), null)); + } + + public void testPrimitiveArg() { + Parameters parameters = new Parameters(); + parameters.add(new Parameter(Boolean.class, new StaticExpression(Boolean.TRUE))); + MethodSignature method = new MethodSignature("testPrimitiveArg", parameters); + assertEquals(Boolean.TRUE, methodInvoker.invoke(method, new TestObject(), null)); + } + + private static class TestObject { + + public void test() { + throw new IllegalArgumentException("just testing"); + } + + public Object testBeanArg(Bean bean) { + return bean; + } + + public boolean testPrimitiveArg(boolean primitive) { + return primitive; + } + } + + private static class Bean { + String value; + } +} diff --git a/spring-binding/src/test/java/org/springframework/binding/method/MethodKeyTests.java b/spring-binding/src/test/java/org/springframework/binding/method/MethodKeyTests.java new file mode 100644 index 00000000..2ccfa1fb --- /dev/null +++ b/spring-binding/src/test/java/org/springframework/binding/method/MethodKeyTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.binding.method; + +import java.io.File; +import java.io.FilenameFilter; +import java.lang.reflect.Method; + +import junit.framework.TestCase; + +/** + * @author Rob Harrop + * @since 1.0 + */ +public class MethodKeyTests extends TestCase { + + private static final Method LIST_NO_ARGS = safeGetMethod(File.class, "list", null); + + private static final Method LIST_FILENAME_FILTER = safeGetMethod(File.class, "list", + new Class[] { FilenameFilter.class }); + + public void testGetMethodWithNoArgs() throws Exception { + MethodKey key = new MethodKey(File.class, "list", new Class[0]); + Method m = key.getMethod(); + assertEquals(LIST_NO_ARGS, m); + } + + public void testGetMoreGenericMethod() throws Exception { + MethodKey key = new MethodKey(Object.class, "equals", new Class[] { Long.class }); + assertEquals(safeGetMethod(Object.class, "equals", new Class[] { Object.class }), key.getMethod()); + } + + public void testGetMethodWithSingleArg() throws Exception { + MethodKey key = new MethodKey(File.class, "list", new Class[] { FilenameFilter.class }); + Method m = key.getMethod(); + assertEquals(LIST_FILENAME_FILTER, m); + } + + public void testGetMethodWithSingleNullArgAndValidMatch() throws Exception { + MethodKey key = new MethodKey(File.class, "list", new Class[] { null }); + Method m = key.getMethod(); + assertEquals(LIST_FILENAME_FILTER, m); + } + + public void testGetMethodWithSingleNullAndUnclearMatch() throws Exception { + new MethodKey(File.class, "listFiles", new Class[] { null }); + } + + private static final Method safeGetMethod(Class type, String name, Class[] argTypes) { + try { + return type.getMethod(name, argTypes); + } + catch (NoSuchMethodException e) { + throw new IllegalStateException("Unable to safely access a known method via reflection. " + e.getMessage()); + } + } +} diff --git a/spring-webflow-samples/birthdate/.classpath b/spring-webflow-samples/birthdate/.classpath new file mode 100644 index 00000000..1ff9d14b --- /dev/null +++ b/spring-webflow-samples/birthdate/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/spring-webflow-samples/birthdate/.cvsignore b/spring-webflow-samples/birthdate/.cvsignore new file mode 100644 index 00000000..16b87eb5 --- /dev/null +++ b/spring-webflow-samples/birthdate/.cvsignore @@ -0,0 +1,8 @@ +target +lib +bak +build.properties +*.log +*.jpx.local* +*.iws +*.tws diff --git a/spring-webflow-samples/birthdate/.project b/spring-webflow-samples/birthdate/.project new file mode 100644 index 00000000..5c7bd5a9 --- /dev/null +++ b/spring-webflow-samples/birthdate/.project @@ -0,0 +1,36 @@ + + + swf-birthdate + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + + org.springframework.ide.eclipse.core.springnature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + + diff --git a/spring-webflow-samples/birthdate/.settings/org.eclipse.jdt.core.prefs b/spring-webflow-samples/birthdate/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..52db45d8 --- /dev/null +++ b/spring-webflow-samples/birthdate/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.5 diff --git a/spring-webflow-samples/birthdate/.settings/org.eclipse.jdt.ui.prefs b/spring-webflow-samples/birthdate/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..31c851b5 --- /dev/null +++ b/spring-webflow-samples/birthdate/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/spring-webflow-samples/birthdate/.settings/org.eclipse.jst.common.project.facet.core.prefs b/spring-webflow-samples/birthdate/.settings/org.eclipse.jst.common.project.facet.core.prefs new file mode 100755 index 00000000..1973f017 --- /dev/null +++ b/spring-webflow-samples/birthdate/.settings/org.eclipse.jst.common.project.facet.core.prefs @@ -0,0 +1,4 @@ +#Mon Nov 13 17:23:47 PST 2006 +classpath.helper/org.eclipse.jdt.launching.JRE_CONTAINER/owners=jst.java\:5.0 +classpath.helper/org.eclipse.jst.server.core.container\:\:org.eclipse.jst.server.tomcat.runtimeTarget\:\:Apache\ Tomcat\ v5.5/owners=jst.web\:2.4 +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/birthdate/.settings/org.eclipse.wst.common.component b/spring-webflow-samples/birthdate/.settings/org.eclipse.wst.common.component new file mode 100755 index 00000000..8c8a88a6 --- /dev/null +++ b/spring-webflow-samples/birthdate/.settings/org.eclipse.wst.common.component @@ -0,0 +1,81 @@ + + + + + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + + + + diff --git a/spring-webflow-samples/birthdate/.settings/org.eclipse.wst.common.project.facet.core.xml b/spring-webflow-samples/birthdate/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100755 index 00000000..b7687079 --- /dev/null +++ b/spring-webflow-samples/birthdate/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spring-webflow-samples/birthdate/.settings/org.eclipse.wst.validation.prefs b/spring-webflow-samples/birthdate/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 00000000..1c7d6317 --- /dev/null +++ b/spring-webflow-samples/birthdate/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,6 @@ +#Mon Nov 13 17:29:33 PST 2006 +DELEGATES_PREFERENCE=delegateValidatorListorg.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator\=org.eclipse.wst.wsdl.validation.internal.eclipse.Validator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator\=org.eclipse.wst.xsd.core.internal.validation.eclipse.Validator; +USER_BUILD_PREFERENCE=enabledBuildValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_MANUAL_PREFERENCE=enabledManualValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_PREFERENCE=overrideGlobalPreferencesfalse +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/birthdate/.springBeans b/spring-webflow-samples/birthdate/.springBeans new file mode 100644 index 00000000..2225acda --- /dev/null +++ b/spring-webflow-samples/birthdate/.springBeans @@ -0,0 +1,10 @@ + + + + xml + + + + + + diff --git a/spring-webflow-samples/birthdate/build.xml b/spring-webflow-samples/birthdate/build.xml new file mode 100644 index 00000000..0b63534d --- /dev/null +++ b/spring-webflow-samples/birthdate/build.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/spring-webflow-samples/birthdate/ivy.xml b/spring-webflow-samples/birthdate/ivy.xml new file mode 100644 index 00000000..5c2f123a --- /dev/null +++ b/spring-webflow-samples/birthdate/ivy.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/project.properties b/spring-webflow-samples/birthdate/project.properties new file mode 100644 index 00000000..7d989e52 --- /dev/null +++ b/spring-webflow-samples/birthdate/project.properties @@ -0,0 +1,10 @@ +# properties defined in this file are overridable by a local build.properties in this project dir + +# The location of the common build system +common.build.dir=${basedir}/../../common-build + +javac.source=1.3 +javac.target=1.3 + +# Do not publish built artifacts to the integration repository +do.publish=false \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/etc/filter.properties b/spring-webflow-samples/birthdate/src/etc/filter.properties new file mode 100644 index 00000000..c54e5880 --- /dev/null +++ b/spring-webflow-samples/birthdate/src/etc/filter.properties @@ -0,0 +1,33 @@ +# $Header$ + +# Contains filterable project settings. Setting placeholders in filterable project text +# files will be replaced with these values when the 'statics' build target is run. +# +# You may add static settings directly to this source file in the format: +# setting=value e.g MY_SETTING=myvalue +# This is appropriate usage if you know the setting value will never change. +# +# At build time this source file is copied to the ${target.dir} where additional +# dynamic settings may be appended using the task. Use this approach +# when a setting value depends on the build or the local user's environment. +# +# An example of this approach is shown below: +# +# build.xml +# +# +# +# +# +# +# +# +# This allows for dynamic replacement values that are sourced from local properties files to facilitate +# local user settings. +# +# To refer to filterable settings within project source files like config files, JSPs, or +# other text files use the standard ant placeholder format: +# @SETTING_NAME@ e.g, @MY_SETTING@ and @MY_LOCAL_SETTING@ +# +# Your settings: diff --git a/spring-webflow-samples/birthdate/src/etc/test-resources/log4j.properties b/spring-webflow-samples/birthdate/src/etc/test-resources/log4j.properties new file mode 100644 index 00000000..37618f3a --- /dev/null +++ b/spring-webflow-samples/birthdate/src/etc/test-resources/log4j.properties @@ -0,0 +1,15 @@ +log4j.rootCategory=DEBUG, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=@TEST_RESULTS_DIR@/@PROJECT_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n diff --git a/spring-webflow-samples/birthdate/src/main/java/org/springframework/webflow/samples/birthdate/BirthDate.java b/spring-webflow-samples/birthdate/src/main/java/org/springframework/webflow/samples/birthdate/BirthDate.java new file mode 100644 index 00000000..5004adcd --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/java/org/springframework/webflow/samples/birthdate/BirthDate.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.birthdate; + +import java.io.Serializable; +import java.util.Date; + +public class BirthDate implements Serializable { + + private String name; + + private Date date; + + private boolean sendCard; + + private String emailAddress; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getDate() { + return date; + } + + public void setDate(Date date) { + this.date = date; + } + + public boolean isSendCard() { + return sendCard; + } + + public void setSendCard(boolean sendCard) { + this.sendCard = sendCard; + } + + public String getEmailAddress() { + return emailAddress; + } + + public void setEmailAddress(String emailAddress) { + this.emailAddress = emailAddress; + } +} \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/main/java/org/springframework/webflow/samples/birthdate/BirthDateFormAction.java b/spring-webflow-samples/birthdate/src/main/java/org/springframework/webflow/samples/birthdate/BirthDateFormAction.java new file mode 100644 index 00000000..81f924fb --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/java/org/springframework/webflow/samples/birthdate/BirthDateFormAction.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.birthdate; + +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.propertyeditors.CustomDateEditor; +import org.springframework.webflow.action.FormAction; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ScopeType; + +public class BirthDateFormAction extends FormAction { + + // standard European format + private static final String BIRTH_DATE_PATTERN = "dd-MM-yyyy"; + + private static final String AGE_NAME = "age"; + + public BirthDateFormAction() { + // tell the superclass about the form object and validator we want to use + // you could also do this in the application context XML ofcourse + setFormObjectName("birthDate"); + setFormObjectClass(BirthDate.class); + setFormObjectScope(ScopeType.FLOW); + setValidator(new BirthDateValidator()); + } + + protected void registerPropertyEditors(PropertyEditorRegistry registry) { + // register a custom property editor to handle the date input + SimpleDateFormat dateFormat = new SimpleDateFormat(BIRTH_DATE_PATTERN); + registry.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); + } + + /* + * Our "onSubmit" hook: an action execute method. + */ + public Event calculateAge(RequestContext context) throws Exception { + // pull the date from the model + BirthDate birthDate = (BirthDate)getFormObject(context); + + // calculate the age (quick & dirty) + // in a real application you would delegate to the business layer for + // this kind of logic + Calendar calBirthDate = new GregorianCalendar(); + calBirthDate.setTime(birthDate.getDate()); + Calendar calNow = new GregorianCalendar(); + + int ageYears = calNow.get(Calendar.YEAR) - calBirthDate.get(Calendar.YEAR); + long ageMonths = calNow.get(Calendar.MONTH) - calBirthDate.get(Calendar.MONTH); + long ageDays = calNow.get(Calendar.DAY_OF_MONTH) - calBirthDate.get(Calendar.DAY_OF_MONTH); + + if (ageDays < 0) { + ageMonths--; + ageDays += calBirthDate.getActualMaximum(Calendar.DAY_OF_MONTH); + } + + if (ageMonths < 0) { + ageYears--; + ageMonths += 12; + } + + // create a nice age string + StringBuffer ageStr = new StringBuffer(); + ageStr.append(ageYears).append(" years, "); + ageStr.append(ageMonths).append(" months and "); + ageStr.append(ageDays).append(" days"); + + // put it in the model for display by the view + context.getRequestScope().put(AGE_NAME, ageStr); + + return success(); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/main/java/org/springframework/webflow/samples/birthdate/BirthDateValidator.java b/spring-webflow-samples/birthdate/src/main/java/org/springframework/webflow/samples/birthdate/BirthDateValidator.java new file mode 100644 index 00000000..22d659dc --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/java/org/springframework/webflow/samples/birthdate/BirthDateValidator.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.birthdate; + +import java.util.Calendar; + +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +public class BirthDateValidator implements Validator { + + public boolean supports(Class clazz) { + return clazz.equals(BirthDate.class); + } + + public void validate(Object obj, Errors errors) { + BirthDate birthDate = (BirthDate)obj; + validateBirthdateForm(birthDate, errors); + validateCardForm(birthDate, errors); + } + + public void validateBirthdateForm(BirthDate birthDate, Errors errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "name", "noName", "Please specify your name."); + ValidationUtils.rejectIfEmpty(errors, "date", "noDate", "Please specify your birth date."); + } + + public void validateCardForm(BirthDate birthDate, Errors errors) { + if (birthDate.isSendCard()) { + Calendar cal = Calendar.getInstance(); + cal.setTime(birthDate.getDate()); + if (cal.get(Calendar.MONTH) == 11) { + errors.reject("tooGoodForCards", "You're born in December--you're too good for a silly card!"); + } + else { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "emailAddress", "noEmail", + "Please specify your email address."); + } + } + + } +} \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/main/java/org/springframework/webflow/samples/birthdate/MessageResources.properties b/spring-webflow-samples/birthdate/src/main/java/org/springframework/webflow/samples/birthdate/MessageResources.properties new file mode 100644 index 00000000..8af848e1 --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/java/org/springframework/webflow/samples/birthdate/MessageResources.properties @@ -0,0 +1,11 @@ +errors.header= +errors.footer=
+errors.prefix= +errors.suffix= + +noName=Your name is required +noDate=Your birth date is required +noEmail=Your email address is required +typeMismatch.java.util.Date=You entered an invalid date format + +tooGoodForCards=You're born in December--you're too good for a silly card! diff --git a/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/birthdate-alternate.xml b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/birthdate-alternate.xml new file mode 100644 index 00000000..a9764f38 --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/birthdate-alternate.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/birthdate.xml b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/birthdate.xml new file mode 100644 index 00000000..47f80b76 --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/birthdate.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/classes/log4j.properties b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/classes/log4j.properties new file mode 100644 index 00000000..5ba9222c --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/classes/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable web flow logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/birthdateForm.jsp b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/birthdateForm.jsp new file mode 100644 index 00000000..f78f7059 --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/birthdateForm.jsp @@ -0,0 +1,36 @@ +<%@ include file="includeTop.jsp" %> + +
+
+

Enter your birth date

+
+ + + + + + + + + + + + + + + + + +
+ +
Your name + +
Your birth date (DD-MM-YYYY) + +
+ + +
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/cardForm.jsp b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/cardForm.jsp new file mode 100644 index 00000000..21b63bd6 --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/cardForm.jsp @@ -0,0 +1,39 @@ +<%@ include file="includeTop.jsp" %> + +
+
+

Personalized birthday cards

+
+ + + + + + + + + + + + + + + + + +
+ +
Would you like a card to be sent on your birthday? + + + +
If so, enter your email address: + +
+ + +
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/includeBottom.jsp b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/includeBottom.jsp new file mode 100644 index 00000000..3b0f1dd6 --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/includeBottom.jsp @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/includeTop.jsp b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/includeTop.jsp new file mode 100644 index 00000000..666b4688 --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/includeTop.jsp @@ -0,0 +1,21 @@ +<%@ page contentType="text/html" %> +<%@ page session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib uri="http://jakarta.apache.org/struts/tags-html" prefix="html" %> + + + +Calculate your Age + + + + + + + + diff --git a/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/yourAge.jsp b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/yourAge.jsp new file mode 100644 index 00000000..b184f462 --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/jsp/yourAge.jsp @@ -0,0 +1,17 @@ +<%@ include file="includeTop.jsp" %> + +
+
+

Your age

+
+

+ ${birthDate.name}, you are now ${age} old. + You were born on . +

+
+
"> + +
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/struts-config.xml b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/struts-config.xml new file mode 100644 index 00000000..d6197a30 --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/struts-config.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/web.xml b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..8c04d75d --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,33 @@ + + + + + + contextConfigLocation + + /WEB-INF/webflow-config.xml + + + + + org.springframework.web.context.ContextLoaderListener + + + + action + org.apache.struts.action.ActionServlet + + + + action + *.do + + + + index.jsp + + + \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/webflow-config.xml b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/webflow-config.xml new file mode 100644 index 00000000..a1f4ee0d --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/webapp/WEB-INF/webflow-config.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/main/webapp/images/spring-logo.jpg b/spring-webflow-samples/birthdate/src/main/webapp/images/spring-logo.jpg new file mode 100644 index 00000000..62be3983 Binary files /dev/null and b/spring-webflow-samples/birthdate/src/main/webapp/images/spring-logo.jpg differ diff --git a/spring-webflow-samples/birthdate/src/main/webapp/images/submit.gif b/spring-webflow-samples/birthdate/src/main/webapp/images/submit.gif new file mode 100644 index 00000000..1a749d18 Binary files /dev/null and b/spring-webflow-samples/birthdate/src/main/webapp/images/submit.gif differ diff --git a/spring-webflow-samples/birthdate/src/main/webapp/images/webflow-logo.jpg b/spring-webflow-samples/birthdate/src/main/webapp/images/webflow-logo.jpg new file mode 100644 index 00000000..ed76bae0 Binary files /dev/null and b/spring-webflow-samples/birthdate/src/main/webapp/images/webflow-logo.jpg differ diff --git a/spring-webflow-samples/birthdate/src/main/webapp/index.jsp b/spring-webflow-samples/birthdate/src/main/webapp/index.jsp new file mode 100644 index 00000000..fe4c8377 --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/webapp/index.jsp @@ -0,0 +1,36 @@ +<%-- make sure we have a session --%> +<%@ page session="true" %> + + + + + + + +
Birth Date - A Spring Web Flow Sample
+ +
+ +
+

+ Birth Date +

+ +

+ This sample application demonstrates the use of the FormAction: a multi-action. + It also shows you how to use a multi-action to group all actions executed by a flow + in a single class. +

+

+ This sample also demonstrates Spring Web Flow's Struts integration.

+

+ Finally, the sample also uses JSTL in conjunction with flows. +

+
+ +
+ +
+ + + diff --git a/spring-webflow-samples/birthdate/src/main/webapp/style.css b/spring-webflow-samples/birthdate/src/main/webapp/style.css new file mode 100644 index 00000000..f4b0a64e --- /dev/null +++ b/spring-webflow-samples/birthdate/src/main/webapp/style.css @@ -0,0 +1,58 @@ +body { + width: 720px; + margin: 0px; + padding: 0px; +} + +div#logo { + width: 720px; + height: 73px; + background: #86AEA5; +} + +div#navigation { + width: 720px; + height: 15px; + background: #E2F3B8; + text-align: right; +} + +div#content { + width: 720px; + padding: 5px; +} + +div#insert { + width: 120; + float: right; + text-align: right; +} + +.buttonBar { + height: 1.5em; + text-align: right; +} + +div#copyright { + width: 720px; +} + +div#copyright p { + text-align: center; + font-family: Tahoma, sans-serif; + font-size: 75%; + color: div#336633; + margin-left: 5px; + font-weight: bold; + clear: both; +} + +.readOnly { + color: rgb(192, 192, 192); +} + +.error { + color: red; + font-weight: bold; + font-family: Arial, sans-serif; +} \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/test/java/log4j.properties b/spring-webflow-samples/birthdate/src/test/java/log4j.properties new file mode 100644 index 00000000..5ba9222c --- /dev/null +++ b/spring-webflow-samples/birthdate/src/test/java/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable web flow logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-webflow-samples/birthdate/src/test/java/org/springframework/webflow/samples/birthdate/BirthdateValidatorTests.java b/spring-webflow-samples/birthdate/src/test/java/org/springframework/webflow/samples/birthdate/BirthdateValidatorTests.java new file mode 100644 index 00000000..e62de52e --- /dev/null +++ b/spring-webflow-samples/birthdate/src/test/java/org/springframework/webflow/samples/birthdate/BirthdateValidatorTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.birthdate; + +import java.text.SimpleDateFormat; + +import junit.framework.TestCase; + +import org.springframework.validation.BindException; + +public class BirthdateValidatorTests extends TestCase { + + public void testValidateCardForm() throws Exception { + BirthDateValidator validator = new BirthDateValidator(); + BirthDate birthDate = new BirthDate(); + birthDate.setName("Keith"); + SimpleDateFormat format = new SimpleDateFormat("dd/MM/yyyy"); + birthDate.setSendCard(true); + birthDate.setDate(format.parse("29/12/1977")); + BindException errors = new BindException(birthDate, "birthDate"); + validator.validateCardForm(birthDate, errors); + assertEquals(1, errors.getAllErrors().size()); + } +} diff --git a/spring-webflow-samples/fileupload/.classpath b/spring-webflow-samples/fileupload/.classpath new file mode 100644 index 00000000..731cc472 --- /dev/null +++ b/spring-webflow-samples/fileupload/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/spring-webflow-samples/fileupload/.cvsignore b/spring-webflow-samples/fileupload/.cvsignore new file mode 100644 index 00000000..16b87eb5 --- /dev/null +++ b/spring-webflow-samples/fileupload/.cvsignore @@ -0,0 +1,8 @@ +target +lib +bak +build.properties +*.log +*.jpx.local* +*.iws +*.tws diff --git a/spring-webflow-samples/fileupload/.project b/spring-webflow-samples/fileupload/.project new file mode 100644 index 00000000..dd0deb24 --- /dev/null +++ b/spring-webflow-samples/fileupload/.project @@ -0,0 +1,36 @@ + + + swf-phonebook + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + + org.springframework.ide.eclipse.core.springnature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + + diff --git a/spring-webflow-samples/fileupload/.settings/org.eclipse.jdt.core.prefs b/spring-webflow-samples/fileupload/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..52db45d8 --- /dev/null +++ b/spring-webflow-samples/fileupload/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.5 diff --git a/spring-webflow-samples/fileupload/.settings/org.eclipse.jdt.ui.prefs b/spring-webflow-samples/fileupload/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..31c851b5 --- /dev/null +++ b/spring-webflow-samples/fileupload/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/spring-webflow-samples/fileupload/.settings/org.eclipse.jst.common.project.facet.core.prefs b/spring-webflow-samples/fileupload/.settings/org.eclipse.jst.common.project.facet.core.prefs new file mode 100755 index 00000000..eca444d7 --- /dev/null +++ b/spring-webflow-samples/fileupload/.settings/org.eclipse.jst.common.project.facet.core.prefs @@ -0,0 +1,3 @@ +#Thu Nov 23 20:59:37 CET 2006 +classpath.helper/org.eclipse.jdt.launching.JRE_CONTAINER\:\:org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType\:\:jdk1.5.0_08/owners=jst.java\:5.0 +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/fileupload/.settings/org.eclipse.wst.common.component b/spring-webflow-samples/fileupload/.settings/org.eclipse.wst.common.component new file mode 100755 index 00000000..bad147c4 --- /dev/null +++ b/spring-webflow-samples/fileupload/.settings/org.eclipse.wst.common.component @@ -0,0 +1,57 @@ + + + + + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + + + + diff --git a/spring-webflow-samples/fileupload/.settings/org.eclipse.wst.common.project.facet.core.xml b/spring-webflow-samples/fileupload/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100755 index 00000000..2f339010 --- /dev/null +++ b/spring-webflow-samples/fileupload/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spring-webflow-samples/fileupload/.settings/org.eclipse.wst.validation.prefs b/spring-webflow-samples/fileupload/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 00000000..1c7d6317 --- /dev/null +++ b/spring-webflow-samples/fileupload/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,6 @@ +#Mon Nov 13 17:29:33 PST 2006 +DELEGATES_PREFERENCE=delegateValidatorListorg.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator\=org.eclipse.wst.wsdl.validation.internal.eclipse.Validator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator\=org.eclipse.wst.xsd.core.internal.validation.eclipse.Validator; +USER_BUILD_PREFERENCE=enabledBuildValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_MANUAL_PREFERENCE=enabledManualValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_PREFERENCE=overrideGlobalPreferencesfalse +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/fileupload/.springBeans b/spring-webflow-samples/fileupload/.springBeans new file mode 100644 index 00000000..eaaa3efc --- /dev/null +++ b/spring-webflow-samples/fileupload/.springBeans @@ -0,0 +1,8 @@ + + + + src/main/webapp/WEB-INF/fileupload-servlet.xml + + + + diff --git a/spring-webflow-samples/fileupload/build.xml b/spring-webflow-samples/fileupload/build.xml new file mode 100644 index 00000000..3074408b --- /dev/null +++ b/spring-webflow-samples/fileupload/build.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/spring-webflow-samples/fileupload/ivy.xml b/spring-webflow-samples/fileupload/ivy.xml new file mode 100644 index 00000000..c38f8046 --- /dev/null +++ b/spring-webflow-samples/fileupload/ivy.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/fileupload/project.properties b/spring-webflow-samples/fileupload/project.properties new file mode 100644 index 00000000..7d989e52 --- /dev/null +++ b/spring-webflow-samples/fileupload/project.properties @@ -0,0 +1,10 @@ +# properties defined in this file are overridable by a local build.properties in this project dir + +# The location of the common build system +common.build.dir=${basedir}/../../common-build + +javac.source=1.3 +javac.target=1.3 + +# Do not publish built artifacts to the integration repository +do.publish=false \ No newline at end of file diff --git a/spring-webflow-samples/fileupload/src/etc/filter.properties b/spring-webflow-samples/fileupload/src/etc/filter.properties new file mode 100644 index 00000000..c54e5880 --- /dev/null +++ b/spring-webflow-samples/fileupload/src/etc/filter.properties @@ -0,0 +1,33 @@ +# $Header$ + +# Contains filterable project settings. Setting placeholders in filterable project text +# files will be replaced with these values when the 'statics' build target is run. +# +# You may add static settings directly to this source file in the format: +# setting=value e.g MY_SETTING=myvalue +# This is appropriate usage if you know the setting value will never change. +# +# At build time this source file is copied to the ${target.dir} where additional +# dynamic settings may be appended using the task. Use this approach +# when a setting value depends on the build or the local user's environment. +# +# An example of this approach is shown below: +# +# build.xml +# +# +# +# +# +# +# +# +# This allows for dynamic replacement values that are sourced from local properties files to facilitate +# local user settings. +# +# To refer to filterable settings within project source files like config files, JSPs, or +# other text files use the standard ant placeholder format: +# @SETTING_NAME@ e.g, @MY_SETTING@ and @MY_LOCAL_SETTING@ +# +# Your settings: diff --git a/spring-webflow-samples/fileupload/src/etc/test-resources/log4j.properties b/spring-webflow-samples/fileupload/src/etc/test-resources/log4j.properties new file mode 100644 index 00000000..37618f3a --- /dev/null +++ b/spring-webflow-samples/fileupload/src/etc/test-resources/log4j.properties @@ -0,0 +1,15 @@ +log4j.rootCategory=DEBUG, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=@TEST_RESULTS_DIR@/@PROJECT_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n diff --git a/spring-webflow-samples/fileupload/src/main/java/org/springframework/webflow/samples/fileupload/FileUploadAction.java b/spring-webflow-samples/fileupload/src/main/java/org/springframework/webflow/samples/fileupload/FileUploadAction.java new file mode 100644 index 00000000..80f9403a --- /dev/null +++ b/spring-webflow-samples/fileupload/src/main/java/org/springframework/webflow/samples/fileupload/FileUploadAction.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.fileupload; + +import org.springframework.web.multipart.MultipartFile; +import org.springframework.webflow.action.AbstractAction; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +public class FileUploadAction extends AbstractAction { + + protected Event doExecute(RequestContext context) throws Exception { + MultipartFile file = context.getRequestParameters().getRequiredMultipartFile("file"); + if (file.getSize() > 0) { + // data was uploaded + context.getFlashScope().put("file", new String(file.getBytes())); + return success(); + } + else { + return error(); + } + } +} diff --git a/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/classes/log4j.properties b/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/classes/log4j.properties new file mode 100644 index 00000000..5ba9222c --- /dev/null +++ b/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/classes/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable web flow logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/fileupload-servlet.xml b/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/fileupload-servlet.xml new file mode 100644 index 00000000..c3e304eb --- /dev/null +++ b/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/fileupload-servlet.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/fileupload.xml b/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/fileupload.xml new file mode 100644 index 00000000..9febbc81 --- /dev/null +++ b/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/fileupload.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/jsp/fileForm.jsp b/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/jsp/fileForm.jsp new file mode 100644 index 00000000..cc74dd2d --- /dev/null +++ b/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/jsp/fileForm.jsp @@ -0,0 +1,38 @@ +<%@ include file="includeTop.jsp" %> + +
+
+ +
+

Select the file to upload

+
+ + +

File uploaded succesfully.

+ +
${file}
+
+
+ + + + + + + + + + + + +
+ File: + + +
+ + +
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/jsp/includeBottom.jsp b/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/jsp/includeBottom.jsp new file mode 100644 index 00000000..3b0f1dd6 --- /dev/null +++ b/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/jsp/includeBottom.jsp @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/jsp/includeTop.jsp b/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/jsp/includeTop.jsp new file mode 100644 index 00000000..ad39cc3d --- /dev/null +++ b/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/jsp/includeTop.jsp @@ -0,0 +1,21 @@ +<%@ page contentType="text/html" %> +<%@ page session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + + + +Upload a file + + + + + + + + diff --git a/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/web.xml b/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..a3ba7b6b --- /dev/null +++ b/spring-webflow-samples/fileupload/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,22 @@ + + + + + + fileupload + org.springframework.web.servlet.DispatcherServlet + + + + fileupload + *.htm + + + + index.jsp + + + \ No newline at end of file diff --git a/spring-webflow-samples/fileupload/src/main/webapp/images/spring-logo.jpg b/spring-webflow-samples/fileupload/src/main/webapp/images/spring-logo.jpg new file mode 100644 index 00000000..62be3983 Binary files /dev/null and b/spring-webflow-samples/fileupload/src/main/webapp/images/spring-logo.jpg differ diff --git a/spring-webflow-samples/fileupload/src/main/webapp/images/webflow-logo.jpg b/spring-webflow-samples/fileupload/src/main/webapp/images/webflow-logo.jpg new file mode 100644 index 00000000..ed76bae0 Binary files /dev/null and b/spring-webflow-samples/fileupload/src/main/webapp/images/webflow-logo.jpg differ diff --git a/spring-webflow-samples/fileupload/src/main/webapp/index.jsp b/spring-webflow-samples/fileupload/src/main/webapp/index.jsp new file mode 100644 index 00000000..68a6849d --- /dev/null +++ b/spring-webflow-samples/fileupload/src/main/webapp/index.jsp @@ -0,0 +1,33 @@ +<%@ page session="true" %> <%-- make sure we have a session --%> + + + + + + + +
File Upload - A Spring Web Flow Sample
+ +
+ +
+

+ File Upload +

+ +

+ This sample application illustrates dealing with file uploads in a + Spring web flow based application. Consult the + Spring reference documentation + for more information about the techniques used here.
+ This implementation uses Jakarta commons FileUpload + to process multipart requests. +

+
+ +
+ +
+ + + diff --git a/spring-webflow-samples/fileupload/src/main/webapp/style.css b/spring-webflow-samples/fileupload/src/main/webapp/style.css new file mode 100644 index 00000000..f4b0a64e --- /dev/null +++ b/spring-webflow-samples/fileupload/src/main/webapp/style.css @@ -0,0 +1,58 @@ +body { + width: 720px; + margin: 0px; + padding: 0px; +} + +div#logo { + width: 720px; + height: 73px; + background: #86AEA5; +} + +div#navigation { + width: 720px; + height: 15px; + background: #E2F3B8; + text-align: right; +} + +div#content { + width: 720px; + padding: 5px; +} + +div#insert { + width: 120; + float: right; + text-align: right; +} + +.buttonBar { + height: 1.5em; + text-align: right; +} + +div#copyright { + width: 720px; +} + +div#copyright p { + text-align: center; + font-family: Tahoma, sans-serif; + font-size: 75%; + color: div#336633; + margin-left: 5px; + font-weight: bold; + clear: both; +} + +.readOnly { + color: rgb(192, 192, 192); +} + +.error { + color: red; + font-weight: bold; + font-family: Arial, sans-serif; +} \ No newline at end of file diff --git a/spring-webflow-samples/flowlauncher/.classpath b/spring-webflow-samples/flowlauncher/.classpath new file mode 100644 index 00000000..731cc472 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/spring-webflow-samples/flowlauncher/.cvsignore b/spring-webflow-samples/flowlauncher/.cvsignore new file mode 100644 index 00000000..16b87eb5 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/.cvsignore @@ -0,0 +1,8 @@ +target +lib +bak +build.properties +*.log +*.jpx.local* +*.iws +*.tws diff --git a/spring-webflow-samples/flowlauncher/.project b/spring-webflow-samples/flowlauncher/.project new file mode 100644 index 00000000..8a6b56a9 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/.project @@ -0,0 +1,36 @@ + + + swf-flowlauncher + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + + org.springframework.ide.eclipse.core.springnature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + + diff --git a/spring-webflow-samples/flowlauncher/.settings/org.eclipse.jdt.core.prefs b/spring-webflow-samples/flowlauncher/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..52db45d8 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.5 diff --git a/spring-webflow-samples/flowlauncher/.settings/org.eclipse.jdt.ui.prefs b/spring-webflow-samples/flowlauncher/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..31c851b5 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/spring-webflow-samples/flowlauncher/.settings/org.eclipse.jst.common.project.facet.core.prefs b/spring-webflow-samples/flowlauncher/.settings/org.eclipse.jst.common.project.facet.core.prefs new file mode 100755 index 00000000..1973f017 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/.settings/org.eclipse.jst.common.project.facet.core.prefs @@ -0,0 +1,4 @@ +#Mon Nov 13 17:23:47 PST 2006 +classpath.helper/org.eclipse.jdt.launching.JRE_CONTAINER/owners=jst.java\:5.0 +classpath.helper/org.eclipse.jst.server.core.container\:\:org.eclipse.jst.server.tomcat.runtimeTarget\:\:Apache\ Tomcat\ v5.5/owners=jst.web\:2.4 +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/flowlauncher/.settings/org.eclipse.wst.common.component b/spring-webflow-samples/flowlauncher/.settings/org.eclipse.wst.common.component new file mode 100755 index 00000000..4366568d --- /dev/null +++ b/spring-webflow-samples/flowlauncher/.settings/org.eclipse.wst.common.component @@ -0,0 +1,51 @@ + + + + + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + + + + diff --git a/spring-webflow-samples/flowlauncher/.settings/org.eclipse.wst.common.project.facet.core.xml b/spring-webflow-samples/flowlauncher/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100755 index 00000000..b7687079 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spring-webflow-samples/flowlauncher/.settings/org.eclipse.wst.validation.prefs b/spring-webflow-samples/flowlauncher/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 00000000..1c7d6317 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,6 @@ +#Mon Nov 13 17:29:33 PST 2006 +DELEGATES_PREFERENCE=delegateValidatorListorg.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator\=org.eclipse.wst.wsdl.validation.internal.eclipse.Validator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator\=org.eclipse.wst.xsd.core.internal.validation.eclipse.Validator; +USER_BUILD_PREFERENCE=enabledBuildValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_MANUAL_PREFERENCE=enabledManualValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_PREFERENCE=overrideGlobalPreferencesfalse +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/flowlauncher/.springBeans b/spring-webflow-samples/flowlauncher/.springBeans new file mode 100644 index 00000000..79942b6a --- /dev/null +++ b/spring-webflow-samples/flowlauncher/.springBeans @@ -0,0 +1,8 @@ + + + + src/main/webapp/WEB-INF/flowlauncher-servlet.xml + + + + diff --git a/spring-webflow-samples/flowlauncher/build.xml b/spring-webflow-samples/flowlauncher/build.xml new file mode 100644 index 00000000..d431f206 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/build.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/spring-webflow-samples/flowlauncher/ivy.xml b/spring-webflow-samples/flowlauncher/ivy.xml new file mode 100644 index 00000000..766423e9 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/ivy.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/flowlauncher/project.properties b/spring-webflow-samples/flowlauncher/project.properties new file mode 100644 index 00000000..7d989e52 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/project.properties @@ -0,0 +1,10 @@ +# properties defined in this file are overridable by a local build.properties in this project dir + +# The location of the common build system +common.build.dir=${basedir}/../../common-build + +javac.source=1.3 +javac.target=1.3 + +# Do not publish built artifacts to the integration repository +do.publish=false \ No newline at end of file diff --git a/spring-webflow-samples/flowlauncher/src/etc/filter.properties b/spring-webflow-samples/flowlauncher/src/etc/filter.properties new file mode 100644 index 00000000..c54e5880 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/src/etc/filter.properties @@ -0,0 +1,33 @@ +# $Header$ + +# Contains filterable project settings. Setting placeholders in filterable project text +# files will be replaced with these values when the 'statics' build target is run. +# +# You may add static settings directly to this source file in the format: +# setting=value e.g MY_SETTING=myvalue +# This is appropriate usage if you know the setting value will never change. +# +# At build time this source file is copied to the ${target.dir} where additional +# dynamic settings may be appended using the task. Use this approach +# when a setting value depends on the build or the local user's environment. +# +# An example of this approach is shown below: +# +# build.xml +# +# +# +# +# +# +# +# +# This allows for dynamic replacement values that are sourced from local properties files to facilitate +# local user settings. +# +# To refer to filterable settings within project source files like config files, JSPs, or +# other text files use the standard ant placeholder format: +# @SETTING_NAME@ e.g, @MY_SETTING@ and @MY_LOCAL_SETTING@ +# +# Your settings: diff --git a/spring-webflow-samples/flowlauncher/src/etc/test-resources/log4j.properties b/spring-webflow-samples/flowlauncher/src/etc/test-resources/log4j.properties new file mode 100644 index 00000000..37618f3a --- /dev/null +++ b/spring-webflow-samples/flowlauncher/src/etc/test-resources/log4j.properties @@ -0,0 +1,15 @@ +log4j.rootCategory=DEBUG, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=@TEST_RESULTS_DIR@/@PROJECT_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n diff --git a/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/classes/log4j.properties b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/classes/log4j.properties new file mode 100644 index 00000000..5ba9222c --- /dev/null +++ b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/classes/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable web flow logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/flowlauncher-servlet.xml b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/flowlauncher-servlet.xml new file mode 100644 index 00000000..6eb73174 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/flowlauncher-servlet.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/jsp/aPage.jsp b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/jsp/aPage.jsp new file mode 100644 index 00000000..80b2ed8b --- /dev/null +++ b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/jsp/aPage.jsp @@ -0,0 +1,110 @@ +<%@ include file="includeTop.jsp" %> + +
+ Sample A Flow +
+ Flow input was: ${input}
+
+ From Sample A you may terminate Sample A and launch Sample B from an end state of Sample A. + You may also pass Sample B input. This can be done using: + + Alternatively, you may spawn Sample B as a sub flow of Sample A. In this case a flow + attribute mapper maps the input stored in the FlowScope of Sample A down to the spawning subflow + Here again you have the option of using: + + Yet another option is to launch Sample B as a top-level flow without involving Sample A as: + +
"> + +
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/jsp/bPage.jsp b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/jsp/bPage.jsp new file mode 100644 index 00000000..f345c9bc --- /dev/null +++ b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/jsp/bPage.jsp @@ -0,0 +1,45 @@ +<%@ include file="includeTop.jsp" %> + +
+ Sample B Flow +
+ Flow input was: ${input}
+ +
+ Sample B is now running as a sub flow within Sample A. This means we can end Sample B and + return to the parent flow. We can do this using either: + +
+
+
"> + +
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/jsp/includeBottom.jsp b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/jsp/includeBottom.jsp new file mode 100644 index 00000000..3b0f1dd6 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/jsp/includeBottom.jsp @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/jsp/includeTop.jsp b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/jsp/includeTop.jsp new file mode 100644 index 00000000..5a677d34 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/jsp/includeTop.jsp @@ -0,0 +1,21 @@ +<%@ page contentType="text/html" %> +<%@ page session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + + + +Launch a flow + + + + + + + + diff --git a/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/sampleA.xml b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/sampleA.xml new file mode 100644 index 00000000..0b9ec7c0 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/sampleA.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/sampleB.xml b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/sampleB.xml new file mode 100644 index 00000000..4cbf61ed --- /dev/null +++ b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/sampleB.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/web.xml b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..2d693640 --- /dev/null +++ b/spring-webflow-samples/flowlauncher/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,22 @@ + + + + + + flowlauncher + org.springframework.web.servlet.DispatcherServlet + + + + flowlauncher + *.htm + + + + index.html + + + \ No newline at end of file diff --git a/spring-webflow-samples/flowlauncher/src/main/webapp/images/spring-logo.jpg b/spring-webflow-samples/flowlauncher/src/main/webapp/images/spring-logo.jpg new file mode 100644 index 00000000..62be3983 Binary files /dev/null and b/spring-webflow-samples/flowlauncher/src/main/webapp/images/spring-logo.jpg differ diff --git a/spring-webflow-samples/flowlauncher/src/main/webapp/images/webflow-logo.jpg b/spring-webflow-samples/flowlauncher/src/main/webapp/images/webflow-logo.jpg new file mode 100644 index 00000000..ed76bae0 Binary files /dev/null and b/spring-webflow-samples/flowlauncher/src/main/webapp/images/webflow-logo.jpg differ diff --git a/spring-webflow-samples/flowlauncher/src/main/webapp/index.html b/spring-webflow-samples/flowlauncher/src/main/webapp/index.html new file mode 100644 index 00000000..074ea7aa --- /dev/null +++ b/spring-webflow-samples/flowlauncher/src/main/webapp/index.html @@ -0,0 +1,72 @@ + + + + + +
Flow Launcher - A Spring Web Flow Sample
+
+
+

+ This sample application demonstrates different ways of launching flows with + input parameters. +

+

+ You can launch the Sample A flow as a top-level flow and pass it input using + request parameters. This can be done using + + + + + + + + + +
an anchor:Start Sample A
or a form: + + + + + + + + + +
+ + +
+
+

+

+ The same is true for Sample B: + + + + + + + + + +
an anchor:Start Sample B
or a form: + + + + + + + + + +
+ + +
+
+

+
+
+
+ + \ No newline at end of file diff --git a/spring-webflow-samples/flowlauncher/src/main/webapp/style.css b/spring-webflow-samples/flowlauncher/src/main/webapp/style.css new file mode 100644 index 00000000..f4b0a64e --- /dev/null +++ b/spring-webflow-samples/flowlauncher/src/main/webapp/style.css @@ -0,0 +1,58 @@ +body { + width: 720px; + margin: 0px; + padding: 0px; +} + +div#logo { + width: 720px; + height: 73px; + background: #86AEA5; +} + +div#navigation { + width: 720px; + height: 15px; + background: #E2F3B8; + text-align: right; +} + +div#content { + width: 720px; + padding: 5px; +} + +div#insert { + width: 120; + float: right; + text-align: right; +} + +.buttonBar { + height: 1.5em; + text-align: right; +} + +div#copyright { + width: 720px; +} + +div#copyright p { + text-align: center; + font-family: Tahoma, sans-serif; + font-size: 75%; + color: div#336633; + margin-left: 5px; + font-weight: bold; + clear: both; +} + +.readOnly { + color: rgb(192, 192, 192); +} + +.error { + color: red; + font-weight: bold; + font-family: Arial, sans-serif; +} \ No newline at end of file diff --git a/spring-webflow-samples/itemlist/.classpath b/spring-webflow-samples/itemlist/.classpath new file mode 100644 index 00000000..731cc472 --- /dev/null +++ b/spring-webflow-samples/itemlist/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/spring-webflow-samples/itemlist/.cvsignore b/spring-webflow-samples/itemlist/.cvsignore new file mode 100644 index 00000000..16b87eb5 --- /dev/null +++ b/spring-webflow-samples/itemlist/.cvsignore @@ -0,0 +1,8 @@ +target +lib +bak +build.properties +*.log +*.jpx.local* +*.iws +*.tws diff --git a/spring-webflow-samples/itemlist/.project b/spring-webflow-samples/itemlist/.project new file mode 100644 index 00000000..1f3ffe82 --- /dev/null +++ b/spring-webflow-samples/itemlist/.project @@ -0,0 +1,36 @@ + + + swf-itemlist + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + + org.springframework.ide.eclipse.core.springnature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + + diff --git a/spring-webflow-samples/itemlist/.settings/org.eclipse.jdt.core.prefs b/spring-webflow-samples/itemlist/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..52db45d8 --- /dev/null +++ b/spring-webflow-samples/itemlist/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.5 diff --git a/spring-webflow-samples/itemlist/.settings/org.eclipse.jdt.ui.prefs b/spring-webflow-samples/itemlist/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..31c851b5 --- /dev/null +++ b/spring-webflow-samples/itemlist/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/spring-webflow-samples/itemlist/.settings/org.eclipse.jst.common.project.facet.core.prefs b/spring-webflow-samples/itemlist/.settings/org.eclipse.jst.common.project.facet.core.prefs new file mode 100755 index 00000000..1973f017 --- /dev/null +++ b/spring-webflow-samples/itemlist/.settings/org.eclipse.jst.common.project.facet.core.prefs @@ -0,0 +1,4 @@ +#Mon Nov 13 17:23:47 PST 2006 +classpath.helper/org.eclipse.jdt.launching.JRE_CONTAINER/owners=jst.java\:5.0 +classpath.helper/org.eclipse.jst.server.core.container\:\:org.eclipse.jst.server.tomcat.runtimeTarget\:\:Apache\ Tomcat\ v5.5/owners=jst.web\:2.4 +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/itemlist/.settings/org.eclipse.wst.common.component b/spring-webflow-samples/itemlist/.settings/org.eclipse.wst.common.component new file mode 100755 index 00000000..3d79d17d --- /dev/null +++ b/spring-webflow-samples/itemlist/.settings/org.eclipse.wst.common.component @@ -0,0 +1,51 @@ + + + + + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + + + + diff --git a/spring-webflow-samples/itemlist/.settings/org.eclipse.wst.common.project.facet.core.xml b/spring-webflow-samples/itemlist/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100755 index 00000000..b7687079 --- /dev/null +++ b/spring-webflow-samples/itemlist/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spring-webflow-samples/itemlist/.settings/org.eclipse.wst.validation.prefs b/spring-webflow-samples/itemlist/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 00000000..1c7d6317 --- /dev/null +++ b/spring-webflow-samples/itemlist/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,6 @@ +#Mon Nov 13 17:29:33 PST 2006 +DELEGATES_PREFERENCE=delegateValidatorListorg.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator\=org.eclipse.wst.wsdl.validation.internal.eclipse.Validator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator\=org.eclipse.wst.xsd.core.internal.validation.eclipse.Validator; +USER_BUILD_PREFERENCE=enabledBuildValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_MANUAL_PREFERENCE=enabledManualValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_PREFERENCE=overrideGlobalPreferencesfalse +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/itemlist/.springBeans b/spring-webflow-samples/itemlist/.springBeans new file mode 100644 index 00000000..6a06111f --- /dev/null +++ b/spring-webflow-samples/itemlist/.springBeans @@ -0,0 +1,8 @@ + + + + src/main/webapp/WEB-INF/itemlist-servlet.xml + + + + diff --git a/spring-webflow-samples/itemlist/build.xml b/spring-webflow-samples/itemlist/build.xml new file mode 100644 index 00000000..c5870ca6 --- /dev/null +++ b/spring-webflow-samples/itemlist/build.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/spring-webflow-samples/itemlist/ivy.xml b/spring-webflow-samples/itemlist/ivy.xml new file mode 100644 index 00000000..d27f8494 --- /dev/null +++ b/spring-webflow-samples/itemlist/ivy.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/itemlist/project.properties b/spring-webflow-samples/itemlist/project.properties new file mode 100644 index 00000000..7d989e52 --- /dev/null +++ b/spring-webflow-samples/itemlist/project.properties @@ -0,0 +1,10 @@ +# properties defined in this file are overridable by a local build.properties in this project dir + +# The location of the common build system +common.build.dir=${basedir}/../../common-build + +javac.source=1.3 +javac.target=1.3 + +# Do not publish built artifacts to the integration repository +do.publish=false \ No newline at end of file diff --git a/spring-webflow-samples/itemlist/src/etc/filter.properties b/spring-webflow-samples/itemlist/src/etc/filter.properties new file mode 100644 index 00000000..c54e5880 --- /dev/null +++ b/spring-webflow-samples/itemlist/src/etc/filter.properties @@ -0,0 +1,33 @@ +# $Header$ + +# Contains filterable project settings. Setting placeholders in filterable project text +# files will be replaced with these values when the 'statics' build target is run. +# +# You may add static settings directly to this source file in the format: +# setting=value e.g MY_SETTING=myvalue +# This is appropriate usage if you know the setting value will never change. +# +# At build time this source file is copied to the ${target.dir} where additional +# dynamic settings may be appended using the task. Use this approach +# when a setting value depends on the build or the local user's environment. +# +# An example of this approach is shown below: +# +# build.xml +# +# +# +# +# +# +# +# +# This allows for dynamic replacement values that are sourced from local properties files to facilitate +# local user settings. +# +# To refer to filterable settings within project source files like config files, JSPs, or +# other text files use the standard ant placeholder format: +# @SETTING_NAME@ e.g, @MY_SETTING@ and @MY_LOCAL_SETTING@ +# +# Your settings: diff --git a/spring-webflow-samples/itemlist/src/etc/test-resources/log4j.properties b/spring-webflow-samples/itemlist/src/etc/test-resources/log4j.properties new file mode 100644 index 00000000..37618f3a --- /dev/null +++ b/spring-webflow-samples/itemlist/src/etc/test-resources/log4j.properties @@ -0,0 +1,15 @@ +log4j.rootCategory=DEBUG, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=@TEST_RESULTS_DIR@/@PROJECT_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n diff --git a/spring-webflow-samples/itemlist/src/main/java/org/springframework/webflow/samples/itemlist/AddItemAction.java b/spring-webflow-samples/itemlist/src/main/java/org/springframework/webflow/samples/itemlist/AddItemAction.java new file mode 100644 index 00000000..fcb4c3cc --- /dev/null +++ b/spring-webflow-samples/itemlist/src/main/java/org/springframework/webflow/samples/itemlist/AddItemAction.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.itemlist; + +import java.util.Collection; + +import org.springframework.webflow.action.AbstractAction; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +public class AddItemAction extends AbstractAction { + + protected Event doExecute(RequestContext context) throws Exception { + Collection list = context.getFlowScope().getRequiredCollection("list"); + String data = context.getRequestParameters().get("data"); + if (data != null && data.length() > 0) { + list.add(data); + } + + try { + // add a bit of artificial think time + Thread.sleep(2000); + } + catch (InterruptedException e) { + } + + return success(); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/itemlist/src/main/java/org/springframework/webflow/samples/itemlist/DataMapper.java b/spring-webflow-samples/itemlist/src/main/java/org/springframework/webflow/samples/itemlist/DataMapper.java new file mode 100644 index 00000000..1f497b0d --- /dev/null +++ b/spring-webflow-samples/itemlist/src/main/java/org/springframework/webflow/samples/itemlist/DataMapper.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.itemlist; + +import org.springframework.binding.mapping.DefaultAttributeMapper; +import org.springframework.binding.mapping.MappingBuilder; +import org.springframework.webflow.core.DefaultExpressionParserFactory; + +public class DataMapper extends DefaultAttributeMapper { + + public DataMapper() { + MappingBuilder mapping = new MappingBuilder(DefaultExpressionParserFactory.getExpressionParser()); + addMapping(mapping.source("requestParameters.data").target("flowScope.item").value()); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/itemlist/src/main/java/org/springframework/webflow/samples/itemlist/NewItemAction.java b/spring-webflow-samples/itemlist/src/main/java/org/springframework/webflow/samples/itemlist/NewItemAction.java new file mode 100644 index 00000000..cc987f2b --- /dev/null +++ b/spring-webflow-samples/itemlist/src/main/java/org/springframework/webflow/samples/itemlist/NewItemAction.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.itemlist; + +import org.springframework.webflow.action.AbstractAction; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +public class NewItemAction extends AbstractAction { + + protected Event doExecute(RequestContext context) throws Exception { + return success(); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/classes/log4j.properties b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/classes/log4j.properties new file mode 100644 index 00000000..5ba9222c --- /dev/null +++ b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/classes/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable web flow logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/itemlist-alternate.xml b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/itemlist-alternate.xml new file mode 100644 index 00000000..0b33f67d --- /dev/null +++ b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/itemlist-alternate.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/itemlist-servlet.xml b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/itemlist-servlet.xml new file mode 100644 index 00000000..c76186c4 --- /dev/null +++ b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/itemlist-servlet.xml @@ -0,0 +1,58 @@ + + + + + + + + + /app/**/**=flowController + + + + + + + + + + + + + + + + + + + + + /WEB-INF/itemlist.xml + /WEB-INF/itemlist-alternate + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/itemlist.xml b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/itemlist.xml new file mode 100644 index 00000000..781cd40d --- /dev/null +++ b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/itemlist.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/jsp/includeBottom.jsp b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/jsp/includeBottom.jsp new file mode 100644 index 00000000..3b0f1dd6 --- /dev/null +++ b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/jsp/includeBottom.jsp @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/jsp/includeTop.jsp b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/jsp/includeTop.jsp new file mode 100644 index 00000000..6716fa78 --- /dev/null +++ b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/jsp/includeTop.jsp @@ -0,0 +1,21 @@ +<%@ page contentType="text/html" %> +<%@ page session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + + + +Create an item list + + + + + + + + diff --git a/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/jsp/item.jsp b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/jsp/item.jsp new file mode 100644 index 00000000..375726be --- /dev/null +++ b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/jsp/item.jsp @@ -0,0 +1,30 @@ +<%@ include file="includeTop.jsp" %> + +
+
+ Web Flow Logo +
+

Add a new item

+
+ +
+ + + + + + + + +
+ Item: + + +
+ + +
+
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/jsp/itemlist.jsp b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/jsp/itemlist.jsp new file mode 100644 index 00000000..138ba93f --- /dev/null +++ b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/jsp/itemlist.jsp @@ -0,0 +1,31 @@ +<%@ include file="includeTop.jsp" %> + +
+
+ Web Flow Logo +
+

Your item list

+
+ +
+ + + + + + + +
+ + + + +
${item}
+
+ + +
+
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/web.xml b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..aeb3e96b --- /dev/null +++ b/spring-webflow-samples/itemlist/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,22 @@ + + + + + + itemlist + org.springframework.web.servlet.DispatcherServlet + + + + itemlist + /app/* + + + + index.jsp + + + \ No newline at end of file diff --git a/spring-webflow-samples/itemlist/src/main/webapp/images/spring-logo.jpg b/spring-webflow-samples/itemlist/src/main/webapp/images/spring-logo.jpg new file mode 100644 index 00000000..62be3983 Binary files /dev/null and b/spring-webflow-samples/itemlist/src/main/webapp/images/spring-logo.jpg differ diff --git a/spring-webflow-samples/itemlist/src/main/webapp/images/webflow-logo.jpg b/spring-webflow-samples/itemlist/src/main/webapp/images/webflow-logo.jpg new file mode 100644 index 00000000..ed76bae0 Binary files /dev/null and b/spring-webflow-samples/itemlist/src/main/webapp/images/webflow-logo.jpg differ diff --git a/spring-webflow-samples/itemlist/src/main/webapp/index.jsp b/spring-webflow-samples/itemlist/src/main/webapp/index.jsp new file mode 100644 index 00000000..031fc66b --- /dev/null +++ b/spring-webflow-samples/itemlist/src/main/webapp/index.jsp @@ -0,0 +1,46 @@ +<%@ page session="true" %> <%-- make sure we have a session --%> + + + + + + + +
Item List - A Spring Web Flow Sample
+ +
+ +
+

+ Item List +

+ +

+ This Spring web flow sample application illustrates several features: +

    +
  • + Launching flow's using bookmark-friendly, REST-style URLS +
  • +
  • + Use of an inline-flow, including the ability to map subflow output attributes + directly into collection attributes in parent flow scope. +
  • +
  • + Event pattern matching, for matching eventId expressions to transitions. +
  • +
  • + "Always redirect on pause" - to achieve the POST+REDIRECT+GET pattern with no special coding. +
  • +
  • + Spring 1.2 compatible configuration, as an alternative to the Spring 2.0 support. +
  • +
+

+
+ +
+ +
+ + + diff --git a/spring-webflow-samples/itemlist/src/main/webapp/style.css b/spring-webflow-samples/itemlist/src/main/webapp/style.css new file mode 100644 index 00000000..f4b0a64e --- /dev/null +++ b/spring-webflow-samples/itemlist/src/main/webapp/style.css @@ -0,0 +1,58 @@ +body { + width: 720px; + margin: 0px; + padding: 0px; +} + +div#logo { + width: 720px; + height: 73px; + background: #86AEA5; +} + +div#navigation { + width: 720px; + height: 15px; + background: #E2F3B8; + text-align: right; +} + +div#content { + width: 720px; + padding: 5px; +} + +div#insert { + width: 120; + float: right; + text-align: right; +} + +.buttonBar { + height: 1.5em; + text-align: right; +} + +div#copyright { + width: 720px; +} + +div#copyright p { + text-align: center; + font-family: Tahoma, sans-serif; + font-size: 75%; + color: div#336633; + margin-left: 5px; + font-weight: bold; + clear: both; +} + +.readOnly { + color: rgb(192, 192, 192); +} + +.error { + color: red; + font-weight: bold; + font-family: Arial, sans-serif; +} \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/.classpath b/spring-webflow-samples/numberguess/.classpath new file mode 100644 index 00000000..1ff9d14b --- /dev/null +++ b/spring-webflow-samples/numberguess/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/spring-webflow-samples/numberguess/.cvsignore b/spring-webflow-samples/numberguess/.cvsignore new file mode 100644 index 00000000..16b87eb5 --- /dev/null +++ b/spring-webflow-samples/numberguess/.cvsignore @@ -0,0 +1,8 @@ +target +lib +bak +build.properties +*.log +*.jpx.local* +*.iws +*.tws diff --git a/spring-webflow-samples/numberguess/.project b/spring-webflow-samples/numberguess/.project new file mode 100644 index 00000000..e64046ed --- /dev/null +++ b/spring-webflow-samples/numberguess/.project @@ -0,0 +1,36 @@ + + + swf-numberguess + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + + org.springframework.ide.eclipse.core.springnature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + + diff --git a/spring-webflow-samples/numberguess/.settings/org.eclipse.jdt.core.prefs b/spring-webflow-samples/numberguess/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..52db45d8 --- /dev/null +++ b/spring-webflow-samples/numberguess/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.5 diff --git a/spring-webflow-samples/numberguess/.settings/org.eclipse.jdt.ui.prefs b/spring-webflow-samples/numberguess/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..31c851b5 --- /dev/null +++ b/spring-webflow-samples/numberguess/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/spring-webflow-samples/numberguess/.settings/org.eclipse.jst.common.project.facet.core.prefs b/spring-webflow-samples/numberguess/.settings/org.eclipse.jst.common.project.facet.core.prefs new file mode 100755 index 00000000..1973f017 --- /dev/null +++ b/spring-webflow-samples/numberguess/.settings/org.eclipse.jst.common.project.facet.core.prefs @@ -0,0 +1,4 @@ +#Mon Nov 13 17:23:47 PST 2006 +classpath.helper/org.eclipse.jdt.launching.JRE_CONTAINER/owners=jst.java\:5.0 +classpath.helper/org.eclipse.jst.server.core.container\:\:org.eclipse.jst.server.tomcat.runtimeTarget\:\:Apache\ Tomcat\ v5.5/owners=jst.web\:2.4 +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/numberguess/.settings/org.eclipse.wst.common.component b/spring-webflow-samples/numberguess/.settings/org.eclipse.wst.common.component new file mode 100755 index 00000000..8106975b --- /dev/null +++ b/spring-webflow-samples/numberguess/.settings/org.eclipse.wst.common.component @@ -0,0 +1,51 @@ + + + + + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + + + + diff --git a/spring-webflow-samples/numberguess/.settings/org.eclipse.wst.common.project.facet.core.xml b/spring-webflow-samples/numberguess/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100755 index 00000000..b7687079 --- /dev/null +++ b/spring-webflow-samples/numberguess/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spring-webflow-samples/numberguess/.settings/org.eclipse.wst.validation.prefs b/spring-webflow-samples/numberguess/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 00000000..1c7d6317 --- /dev/null +++ b/spring-webflow-samples/numberguess/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,6 @@ +#Mon Nov 13 17:29:33 PST 2006 +DELEGATES_PREFERENCE=delegateValidatorListorg.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator\=org.eclipse.wst.wsdl.validation.internal.eclipse.Validator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator\=org.eclipse.wst.xsd.core.internal.validation.eclipse.Validator; +USER_BUILD_PREFERENCE=enabledBuildValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_MANUAL_PREFERENCE=enabledManualValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_PREFERENCE=overrideGlobalPreferencesfalse +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/numberguess/.springBeans b/spring-webflow-samples/numberguess/.springBeans new file mode 100644 index 00000000..71da5896 --- /dev/null +++ b/spring-webflow-samples/numberguess/.springBeans @@ -0,0 +1,25 @@ + + + + xml + + + + + + higherlower + false + false + + + + + mastermind + false + false + + src/main/webapp/WEB-INF/dispatcher-servlet.xml + + + + diff --git a/spring-webflow-samples/numberguess/build.xml b/spring-webflow-samples/numberguess/build.xml new file mode 100644 index 00000000..2d795a4a --- /dev/null +++ b/spring-webflow-samples/numberguess/build.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/spring-webflow-samples/numberguess/ivy.xml b/spring-webflow-samples/numberguess/ivy.xml new file mode 100644 index 00000000..30e120bc --- /dev/null +++ b/spring-webflow-samples/numberguess/ivy.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/project.properties b/spring-webflow-samples/numberguess/project.properties new file mode 100644 index 00000000..f80ab3a7 --- /dev/null +++ b/spring-webflow-samples/numberguess/project.properties @@ -0,0 +1,10 @@ +# properties defined in this file are overridable by a local build.properties in this project dir + +# The location of the common build system +common.build.dir=${basedir}/../../common-build + +javac.source=1.5 +javac.target=1.5 + +# Do not publish built artifacts to the integration repository +do.publish=false \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/etc/filter.properties b/spring-webflow-samples/numberguess/src/etc/filter.properties new file mode 100644 index 00000000..c54e5880 --- /dev/null +++ b/spring-webflow-samples/numberguess/src/etc/filter.properties @@ -0,0 +1,33 @@ +# $Header$ + +# Contains filterable project settings. Setting placeholders in filterable project text +# files will be replaced with these values when the 'statics' build target is run. +# +# You may add static settings directly to this source file in the format: +# setting=value e.g MY_SETTING=myvalue +# This is appropriate usage if you know the setting value will never change. +# +# At build time this source file is copied to the ${target.dir} where additional +# dynamic settings may be appended using the task. Use this approach +# when a setting value depends on the build or the local user's environment. +# +# An example of this approach is shown below: +# +# build.xml +# +# +# +# +# +# +# +# +# This allows for dynamic replacement values that are sourced from local properties files to facilitate +# local user settings. +# +# To refer to filterable settings within project source files like config files, JSPs, or +# other text files use the standard ant placeholder format: +# @SETTING_NAME@ e.g, @MY_SETTING@ and @MY_LOCAL_SETTING@ +# +# Your settings: diff --git a/spring-webflow-samples/numberguess/src/etc/test-resources/log4j.properties b/spring-webflow-samples/numberguess/src/etc/test-resources/log4j.properties new file mode 100644 index 00000000..59974077 --- /dev/null +++ b/spring-webflow-samples/numberguess/src/etc/test-resources/log4j.properties @@ -0,0 +1,20 @@ +log4j.rootCategory=WARN, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=${@PROJECT_WEBAPP_NAME@.root}/@PROJECT_WEBAPP_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout + +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n + +#Enable webflow debug logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG diff --git a/spring-webflow-samples/numberguess/src/main/java/org/springframework/webflow/samples/numberguess/HigherLowerGame.java b/spring-webflow-samples/numberguess/src/main/java/org/springframework/webflow/samples/numberguess/HigherLowerGame.java new file mode 100644 index 00000000..cbe7b72e --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/java/org/springframework/webflow/samples/numberguess/HigherLowerGame.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.numberguess; + +import java.io.Serializable; +import java.util.Calendar; +import java.util.Random; + +/** + * Action that encapsulates logic for the number guess sample flow. Note that + * this is a stateful action: it holds modifiable state in instance members! + * + * @author Erwin Vervaet + * @author Keith Donald + */ +public class HigherLowerGame implements Serializable { + + private static final Random random = new Random(); + + private Calendar start = Calendar.getInstance(); + + private int answer = random.nextInt(101); + + private int guesses = 0; + + private GuessResult result; + + public int getAnswer() { + return answer; + } + + public int getGuesses() { + return guesses; + } + + public GuessResult getResult() { + return result; + } + + public void setResult(GuessResult result) { + this.result = result; + } + + public long getDuration() { + Calendar now = Calendar.getInstance(); + long durationMilliseconds = now.getTime().getTime() - start.getTime().getTime(); + return durationMilliseconds / 1000; + } + + public GuessResult makeGuess(int guess) { + if (guess < 0 || guess > 100) { + setResult(GuessResult.INVALID); + } + else { + guesses++; + if (answer < guess) { + setResult(GuessResult.TOO_HIGH); + } + else if (answer > guess) { + setResult(GuessResult.TOO_LOW); + } + else { + setResult(GuessResult.CORRECT); + } + } + return getResult(); + } + + enum GuessResult { + TOO_HIGH, TOO_LOW, CORRECT, INVALID + } +} \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/main/java/org/springframework/webflow/samples/numberguess/MastermindGame.java b/spring-webflow-samples/numberguess/src/main/java/org/springframework/webflow/samples/numberguess/MastermindGame.java new file mode 100644 index 00000000..6efc12f7 --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/java/org/springframework/webflow/samples/numberguess/MastermindGame.java @@ -0,0 +1,209 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.numberguess; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Collection; +import java.util.List; +import java.util.Random; + +/** + * Action that encapsulates logic for the number guess sample flow. + * + * @author Keri Donald + * @author Keith Donald + */ +public class MastermindGame implements Serializable { + + private GameData data = new GameData(); + + public GameData getData() { + return data; + } + + public Collection getGuessHistory() { + return data.getGuessHistory(); + } + + public GuessResult getResult() { + return data.getLastGuessResult(); + } + + public void setResult(GuessResult result) { + data.setLastGuessResult(result); + } + + public GuessResult makeGuess(String guess) { + if (isGuessValid(guess)) { + setResult(calculateResult(guess)); + } + else { + setResult(GuessResult.INVALID); + } + return getResult(); + } + + private boolean isGuessValid(String guess) { + if (guess == null || guess.length() != 4) { + return false; + } + for (int i = 0; i < 4; i++) { + if (!Character.isDigit(guess.charAt(i))) { + return false; + } + int digit = Character.getNumericValue(guess.charAt(i)); + for (int j = 0; j < i; j++) { + if (digit == Character.getNumericValue(guess.charAt(j))) { + return false; + } + } + } + return true; + } + + private GuessResult calculateResult(String guess) { + int rightPosition = 0; + int correctButWrongPosition = 0; + for (int i = 0; i < guess.length(); i++) { + char digit = guess.charAt(i); + for (int j = 0; j < data.answer.length(); j++) { + char answerDigit = data.answer.charAt(j); + if (digit == answerDigit) { + if (i == j) { + rightPosition++; + } + else { + correctButWrongPosition++; + } + break; + } + } + } + data.recordGuessData(guess, rightPosition, correctButWrongPosition); + if (rightPosition == 4) { + return GuessResult.CORRECT; + } + else { + return GuessResult.WRONG; + } + } + + /** + * Simple data holder for number guess info. + */ + public static class GameData implements Serializable { + + private static Random random = new Random(); + + private Calendar start = Calendar.getInstance(); + + private String answer; + + private List guessHistory = new ArrayList(); + + private GuessResult lastGuessResult; + + // property accessors for JSTL EL + + public GameData() { + this.answer = createAnswer(); + } + + public int getGuesses() { + return guessHistory.size(); + } + + public GuessResult getLastGuessResult() { + return lastGuessResult; + } + + public void setLastGuessResult(GuessResult lastGuessResult) { + this.lastGuessResult = lastGuessResult; + } + + public String getAnswer() { + return answer; + } + + public long getDuration() { + Calendar now = Calendar.getInstance(); + long durationMilliseconds = now.getTime().getTime() - start.getTime().getTime(); + return durationMilliseconds / 1000; + } + + public Collection getGuessHistory() { + return guessHistory; + } + + public GuessData getLastGuessData() { + if (guessHistory.isEmpty()) { + return null; + } + return guessHistory.get(guessHistory.size() - 1); + } + + public void recordGuessData(String guess, int rightPosition, int correctButWrongPosition) { + guessHistory.add(new GuessData(guess, rightPosition, correctButWrongPosition)); + } + + public String createAnswer() { + StringBuffer buffer = new StringBuffer(4); + for (int i = 0; i < 4; i++) { + int digit = random.nextInt(10); + for (int j = 0; j < i; j++) { + if (digit == Character.getNumericValue(buffer.charAt(j))) { + j = -1; + digit = random.nextInt(10); + } + } + buffer.append(digit); + } + return buffer.toString(); + } + + public class GuessData implements Serializable { + private String guess; + + private int rightPosition; + + private int correctButWrongPosition; + + public GuessData(String guess, int rightPosition, int correctButWrongPosition) { + this.guess = guess; + this.rightPosition = rightPosition; + this.correctButWrongPosition = correctButWrongPosition; + } + + public int getCorrectButWrongPosition() { + return correctButWrongPosition; + } + + public String getGuess() { + return guess; + } + + public int getRightPosition() { + return rightPosition; + } + } + } + + enum GuessResult { + WRONG, CORRECT, INVALID + } +} \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/classes/log4j.properties b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/classes/log4j.properties new file mode 100644 index 00000000..5ba9222c --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/classes/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable web flow logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/dispatcher-servlet.xml b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/dispatcher-servlet.xml new file mode 100644 index 00000000..4a4bddd5 --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/dispatcher-servlet.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/higherlower.xml b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/higherlower.xml new file mode 100644 index 00000000..56dee132 --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/higherlower.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/higherlower.enterGuess.jsp b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/higherlower.enterGuess.jsp new file mode 100644 index 00000000..13358d58 --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/higherlower.enterGuess.jsp @@ -0,0 +1,34 @@ +<%@ include file="includeTop.jsp" %> + +
+
+

The Number Guess Game

+

Guess a number between 0 and 100!

+
+
+ + + + + + + + + + + + + + + + +
Number of guesses:${game.guesses}
Your last guess was:${game.result}
Guess: + +
+ + +
+
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/higherlower.showAnswer.jsp b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/higherlower.showAnswer.jsp new file mode 100644 index 00000000..28c9b462 --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/higherlower.showAnswer.jsp @@ -0,0 +1,29 @@ +<%@ include file="includeTop.jsp" %> + +
+
+

You guessed it!

+ + + + + + + + + + + + + + + + +
Answer:${game.answer}
Total number of guesses:${game.guesses}
Elapsed time in seconds:${game.duration}
+
+ + +
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/includeBottom.jsp b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/includeBottom.jsp new file mode 100644 index 00000000..3b0f1dd6 --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/includeBottom.jsp @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/includeTop.jsp b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/includeTop.jsp new file mode 100644 index 00000000..c2a66b9c --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/includeTop.jsp @@ -0,0 +1,21 @@ +<%@ page contentType="text/html" %> +<%@ page session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + + + +Play a game + + + + + + + + diff --git a/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/mastermind.enterGuess.jsp b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/mastermind.enterGuess.jsp new file mode 100644 index 00000000..0cbca015 --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/mastermind.enterGuess.jsp @@ -0,0 +1,34 @@ +<%@ include file="includeTop.jsp" %> + +
+
+

Mastermind

+

Guess a four digit number!

+
+

Note: each guess must be 4 unique digits!

+

Number of guesses so far: ${game.data.guesses}

+ + <%@include file="mastermind.guessHistoryTable.jsp" %> + +
+ +
Your guess was invalid: it must be a 4 digit number (e.g 1234), and each digit must be unique.
+
+ + + + + + + + +
Guess: + +
+ + +
+
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/mastermind.guessHistoryTable.jsp b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/mastermind.guessHistoryTable.jsp new file mode 100644 index 00000000..e4afe694 --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/mastermind.guessHistoryTable.jsp @@ -0,0 +1,15 @@ + +

Guess history:

+ + + + + + + + + + + +
GuessRight PositionPresent But Wrong Position
${guessData.guess}${guessData.rightPosition}${guessData.correctButWrongPosition}
+
\ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/mastermind.showAnswer.jsp b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/mastermind.showAnswer.jsp new file mode 100644 index 00000000..adbc2086 --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/jsp/mastermind.showAnswer.jsp @@ -0,0 +1,28 @@ +<%@ include file="includeTop.jsp" %> + +
+
+

Show answer

+ + + + + + + + + + + + + + + + +
Total number of guesses:${game.data.guesses}
Elapsed time in seconds:${game.data.duration}
Answer:${game.data.answer}
+
+ + +
+
+<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/mastermind.xml b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/mastermind.xml new file mode 100644 index 00000000..dc044d5a --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/mastermind.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/web.xml b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..ec4cfd7f --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,22 @@ + + + + + + numberguess + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + /WEB-INF/dispatcher-servlet.xml + + + + + numberguess + *.htm + + + \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/main/webapp/images/spring-logo.jpg b/spring-webflow-samples/numberguess/src/main/webapp/images/spring-logo.jpg new file mode 100644 index 00000000..62be3983 Binary files /dev/null and b/spring-webflow-samples/numberguess/src/main/webapp/images/spring-logo.jpg differ diff --git a/spring-webflow-samples/numberguess/src/main/webapp/images/webflow-logo.jpg b/spring-webflow-samples/numberguess/src/main/webapp/images/webflow-logo.jpg new file mode 100644 index 00000000..ed76bae0 Binary files /dev/null and b/spring-webflow-samples/numberguess/src/main/webapp/images/webflow-logo.jpg differ diff --git a/spring-webflow-samples/numberguess/src/main/webapp/index.html b/spring-webflow-samples/numberguess/src/main/webapp/index.html new file mode 100644 index 00000000..33f602b0 --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/webapp/index.html @@ -0,0 +1,40 @@ + + + + + +
Number Guess - A Spring Web Flow Sample
+
+ This Spring Web Flow (SWF) sample application illustrates: +
    +
  • + Use of stateful middle-tier components to carry out the execution of game business logic. It shows how to use SWF to + develop such components without coupling your application code to SWF APIs. +
  • +
  • + Use of custom state exception handlers to handle exceptions. +
  • +
  • + Use of the evaluate-action to evaluate expressions against the flow request context; + in this example to call the "makeGuess" method on a flow-scoped "game" bean. +
  • +
+
+

+ Play Higher or Lower +

+

+ The well known higher-or-lower number guess sample app. +

+
+
+

+ Play Mastermind +

+

+ Figure out the 4 digit number - a fun mindteaser sample app. +

+
+
+ + diff --git a/spring-webflow-samples/numberguess/src/main/webapp/style.css b/spring-webflow-samples/numberguess/src/main/webapp/style.css new file mode 100644 index 00000000..f4b0a64e --- /dev/null +++ b/spring-webflow-samples/numberguess/src/main/webapp/style.css @@ -0,0 +1,58 @@ +body { + width: 720px; + margin: 0px; + padding: 0px; +} + +div#logo { + width: 720px; + height: 73px; + background: #86AEA5; +} + +div#navigation { + width: 720px; + height: 15px; + background: #E2F3B8; + text-align: right; +} + +div#content { + width: 720px; + padding: 5px; +} + +div#insert { + width: 120; + float: right; + text-align: right; +} + +.buttonBar { + height: 1.5em; + text-align: right; +} + +div#copyright { + width: 720px; +} + +div#copyright p { + text-align: center; + font-family: Tahoma, sans-serif; + font-size: 75%; + color: div#336633; + margin-left: 5px; + font-weight: bold; + clear: both; +} + +.readOnly { + color: rgb(192, 192, 192); +} + +.error { + color: red; + font-weight: bold; + font-family: Arial, sans-serif; +} \ No newline at end of file diff --git a/spring-webflow-samples/numberguess/src/test/java/org/springframework/webflow/samples/numberguess/MastermindGameTests.java b/spring-webflow-samples/numberguess/src/test/java/org/springframework/webflow/samples/numberguess/MastermindGameTests.java new file mode 100644 index 00000000..5fb40bc0 --- /dev/null +++ b/spring-webflow-samples/numberguess/src/test/java/org/springframework/webflow/samples/numberguess/MastermindGameTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.numberguess; + +import junit.framework.TestCase; + +import org.springframework.webflow.samples.numberguess.MastermindGame.GameData; +import org.springframework.webflow.samples.numberguess.MastermindGame.GuessResult; + +public class MastermindGameTests extends TestCase { + + private MastermindGame action = new MastermindGame(); + + protected void setUp() { + action = new MastermindGame(); + } + + public void testGuessNoInputProvided() throws Exception { + GuessResult result = action.makeGuess(null); + assertEquals(GuessResult.INVALID, result); + } + + public void testGuessInputInvalidLength() throws Exception { + GuessResult result = action.makeGuess("123"); + assertEquals(GuessResult.INVALID, result); + } + + public void testGuessInputNotAllDigits() throws Exception { + GuessResult result = action.makeGuess("12AB"); + assertEquals(GuessResult.INVALID, result); + } + + public void testGuessInputNotUniqueDigits() throws Exception { + GuessResult result = action.makeGuess("1111"); + assertEquals(GuessResult.INVALID, result); + } + + public void testGuessRetry() throws Exception { + GuessResult result = action.makeGuess("1234"); + assertEquals(GuessResult.WRONG, result); + } + + public void testGuessCorrect() throws Exception { + GuessResult result = action.makeGuess(null); + assertEquals(GuessResult.INVALID, result); + GameData data = action.getData(); + result = action.makeGuess(data.getAnswer()); + assertEquals(GuessResult.CORRECT, result); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/.classpath b/spring-webflow-samples/phonebook-portlet/.classpath new file mode 100644 index 00000000..7962aad1 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/.classpath @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-webflow-samples/phonebook-portlet/.cvsignore b/spring-webflow-samples/phonebook-portlet/.cvsignore new file mode 100644 index 00000000..16b87eb5 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/.cvsignore @@ -0,0 +1,8 @@ +target +lib +bak +build.properties +*.log +*.jpx.local* +*.iws +*.tws diff --git a/spring-webflow-samples/phonebook-portlet/.project b/spring-webflow-samples/phonebook-portlet/.project new file mode 100644 index 00000000..3b9ef6dc --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/.project @@ -0,0 +1,24 @@ + + + swf-phonebook-portlet + + + spring-webflow + + + + org.eclipse.jdt.core.javabuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + + org.springframework.ide.eclipse.core.springnature + org.eclipse.jdt.core.javanature + + diff --git a/spring-webflow-samples/phonebook-portlet/.settings/org.eclipse.jdt.core.prefs b/spring-webflow-samples/phonebook-portlet/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..dcae5b26 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,12 @@ +#Sun Apr 16 11:53:00 EDT 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.5 diff --git a/spring-webflow-samples/phonebook-portlet/.settings/org.eclipse.jdt.ui.prefs b/spring-webflow-samples/phonebook-portlet/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..12c73352 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Tue Feb 07 10:23:17 EST 2006 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/spring-webflow-samples/phonebook-portlet/.settings/org.eclipse.wst.validation.prefs b/spring-webflow-samples/phonebook-portlet/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 00000000..68bb37d5 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,6 @@ +#Fri May 05 18:13:37 EDT 2006 +DELEGATES_PREFERENCE=delegateValidatorListorg.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator\=org.eclipse.wst.wsdl.validation.internal.eclipse.Validator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator\=org.eclipse.wst.xsd.core.internal.validation.eclipse.Validator; +USER_BUILD_PREFERENCE=enabledBuildValidatorListorg.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator; +USER_MANUAL_PREFERENCE=enabledManualValidatorListorg.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator; +USER_PREFERENCE=overrideGlobalPreferencesfalse +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/phonebook-portlet/.springBeans b/spring-webflow-samples/phonebook-portlet/.springBeans new file mode 100644 index 00000000..427d0a04 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/.springBeans @@ -0,0 +1,36 @@ + + + + xml + + + src/main/webapp/WEB-INF/flows/search-flow-beans.xml + + + + search-flow + false + false + + src/main/webapp/WEB-INF/flows/search-flow-beans.xml + + + + service-layer + false + false + + src/main/java/org/springframework/webflow/samples/phonebook/domain/services.xml + + + + webapp + false + false + + src/main/webapp/WEB-INF/dispatcher-portlet.xml + src/main/webapp/WEB-INF/webflow.xml + + + + diff --git a/spring-webflow-samples/phonebook-portlet/build.xml b/spring-webflow-samples/phonebook-portlet/build.xml new file mode 100644 index 00000000..8bee2e4a --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/build.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-webflow-samples/phonebook-portlet/ivy.xml b/spring-webflow-samples/phonebook-portlet/ivy.xml new file mode 100644 index 00000000..3ed99317 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/ivy.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/project.properties b/spring-webflow-samples/phonebook-portlet/project.properties new file mode 100644 index 00000000..f80ab3a7 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/project.properties @@ -0,0 +1,10 @@ +# properties defined in this file are overridable by a local build.properties in this project dir + +# The location of the common build system +common.build.dir=${basedir}/../../common-build + +javac.source=1.5 +javac.target=1.5 + +# Do not publish built artifacts to the integration repository +do.publish=false \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/etc/filter.properties b/spring-webflow-samples/phonebook-portlet/src/etc/filter.properties new file mode 100644 index 00000000..c54e5880 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/etc/filter.properties @@ -0,0 +1,33 @@ +# $Header$ + +# Contains filterable project settings. Setting placeholders in filterable project text +# files will be replaced with these values when the 'statics' build target is run. +# +# You may add static settings directly to this source file in the format: +# setting=value e.g MY_SETTING=myvalue +# This is appropriate usage if you know the setting value will never change. +# +# At build time this source file is copied to the ${target.dir} where additional +# dynamic settings may be appended using the task. Use this approach +# when a setting value depends on the build or the local user's environment. +# +# An example of this approach is shown below: +# +# build.xml +# +# +# +# +# +# +# +# +# This allows for dynamic replacement values that are sourced from local properties files to facilitate +# local user settings. +# +# To refer to filterable settings within project source files like config files, JSPs, or +# other text files use the standard ant placeholder format: +# @SETTING_NAME@ e.g, @MY_SETTING@ and @MY_LOCAL_SETTING@ +# +# Your settings: diff --git a/spring-webflow-samples/phonebook-portlet/src/etc/test-resources/log4j.properties b/spring-webflow-samples/phonebook-portlet/src/etc/test-resources/log4j.properties new file mode 100644 index 00000000..3e030448 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/etc/test-resources/log4j.properties @@ -0,0 +1,15 @@ +log4j.rootCategory=WARN, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=@TEST_RESULTS_DIR@/@PROJECT_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n diff --git a/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/Person.java b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/Person.java new file mode 100644 index 00000000..33dd08c5 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/Person.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class Person implements Serializable { + + private Long id; + + private String firstName; + + private String lastName; + + private String userId; + + private String phone; + + private List colleagues = new ArrayList(); + + public Person() { + this(-1, "", "", "", ""); + } + + public Person(long id, String firstName, String lastName, String userId, String phone) { + this.id = new Long(id); + this.firstName = firstName; + this.lastName = lastName; + this.userId = userId; + this.phone = phone; + } + + public Long getId() { + return id; + } + + public String getFirstName() { + return this.firstName; + } + + public String getLastName() { + return this.lastName; + } + + public String getUserId() { + return userId; + } + + public String getPhone() { + return this.phone; + } + + public List getColleagues() { + return this.colleagues; + } + + public int getColleagueCount() { + return this.colleagues.size(); + } + + public Person getColleague(int i) { + return this.colleagues.get(i); + } + + public void addColleague(Person colleague) { + this.colleagues.add(colleague); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/Phonebook.java b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/Phonebook.java new file mode 100644 index 00000000..bdb53330 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/Phonebook.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook; + +import java.util.List; + +public interface Phonebook { + + public List search(SearchCriteria criteria); + + public Person getPerson(Long id); + + public Person getPerson(String userId); +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/SearchCriteria.java b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/SearchCriteria.java new file mode 100644 index 00000000..283e131c --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/SearchCriteria.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook; + +import java.io.Serializable; + +public class SearchCriteria implements Serializable { + + private String firstName = ""; + + private String lastName = ""; + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/SearchCriteriaValidator.java b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/SearchCriteriaValidator.java new file mode 100644 index 00000000..d095c5f3 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/SearchCriteriaValidator.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook; + +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +public class SearchCriteriaValidator implements Validator { + + public boolean supports(Class clazz) { + return clazz.equals(SearchCriteria.class); + } + + public void validate(Object obj, Errors errors) { + SearchCriteria query = (SearchCriteria)obj; + if (!StringUtils.hasText(query.getFirstName()) && !StringUtils.hasText(query.getLastName())) { + errors.reject("noCriteria", "Please provide some query criteria!"); + } + } +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/stub/StubPhonebook.java b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/stub/StubPhonebook.java new file mode 100644 index 00000000..dc20c481 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/stub/StubPhonebook.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook.stub; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.webflow.samples.phonebook.Person; +import org.springframework.webflow.samples.phonebook.Phonebook; +import org.springframework.webflow.samples.phonebook.SearchCriteria; + +public class StubPhonebook implements Phonebook { + + private List persons = new ArrayList(); + + public StubPhonebook() { + // setup some dummy test data + Person kd = new Person(1, "Keith", "Donald", "kdonald", "11111"); + Person ev = new Person(2, "Erwin", "Vervaet", "klr8", "22222"); + Person cs = new Person(3, "Colin", "Sampaleanu", "sampa", "33333"); + Person jh = new Person(4, "Juergen", "Hoeller", "jhoeller", "44444"); + Person rj = new Person(5, "Rod", "Johnson", "rod", "55555"); + Person tr = new Person(6, "Thomas", "Risberg", "trisberg", "66666"); + Person aa = new Person(7, "Alef", "Arendsen", "alef", "77777"); + Person mp = new Person(8, "Mark", "Pollack", "mark", "88888"); + + kd.addColleague(ev); + kd.addColleague(cs); + kd.addColleague(jh); + kd.addColleague(rj); + kd.addColleague(tr); + kd.addColleague(aa); + kd.addColleague(mp); + + ev.addColleague(kd); + ev.addColleague(cs); + ev.addColleague(jh); + ev.addColleague(rj); + + cs.addColleague(kd); + cs.addColleague(ev); + cs.addColleague(jh); + cs.addColleague(rj); + cs.addColleague(aa); + cs.addColleague(mp); + + rj.addColleague(cs); + rj.addColleague(kd); + rj.addColleague(ev); + rj.addColleague(jh); + rj.addColleague(tr); + rj.addColleague(aa); + rj.addColleague(mp); + + jh.addColleague(cs); + jh.addColleague(kd); + jh.addColleague(ev); + jh.addColleague(jh); + jh.addColleague(tr); + jh.addColleague(aa); + + Person sa = new Person(9, "Shaun", "Alexander", "rolltide", "44444"); + Person dj = new Person(10, "Darell", "Jackson", "gatorcountry", "55555"); + sa.addColleague(dj); + dj.addColleague(sa); + + persons.add(kd); + persons.add(ev); + persons.add(cs); + persons.add(jh); + persons.add(rj); + persons.add(tr); + persons.add(aa); + persons.add(mp); + + persons.add(sa); + persons.add(dj); + } + + public List search(SearchCriteria query) { + List res = new ArrayList(); + for (Person person : persons) { + if ((person.getFirstName().indexOf(query.getFirstName()) != -1) + && (person.getLastName().indexOf(query.getLastName()) != -1)) { + res.add(person); + } + } + return res; + } + + public Person getPerson(Long id) { + for (Person person : persons) { + if (person.getId().equals(id)) { + return person; + } + } + return null; + } + + public Person getPerson(String userId) { + for (Person person : persons) { + if (person.getUserId().equals(userId)) { + return person; + } + } + return null; + } + +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/stub/services-config.xml b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/stub/services-config.xml new file mode 100644 index 00000000..b47c2188 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/stub/services-config.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PersonDetailFlowBuilder.java b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PersonDetailFlowBuilder.java new file mode 100644 index 00000000..9458deaf --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PersonDetailFlowBuilder.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook.webflow; + +import org.springframework.binding.mapping.DefaultAttributeMapper; +import org.springframework.binding.mapping.Mapping; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.engine.builder.AbstractFlowBuilder; +import org.springframework.webflow.engine.builder.FlowBuilderException; +import org.springframework.webflow.engine.builder.FlowServiceLocator; +import org.springframework.webflow.engine.support.ConfigurableFlowAttributeMapper; + +/** + * Java-based flow builder that builds the person details flow, exactly like it + * is defined in the detail-flow.xml XML flow definition. + *

+ * This encapsulates the page flow of viewing a person's details and their + * collegues in a reusable, self-contained module. + * + * @author Keith Donald + */ +class PersonDetailFlowBuilder extends AbstractFlowBuilder { + + public PersonDetailFlowBuilder(FlowServiceLocator flowServiceLocator) { + super(flowServiceLocator); + } + + public void buildInputMapper() throws FlowBuilderException { + Mapping idMapping = mapping().source("id").target("flowScope.id").value(); + getFlow().setInputMapper(new DefaultAttributeMapper().addMapping(idMapping)); + } + + public void buildStates() throws FlowBuilderException { + // get the person given a userid as input + addActionState("getDetails", action("phonebook", method("getPerson(${flowScope.id})"), result("person")), + transition(on(success()), to("displayDetails"))); + + // view the person details + addViewState("displayDetails", "details", new Transition[] { transition(on(back()), to("finish")), + transition(on(select()), to("browseColleagueDetails")) }); + + // view details for selected collegue + ConfigurableFlowAttributeMapper idMapper = new ConfigurableFlowAttributeMapper(); + idMapper.addInputMapping(mapping().source("requestParameters.id").target("id").from(String.class) + .to(Long.class).value()); + addSubflowState("browseColleagueDetails", getFlow(), idMapper, transition(on(finish()), to("getDetails"))); + + // end + addEndState("finish"); + + // end error + addEndState("error"); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PhonebookFlowRegistrar.java b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PhonebookFlowRegistrar.java new file mode 100644 index 00000000..9eb78773 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PhonebookFlowRegistrar.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook.webflow; + +import org.springframework.webflow.definition.registry.FlowDefinitionHolder; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistrar; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.definition.registry.StaticFlowDefinitionHolder; +import org.springframework.webflow.engine.builder.FlowAssembler; +import org.springframework.webflow.engine.builder.FlowBuilder; +import org.springframework.webflow.engine.builder.FlowServiceLocator; + +/** + * Demonstrates how to register flows programatically. + * + * @author Keith Donald + */ +class PhonebookFlowRegistrar implements FlowDefinitionRegistrar { + + private FlowServiceLocator serviceLocator; + + public PhonebookFlowRegistrar(FlowServiceLocator serviceLocator) { + this.serviceLocator = serviceLocator; + } + + public void registerFlowDefinitions(FlowDefinitionRegistry registry) { + registry.registerFlowDefinition(assemble("detail-flow", new PersonDetailFlowBuilder(serviceLocator))); + registry.registerFlowDefinition(assemble("search-flow", new SearchPersonFlowBuilder(serviceLocator))); + } + + private FlowDefinitionHolder assemble(String flowId, FlowBuilder flowBuilder) { + return new StaticFlowDefinitionHolder(new FlowAssembler(flowId, flowBuilder).assembleFlow()); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PhonebookFlowRegistryFactoryBean.java b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PhonebookFlowRegistryFactoryBean.java new file mode 100644 index 00000000..1ebace80 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PhonebookFlowRegistryFactoryBean.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook.webflow; + +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.engine.builder.AbstractFlowBuildingFlowRegistryFactoryBean; + +/** + * Demonstrates how to populate a flow registry programatically. + * + * @author Keith Donald + */ +public class PhonebookFlowRegistryFactoryBean extends AbstractFlowBuildingFlowRegistryFactoryBean { + + protected void doPopulate(FlowDefinitionRegistry registry) { + new PhonebookFlowRegistrar(getFlowServiceLocator()).registerFlowDefinitions(registry); + } +} diff --git a/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/SearchPersonFlowBuilder.java b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/SearchPersonFlowBuilder.java new file mode 100644 index 00000000..f8edc999 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/SearchPersonFlowBuilder.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook.webflow; + +import org.springframework.webflow.action.FormAction; +import org.springframework.webflow.action.MultiAction; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.engine.builder.AbstractFlowBuilder; +import org.springframework.webflow.engine.builder.FlowBuilderException; +import org.springframework.webflow.engine.builder.FlowServiceLocator; +import org.springframework.webflow.engine.support.ConfigurableFlowAttributeMapper; +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.samples.phonebook.SearchCriteria; +import org.springframework.webflow.samples.phonebook.SearchCriteriaValidator; + +/** + * Java-based flow builder that searches for people in the phonebook. The flow + * defined by this class is exactly the same as that defined in the + * search-flow.xml XML flow definition. + *

+ * This encapsulates the page flow of searching for some people, selecting a + * person you care about, and viewing their person's details and those of their + * collegues in a reusable, self-contained module. + * + * @author Keith Donald + */ +class SearchPersonFlowBuilder extends AbstractFlowBuilder { + + public SearchPersonFlowBuilder(FlowServiceLocator flowServiceLocator) { + super(flowServiceLocator); + } + + public void buildStates() throws FlowBuilderException { + // view search criteria + MultiAction searchFormAction = createSearchFormAction(); + addViewState("enterCriteria", "searchCriteria", invoke("setupForm", searchFormAction), + new Transition[] { transition(on("search"), to("executeSearch"), ifReturnedSuccess(invoke( + "bindAndValidate", searchFormAction))) }); + + // execute query + addActionState("executeSearch", action("phonebook", method("search(${flowScope.searchCriteria})"), + result("results")), transition(on(success()), to("displayResults"))); + + // view results + addViewState("displayResults", "searchResults", new Transition[] { + transition(on("newSearch"), to("enterCriteria")), transition(on(select()), to("browseDetails")) }); + + // view details for selected user id + ConfigurableFlowAttributeMapper idMapper = new ConfigurableFlowAttributeMapper(); + idMapper.addInputMapping(mapping().source("requestParameters.id").target("id").from(String.class) + .to(Long.class).value()); + addSubflowState("browseDetails", flow("detail-flow"), idMapper, transition(on(finish()), to("executeSearch"))); + + // end - an error occured + addEndState(error(), "error"); + } + + protected FormAction createSearchFormAction() { + FormAction action = new FormAction(SearchCriteria.class); + action.setFormObjectScope(ScopeType.FLOW); + action.setValidator(new SearchCriteriaValidator()); + return action; + } +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/phonebook-webflow-config.xml b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/phonebook-webflow-config.xml new file mode 100644 index 00000000..25342399 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/java/org/springframework/webflow/samples/phonebook/webflow/phonebook-webflow-config.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/classes/log4j.properties b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/classes/log4j.properties new file mode 100644 index 00000000..59974077 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/classes/log4j.properties @@ -0,0 +1,20 @@ +log4j.rootCategory=WARN, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=${@PROJECT_WEBAPP_NAME@.root}/@PROJECT_WEBAPP_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout + +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n + +#Enable webflow debug logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/flows/detail-flow.xml b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/flows/detail-flow.xml new file mode 100644 index 00000000..0bba64c5 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/flows/detail-flow.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/flows/search-flow-beans.xml b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/flows/search-flow-beans.xml new file mode 100644 index 00000000..711c661f --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/flows/search-flow-beans.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/flows/search-flow.xml b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/flows/search-flow.xml new file mode 100644 index 00000000..8b38a423 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/flows/search-flow.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/details.jsp b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/details.jsp new file mode 100644 index 00000000..e62b8172 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/details.jsp @@ -0,0 +1,58 @@ +<%@ include file="includeTop.jsp" %> + +<%@ page import="org.springframework.webflow.samples.phonebook.Person" %> + +

+
+ "/> +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Person Details

First Name
Last Name
User Id
Phone
+
+ Colleagues: +
+ + " /> + + "/> + "> +
+
+
+
+ "> + +
+
+
\ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/error.jsp b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/error.jsp new file mode 100644 index 00000000..87754023 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/error.jsp @@ -0,0 +1,18 @@ +<%@ page session="false" %> + + + + + + +
Error
+
+
+

+ An error has occured! +

+
+
+
+ + diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/includeBottom.jsp b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/includeBottom.jsp new file mode 100644 index 00000000..3b0f1dd6 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/includeBottom.jsp @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/includeTop.jsp b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/includeTop.jsp new file mode 100644 index 00000000..91100ef3 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/includeTop.jsp @@ -0,0 +1,23 @@ +<%@ page contentType="text/html" %> +<%@ page session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jstl/core" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="portlet" uri="http://java.sun.com/portlet" %> + + + + + +Search the Phonebook + +" type="text/css"> + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/searchCriteria.jsp b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/searchCriteria.jsp new file mode 100644 index 00000000..d7de8568 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/searchCriteria.jsp @@ -0,0 +1,58 @@ +<%@ include file="includeTop.jsp" %> + +
+ +
+ "/> +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
Search Criteria
+
+
+
+
Please provide valid search criteria!
+
First Name + " value=""> +
Last Name + " value=""> +
+
+
+ "> + +
+
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/searchResults.jsp b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/searchResults.jsp new file mode 100644 index 00000000..282a0a1f --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/jsp/searchResults.jsp @@ -0,0 +1,60 @@ +<%@ include file="includeTop.jsp" %> + +<%@ page import="org.springframework.webflow.samples.phonebook.Person" %> + +
+
+ "/> +
+
+ + + + + + + + + + + + + +
+
Search Results
+
+
+
+ + + + + + + + + + + + + + + +
First NameLast NameUser IdPhone
+ " /> + + "/> + "> + + +
+
+ "> + +
+
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/phonebook-portlet-config.xml b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/phonebook-portlet-config.xml new file mode 100644 index 00000000..74db9e7d --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/phonebook-portlet-config.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/phonebook-webflow-config.xml b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/phonebook-webflow-config.xml new file mode 100644 index 00000000..0c77f6cc --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/phonebook-webflow-config.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/portlet.xml b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/portlet.xml new file mode 100644 index 00000000..c4c60267 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/portlet.xml @@ -0,0 +1,47 @@ + + + + + phonebook + Phone Book + + + org.springframework.web.portlet.DispatcherPortlet + + + + contextConfigLocation + + /WEB-INF/phonebook-portlet-config.xml /WEB-INF/phonebook-webflow-config.xml + + + + + viewRendererUrl + /WEB-INF/servlet/view + + + 0 + + + text/html + view + + + + Phone Book + + + + + page.size + 2 + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/web.xml b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..43809551 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,74 @@ + + + + + + webAppRootKey + swf-phonebook-portlet.root + + + + log4jConfigLocation + /WEB-INF/classes/log4j.properties + + + + contextConfigLocation + + classpath:org/springframework/webflow/samples/phonebook/stub/services-config.xml + + + + + + org.springframework.web.util.Log4jConfigListener + + + + + + org.springframework.web.context.ContextLoaderListener + + + + + + + phonebook + phonebook Wrapper + Automated generated Portlet Wrapper + + org.apache.pluto.core.PortletServlet + + + portlet-guid + swf-phonebook-portlet.phonebook + + + portlet-class + + org.springframework.web.portlet.DispatcherPortlet + + + + + + + + viewRendererServlet + + org.springframework.web.servlet.ViewRendererServlet + + + + + phonebook + /phonebook/* + + + + viewRendererServlet + /WEB-INF/servlet/view + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/images/spring-logo.jpg b/spring-webflow-samples/phonebook-portlet/src/main/webapp/images/spring-logo.jpg new file mode 100644 index 00000000..62be3983 Binary files /dev/null and b/spring-webflow-samples/phonebook-portlet/src/main/webapp/images/spring-logo.jpg differ diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/images/webflow-logo.jpg b/spring-webflow-samples/phonebook-portlet/src/main/webapp/images/webflow-logo.jpg new file mode 100644 index 00000000..ed76bae0 Binary files /dev/null and b/spring-webflow-samples/phonebook-portlet/src/main/webapp/images/webflow-logo.jpg differ diff --git a/spring-webflow-samples/phonebook-portlet/src/main/webapp/style.css b/spring-webflow-samples/phonebook-portlet/src/main/webapp/style.css new file mode 100644 index 00000000..f4b0a64e --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/main/webapp/style.css @@ -0,0 +1,58 @@ +body { + width: 720px; + margin: 0px; + padding: 0px; +} + +div#logo { + width: 720px; + height: 73px; + background: #86AEA5; +} + +div#navigation { + width: 720px; + height: 15px; + background: #E2F3B8; + text-align: right; +} + +div#content { + width: 720px; + padding: 5px; +} + +div#insert { + width: 120; + float: right; + text-align: right; +} + +.buttonBar { + height: 1.5em; + text-align: right; +} + +div#copyright { + width: 720px; +} + +div#copyright p { + text-align: center; + font-family: Tahoma, sans-serif; + font-size: 75%; + color: div#336633; + margin-left: 5px; + font-weight: bold; + clear: both; +} + +.readOnly { + color: rgb(192, 192, 192); +} + +.error { + color: red; + font-weight: bold; + font-family: Arial, sans-serif; +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook-portlet/src/test/java/org/springframework/webflow/samples/phonebook/webflow/SearchFlowExecutionTests.java b/spring-webflow-samples/phonebook-portlet/src/test/java/org/springframework/webflow/samples/phonebook/webflow/SearchFlowExecutionTests.java new file mode 100644 index 00000000..ed4d6f19 --- /dev/null +++ b/spring-webflow-samples/phonebook-portlet/src/test/java/org/springframework/webflow/samples/phonebook/webflow/SearchFlowExecutionTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook.webflow; + +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.binding.mapping.MappingContext; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.definition.registry.FlowDefinitionResource; +import org.springframework.webflow.engine.EndState; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.samples.phonebook.stub.StubPhonebook; +import org.springframework.webflow.test.MockFlowServiceLocator; +import org.springframework.webflow.test.MockParameterMap; +import org.springframework.webflow.test.execution.AbstractXmlFlowExecutionTests; + +public class SearchFlowExecutionTests extends AbstractXmlFlowExecutionTests { + + public void testStartFlow() { + ApplicationView view = applicationView(startFlow()); + assertCurrentStateEquals("enterCriteria"); + assertViewNameEquals("searchCriteria", view); + assertModelAttributeNotNull("searchCriteria", view); + } + + public void testCriteriaSubmitSuccess() { + startFlow(); + MockParameterMap parameters = new MockParameterMap(); + parameters.put("firstName", "Keith"); + parameters.put("lastName", "Donald"); + ApplicationView view = applicationView(signalEvent("search", parameters)); + assertCurrentStateEquals("displayResults"); + assertViewNameEquals("searchResults", view); + assertModelAttributeCollectionSize(1, "results", view); + } + + public void testCriteriaSubmitError() { + startFlow(); + signalEvent("search"); + assertCurrentStateEquals("enterCriteria"); + } + + public void testNewSearch() { + testCriteriaSubmitSuccess(); + ApplicationView view = applicationView(signalEvent("newSearch")); + assertCurrentStateEquals("enterCriteria"); + assertViewNameEquals("searchCriteria", view); + } + + public void testSelectValidResult() { + testCriteriaSubmitSuccess(); + MockParameterMap parameters = new MockParameterMap(); + parameters.put("id", "1"); + ApplicationView view = applicationView(signalEvent("select", parameters)); + assertCurrentStateEquals("displayResults"); + assertViewNameEquals("searchResults", view); + assertModelAttributeCollectionSize(1, "results", view); + } + + @Override + protected FlowDefinitionResource getFlowDefinitionResource() { + return createFlowDefinitionResource("src/main/webapp/WEB-INF/flows/search-flow.xml"); + } + + @Override + protected void registerMockServices(MockFlowServiceLocator serviceRegistry) { + Flow mockDetailFlow = new Flow("detail-flow"); + mockDetailFlow.setInputMapper(new AttributeMapper() { + public void map(Object source, Object target, MappingContext context) { + assertEquals("id of value 1 not provided as input by calling search flow", new Long(1), ((AttributeMap)source).get("id")); + } + }); + // test responding to finish result + new EndState(mockDetailFlow, "finish"); + + serviceRegistry.registerSubflow(mockDetailFlow); + serviceRegistry.registerBean("phonebook", new StubPhonebook()); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/.classpath b/spring-webflow-samples/phonebook/.classpath new file mode 100644 index 00000000..1ff9d14b --- /dev/null +++ b/spring-webflow-samples/phonebook/.classpath @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/spring-webflow-samples/phonebook/.cvsignore b/spring-webflow-samples/phonebook/.cvsignore new file mode 100644 index 00000000..16b87eb5 --- /dev/null +++ b/spring-webflow-samples/phonebook/.cvsignore @@ -0,0 +1,8 @@ +target +lib +bak +build.properties +*.log +*.jpx.local* +*.iws +*.tws diff --git a/spring-webflow-samples/phonebook/.project b/spring-webflow-samples/phonebook/.project new file mode 100644 index 00000000..dd0deb24 --- /dev/null +++ b/spring-webflow-samples/phonebook/.project @@ -0,0 +1,36 @@ + + + swf-phonebook + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + + org.springframework.ide.eclipse.core.springnature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + + diff --git a/spring-webflow-samples/phonebook/.settings/org.eclipse.jdt.core.prefs b/spring-webflow-samples/phonebook/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..52db45d8 --- /dev/null +++ b/spring-webflow-samples/phonebook/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.5 diff --git a/spring-webflow-samples/phonebook/.settings/org.eclipse.jdt.ui.prefs b/spring-webflow-samples/phonebook/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..31c851b5 --- /dev/null +++ b/spring-webflow-samples/phonebook/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/spring-webflow-samples/phonebook/.settings/org.eclipse.jst.common.project.facet.core.prefs b/spring-webflow-samples/phonebook/.settings/org.eclipse.jst.common.project.facet.core.prefs new file mode 100755 index 00000000..1973f017 --- /dev/null +++ b/spring-webflow-samples/phonebook/.settings/org.eclipse.jst.common.project.facet.core.prefs @@ -0,0 +1,4 @@ +#Mon Nov 13 17:23:47 PST 2006 +classpath.helper/org.eclipse.jdt.launching.JRE_CONTAINER/owners=jst.java\:5.0 +classpath.helper/org.eclipse.jst.server.core.container\:\:org.eclipse.jst.server.tomcat.runtimeTarget\:\:Apache\ Tomcat\ v5.5/owners=jst.web\:2.4 +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/phonebook/.settings/org.eclipse.wst.common.component b/spring-webflow-samples/phonebook/.settings/org.eclipse.wst.common.component new file mode 100755 index 00000000..2d331bb9 --- /dev/null +++ b/spring-webflow-samples/phonebook/.settings/org.eclipse.wst.common.component @@ -0,0 +1,51 @@ + + + + + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + + + + diff --git a/spring-webflow-samples/phonebook/.settings/org.eclipse.wst.common.project.facet.core.xml b/spring-webflow-samples/phonebook/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100755 index 00000000..b7687079 --- /dev/null +++ b/spring-webflow-samples/phonebook/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spring-webflow-samples/phonebook/.settings/org.eclipse.wst.validation.prefs b/spring-webflow-samples/phonebook/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 00000000..1c7d6317 --- /dev/null +++ b/spring-webflow-samples/phonebook/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,6 @@ +#Mon Nov 13 17:29:33 PST 2006 +DELEGATES_PREFERENCE=delegateValidatorListorg.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator\=org.eclipse.wst.wsdl.validation.internal.eclipse.Validator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator\=org.eclipse.wst.xsd.core.internal.validation.eclipse.Validator; +USER_BUILD_PREFERENCE=enabledBuildValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_MANUAL_PREFERENCE=enabledManualValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_PREFERENCE=overrideGlobalPreferencesfalse +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/phonebook/.springBeans b/spring-webflow-samples/phonebook/.springBeans new file mode 100644 index 00000000..0fc6a791 --- /dev/null +++ b/spring-webflow-samples/phonebook/.springBeans @@ -0,0 +1,41 @@ + + + + xml + + + src/main/webapp/WEB-INF/phonebook-servlet-config.xml + src/main/webapp/WEB-INF/flows/search-flow-beans.xml + src/main/java/org/springframework/webflow/samples/phonebook/stub/services-config.xml + src/main/webapp/WEB-INF/phonebook-webflow-config.xml + + + + serviceLayer + false + false + + src/main/java/org/springframework/webflow/samples/phonebook/stub/services-config.xml + + + + searchFlow + false + false + + src/main/webapp/WEB-INF/flows/search-flow-beans.xml + + + + webapp + false + false + + src/main/webapp/WEB-INF/flows/search-flow-beans.xml + src/main/webapp/WEB-INF/phonebook-servlet-config.xml + src/main/webapp/WEB-INF/phonebook-webflow-config.xml + src/main/java/org/springframework/webflow/samples/phonebook/stub/services-config.xml + + + + diff --git a/spring-webflow-samples/phonebook/build.xml b/spring-webflow-samples/phonebook/build.xml new file mode 100644 index 00000000..d5e71e29 --- /dev/null +++ b/spring-webflow-samples/phonebook/build.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/spring-webflow-samples/phonebook/ivy.xml b/spring-webflow-samples/phonebook/ivy.xml new file mode 100644 index 00000000..4318dd92 --- /dev/null +++ b/spring-webflow-samples/phonebook/ivy.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/project.properties b/spring-webflow-samples/phonebook/project.properties new file mode 100644 index 00000000..f80ab3a7 --- /dev/null +++ b/spring-webflow-samples/phonebook/project.properties @@ -0,0 +1,10 @@ +# properties defined in this file are overridable by a local build.properties in this project dir + +# The location of the common build system +common.build.dir=${basedir}/../../common-build + +javac.source=1.5 +javac.target=1.5 + +# Do not publish built artifacts to the integration repository +do.publish=false \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/Person.java b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/Person.java new file mode 100644 index 00000000..f9952d30 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/Person.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class Person implements Serializable { + + private Long id; + + private String firstName; + + private String lastName; + + private String userId; + + private String phone; + + private List colleagues = new ArrayList(); + + public Person() { + this(-1, "", "", "", ""); + } + + public Person(long id, String firstName, String lastName, String userId, String phone) { + this.id = new Long(id); + this.firstName = firstName; + this.lastName = lastName; + this.userId = userId; + this.phone = phone; + } + + public Long getId() { + return id; + } + + public String getFirstName() { + return this.firstName; + } + + public String getLastName() { + return this.lastName; + } + + public String getUserId() { + return userId; + } + + public String getPhone() { + return this.phone; + } + + public List getColleagues() { + return this.colleagues; + } + + public int getColleagueCount() { + return this.colleagues.size(); + } + + public Person getColleague(int i) { + return this.colleagues.get(i); + } + + public void addColleague(Person colleague) { + this.colleagues.add(colleague); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/Phonebook.java b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/Phonebook.java new file mode 100644 index 00000000..29d2f7ae --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/Phonebook.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook; + +import java.util.List; + +public interface Phonebook { + + public List search(SearchCriteria criteria); + + public Person getPerson(Long id); + + public Person getPerson(String userId); +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/SearchCriteria.java b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/SearchCriteria.java new file mode 100644 index 00000000..183793dd --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/SearchCriteria.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook; + +import java.io.Serializable; + +public class SearchCriteria implements Serializable { + + private String firstName = ""; + + private String lastName = ""; + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/SearchCriteriaValidator.java b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/SearchCriteriaValidator.java new file mode 100644 index 00000000..6a7d6dbd --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/SearchCriteriaValidator.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook; + +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +public class SearchCriteriaValidator implements Validator { + + public boolean supports(Class clazz) { + return clazz.equals(SearchCriteria.class); + } + + public void validate(Object obj, Errors errors) { + SearchCriteria query = (SearchCriteria)obj; + if (!StringUtils.hasText(query.getFirstName()) && !StringUtils.hasText(query.getLastName())) { + errors.reject("noCriteria", "Please provide some query criteria!"); + } + } +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/stub/StubPhonebook.java b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/stub/StubPhonebook.java new file mode 100644 index 00000000..dc20c481 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/stub/StubPhonebook.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook.stub; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.webflow.samples.phonebook.Person; +import org.springframework.webflow.samples.phonebook.Phonebook; +import org.springframework.webflow.samples.phonebook.SearchCriteria; + +public class StubPhonebook implements Phonebook { + + private List persons = new ArrayList(); + + public StubPhonebook() { + // setup some dummy test data + Person kd = new Person(1, "Keith", "Donald", "kdonald", "11111"); + Person ev = new Person(2, "Erwin", "Vervaet", "klr8", "22222"); + Person cs = new Person(3, "Colin", "Sampaleanu", "sampa", "33333"); + Person jh = new Person(4, "Juergen", "Hoeller", "jhoeller", "44444"); + Person rj = new Person(5, "Rod", "Johnson", "rod", "55555"); + Person tr = new Person(6, "Thomas", "Risberg", "trisberg", "66666"); + Person aa = new Person(7, "Alef", "Arendsen", "alef", "77777"); + Person mp = new Person(8, "Mark", "Pollack", "mark", "88888"); + + kd.addColleague(ev); + kd.addColleague(cs); + kd.addColleague(jh); + kd.addColleague(rj); + kd.addColleague(tr); + kd.addColleague(aa); + kd.addColleague(mp); + + ev.addColleague(kd); + ev.addColleague(cs); + ev.addColleague(jh); + ev.addColleague(rj); + + cs.addColleague(kd); + cs.addColleague(ev); + cs.addColleague(jh); + cs.addColleague(rj); + cs.addColleague(aa); + cs.addColleague(mp); + + rj.addColleague(cs); + rj.addColleague(kd); + rj.addColleague(ev); + rj.addColleague(jh); + rj.addColleague(tr); + rj.addColleague(aa); + rj.addColleague(mp); + + jh.addColleague(cs); + jh.addColleague(kd); + jh.addColleague(ev); + jh.addColleague(jh); + jh.addColleague(tr); + jh.addColleague(aa); + + Person sa = new Person(9, "Shaun", "Alexander", "rolltide", "44444"); + Person dj = new Person(10, "Darell", "Jackson", "gatorcountry", "55555"); + sa.addColleague(dj); + dj.addColleague(sa); + + persons.add(kd); + persons.add(ev); + persons.add(cs); + persons.add(jh); + persons.add(rj); + persons.add(tr); + persons.add(aa); + persons.add(mp); + + persons.add(sa); + persons.add(dj); + } + + public List search(SearchCriteria query) { + List res = new ArrayList(); + for (Person person : persons) { + if ((person.getFirstName().indexOf(query.getFirstName()) != -1) + && (person.getLastName().indexOf(query.getLastName()) != -1)) { + res.add(person); + } + } + return res; + } + + public Person getPerson(Long id) { + for (Person person : persons) { + if (person.getId().equals(id)) { + return person; + } + } + return null; + } + + public Person getPerson(String userId) { + for (Person person : persons) { + if (person.getUserId().equals(userId)) { + return person; + } + } + return null; + } + +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/stub/services-config.xml b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/stub/services-config.xml new file mode 100644 index 00000000..b47c2188 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/stub/services-config.xml @@ -0,0 +1,10 @@ + + + + + + diff --git a/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PersonDetailFlowBuilder.java b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PersonDetailFlowBuilder.java new file mode 100644 index 00000000..9458deaf --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PersonDetailFlowBuilder.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook.webflow; + +import org.springframework.binding.mapping.DefaultAttributeMapper; +import org.springframework.binding.mapping.Mapping; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.engine.builder.AbstractFlowBuilder; +import org.springframework.webflow.engine.builder.FlowBuilderException; +import org.springframework.webflow.engine.builder.FlowServiceLocator; +import org.springframework.webflow.engine.support.ConfigurableFlowAttributeMapper; + +/** + * Java-based flow builder that builds the person details flow, exactly like it + * is defined in the detail-flow.xml XML flow definition. + *

+ * This encapsulates the page flow of viewing a person's details and their + * collegues in a reusable, self-contained module. + * + * @author Keith Donald + */ +class PersonDetailFlowBuilder extends AbstractFlowBuilder { + + public PersonDetailFlowBuilder(FlowServiceLocator flowServiceLocator) { + super(flowServiceLocator); + } + + public void buildInputMapper() throws FlowBuilderException { + Mapping idMapping = mapping().source("id").target("flowScope.id").value(); + getFlow().setInputMapper(new DefaultAttributeMapper().addMapping(idMapping)); + } + + public void buildStates() throws FlowBuilderException { + // get the person given a userid as input + addActionState("getDetails", action("phonebook", method("getPerson(${flowScope.id})"), result("person")), + transition(on(success()), to("displayDetails"))); + + // view the person details + addViewState("displayDetails", "details", new Transition[] { transition(on(back()), to("finish")), + transition(on(select()), to("browseColleagueDetails")) }); + + // view details for selected collegue + ConfigurableFlowAttributeMapper idMapper = new ConfigurableFlowAttributeMapper(); + idMapper.addInputMapping(mapping().source("requestParameters.id").target("id").from(String.class) + .to(Long.class).value()); + addSubflowState("browseColleagueDetails", getFlow(), idMapper, transition(on(finish()), to("getDetails"))); + + // end + addEndState("finish"); + + // end error + addEndState("error"); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PhonebookFlowRegistrar.java b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PhonebookFlowRegistrar.java new file mode 100644 index 00000000..9eb78773 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PhonebookFlowRegistrar.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook.webflow; + +import org.springframework.webflow.definition.registry.FlowDefinitionHolder; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistrar; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.definition.registry.StaticFlowDefinitionHolder; +import org.springframework.webflow.engine.builder.FlowAssembler; +import org.springframework.webflow.engine.builder.FlowBuilder; +import org.springframework.webflow.engine.builder.FlowServiceLocator; + +/** + * Demonstrates how to register flows programatically. + * + * @author Keith Donald + */ +class PhonebookFlowRegistrar implements FlowDefinitionRegistrar { + + private FlowServiceLocator serviceLocator; + + public PhonebookFlowRegistrar(FlowServiceLocator serviceLocator) { + this.serviceLocator = serviceLocator; + } + + public void registerFlowDefinitions(FlowDefinitionRegistry registry) { + registry.registerFlowDefinition(assemble("detail-flow", new PersonDetailFlowBuilder(serviceLocator))); + registry.registerFlowDefinition(assemble("search-flow", new SearchPersonFlowBuilder(serviceLocator))); + } + + private FlowDefinitionHolder assemble(String flowId, FlowBuilder flowBuilder) { + return new StaticFlowDefinitionHolder(new FlowAssembler(flowId, flowBuilder).assembleFlow()); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PhonebookFlowRegistryFactoryBean.java b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PhonebookFlowRegistryFactoryBean.java new file mode 100644 index 00000000..1ebace80 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/PhonebookFlowRegistryFactoryBean.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook.webflow; + +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.engine.builder.AbstractFlowBuildingFlowRegistryFactoryBean; + +/** + * Demonstrates how to populate a flow registry programatically. + * + * @author Keith Donald + */ +public class PhonebookFlowRegistryFactoryBean extends AbstractFlowBuildingFlowRegistryFactoryBean { + + protected void doPopulate(FlowDefinitionRegistry registry) { + new PhonebookFlowRegistrar(getFlowServiceLocator()).registerFlowDefinitions(registry); + } +} diff --git a/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/SearchPersonFlowBuilder.java b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/SearchPersonFlowBuilder.java new file mode 100644 index 00000000..f8edc999 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/SearchPersonFlowBuilder.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook.webflow; + +import org.springframework.webflow.action.FormAction; +import org.springframework.webflow.action.MultiAction; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.engine.builder.AbstractFlowBuilder; +import org.springframework.webflow.engine.builder.FlowBuilderException; +import org.springframework.webflow.engine.builder.FlowServiceLocator; +import org.springframework.webflow.engine.support.ConfigurableFlowAttributeMapper; +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.samples.phonebook.SearchCriteria; +import org.springframework.webflow.samples.phonebook.SearchCriteriaValidator; + +/** + * Java-based flow builder that searches for people in the phonebook. The flow + * defined by this class is exactly the same as that defined in the + * search-flow.xml XML flow definition. + *

+ * This encapsulates the page flow of searching for some people, selecting a + * person you care about, and viewing their person's details and those of their + * collegues in a reusable, self-contained module. + * + * @author Keith Donald + */ +class SearchPersonFlowBuilder extends AbstractFlowBuilder { + + public SearchPersonFlowBuilder(FlowServiceLocator flowServiceLocator) { + super(flowServiceLocator); + } + + public void buildStates() throws FlowBuilderException { + // view search criteria + MultiAction searchFormAction = createSearchFormAction(); + addViewState("enterCriteria", "searchCriteria", invoke("setupForm", searchFormAction), + new Transition[] { transition(on("search"), to("executeSearch"), ifReturnedSuccess(invoke( + "bindAndValidate", searchFormAction))) }); + + // execute query + addActionState("executeSearch", action("phonebook", method("search(${flowScope.searchCriteria})"), + result("results")), transition(on(success()), to("displayResults"))); + + // view results + addViewState("displayResults", "searchResults", new Transition[] { + transition(on("newSearch"), to("enterCriteria")), transition(on(select()), to("browseDetails")) }); + + // view details for selected user id + ConfigurableFlowAttributeMapper idMapper = new ConfigurableFlowAttributeMapper(); + idMapper.addInputMapping(mapping().source("requestParameters.id").target("id").from(String.class) + .to(Long.class).value()); + addSubflowState("browseDetails", flow("detail-flow"), idMapper, transition(on(finish()), to("executeSearch"))); + + // end - an error occured + addEndState(error(), "error"); + } + + protected FormAction createSearchFormAction() { + FormAction action = new FormAction(SearchCriteria.class); + action.setFormObjectScope(ScopeType.FLOW); + action.setValidator(new SearchCriteriaValidator()); + return action; + } +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/phonebook-webflow-config.xml b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/phonebook-webflow-config.xml new file mode 100644 index 00000000..25342399 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/java/org/springframework/webflow/samples/phonebook/webflow/phonebook-webflow-config.xml @@ -0,0 +1,17 @@ + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/classes/log4j.properties b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/classes/log4j.properties new file mode 100644 index 00000000..1381d28c --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/classes/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +#Enable webflow debug logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG diff --git a/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/flows/detail-flow.xml b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/flows/detail-flow.xml new file mode 100644 index 00000000..0bba64c5 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/flows/detail-flow.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/flows/search-flow-beans.xml b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/flows/search-flow-beans.xml new file mode 100644 index 00000000..711c661f --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/flows/search-flow-beans.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/flows/search-flow.xml b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/flows/search-flow.xml new file mode 100644 index 00000000..8b38a423 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/flows/search-flow.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/details.jsp b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/details.jsp new file mode 100644 index 00000000..b5cecf43 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/details.jsp @@ -0,0 +1,51 @@ +<%@ include file="includeTop.jsp" %> + +

+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Person Details

First Name${person.firstName}
Last Name${person.lastName}
User Id${person.userId}
Phone${person.phone}
+
+ Colleagues: +
+ + + ${colleague.firstName} ${colleague.lastName}
+
+
+
+ + +
+
+
\ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/error.jsp b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/error.jsp new file mode 100644 index 00000000..87754023 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/error.jsp @@ -0,0 +1,18 @@ +<%@ page session="false" %> + + + + + + +
Error
+
+
+

+ An error has occured! +

+
+
+
+ + diff --git a/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/includeBottom.jsp b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/includeBottom.jsp new file mode 100644 index 00000000..3b0f1dd6 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/includeBottom.jsp @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/includeTop.jsp b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/includeTop.jsp new file mode 100644 index 00000000..bc8f8463 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/includeTop.jsp @@ -0,0 +1,21 @@ +<%@ page contentType="text/html" %> +<%@ page session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> + + + +Search the Phonebook + + + + + + + + diff --git a/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/searchCriteria.jsp b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/searchCriteria.jsp new file mode 100644 index 00000000..0fd259e8 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/searchCriteria.jsp @@ -0,0 +1,51 @@ +<%@ include file="includeTop.jsp" %> + +
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Search Criteria
+
+
+
Please provide valid search criteria
+
First Name + +
Last Name + +
+
+
+ + +
+
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/searchResults.jsp b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/searchResults.jsp new file mode 100644 index 00000000..ac186ba7 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/jsp/searchResults.jsp @@ -0,0 +1,53 @@ +<%@ include file="includeTop.jsp" %> + +
+
+ +
+
+ + + + + + + + + + + + + +
+ Search Results +
+
+
+ + + + + + + + + + + + + + + +
First NameLast NameUser IdPhone
${person.firstName}${person.lastName} + + ${person.userId} + + ${person.phone}
+
+ + +
+
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/phonebook-servlet-config.xml b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/phonebook-servlet-config.xml new file mode 100644 index 00000000..59c33b2f --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/phonebook-servlet-config.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/phonebook-webflow-config.xml b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/phonebook-webflow-config.xml new file mode 100644 index 00000000..96f211e4 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/phonebook-webflow-config.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/web.xml b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..971338bd --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,43 @@ + + + + + + contextConfigLocation + + classpath:org/springframework/webflow/samples/phonebook/stub/services-config.xml + + + + + org.springframework.web.context.ContextLoaderListener + + + + phonebook + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + + /WEB-INF/phonebook-servlet-config.xml + /WEB-INF/phonebook-webflow-config.xml + + + + + + + phonebook + *.htm + + + + index.jsp + + + \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/main/webapp/images/spring-logo.jpg b/spring-webflow-samples/phonebook/src/main/webapp/images/spring-logo.jpg new file mode 100644 index 00000000..62be3983 Binary files /dev/null and b/spring-webflow-samples/phonebook/src/main/webapp/images/spring-logo.jpg differ diff --git a/spring-webflow-samples/phonebook/src/main/webapp/images/webflow-logo.jpg b/spring-webflow-samples/phonebook/src/main/webapp/images/webflow-logo.jpg new file mode 100644 index 00000000..ed76bae0 Binary files /dev/null and b/spring-webflow-samples/phonebook/src/main/webapp/images/webflow-logo.jpg differ diff --git a/spring-webflow-samples/phonebook/src/main/webapp/index.jsp b/spring-webflow-samples/phonebook/src/main/webapp/index.jsp new file mode 100644 index 00000000..0425d61c --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/index.jsp @@ -0,0 +1,21 @@ +<%@ page session="true" %> <%-- make sure we have a session --%> + + + + + + +
Phonebook - A Spring Web Flow Sample
+
+
+

+ Phonebook +

+

+ This sample application illustrates core features of the web flow system. +

+
+
+
+ + diff --git a/spring-webflow-samples/phonebook/src/main/webapp/style.css b/spring-webflow-samples/phonebook/src/main/webapp/style.css new file mode 100644 index 00000000..f4b0a64e --- /dev/null +++ b/spring-webflow-samples/phonebook/src/main/webapp/style.css @@ -0,0 +1,58 @@ +body { + width: 720px; + margin: 0px; + padding: 0px; +} + +div#logo { + width: 720px; + height: 73px; + background: #86AEA5; +} + +div#navigation { + width: 720px; + height: 15px; + background: #E2F3B8; + text-align: right; +} + +div#content { + width: 720px; + padding: 5px; +} + +div#insert { + width: 120; + float: right; + text-align: right; +} + +.buttonBar { + height: 1.5em; + text-align: right; +} + +div#copyright { + width: 720px; +} + +div#copyright p { + text-align: center; + font-family: Tahoma, sans-serif; + font-size: 75%; + color: div#336633; + margin-left: 5px; + font-weight: bold; + clear: both; +} + +.readOnly { + color: rgb(192, 192, 192); +} + +.error { + color: red; + font-weight: bold; + font-family: Arial, sans-serif; +} \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/test/java/log4j.properties b/spring-webflow-samples/phonebook/src/test/java/log4j.properties new file mode 100644 index 00000000..5ba9222c --- /dev/null +++ b/spring-webflow-samples/phonebook/src/test/java/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable web flow logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-webflow-samples/phonebook/src/test/java/org/springframework/webflow/samples/phonebook/webflow/SearchFlowExecutionTests.java b/spring-webflow-samples/phonebook/src/test/java/org/springframework/webflow/samples/phonebook/webflow/SearchFlowExecutionTests.java new file mode 100644 index 00000000..ed4d6f19 --- /dev/null +++ b/spring-webflow-samples/phonebook/src/test/java/org/springframework/webflow/samples/phonebook/webflow/SearchFlowExecutionTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.phonebook.webflow; + +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.binding.mapping.MappingContext; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.definition.registry.FlowDefinitionResource; +import org.springframework.webflow.engine.EndState; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.samples.phonebook.stub.StubPhonebook; +import org.springframework.webflow.test.MockFlowServiceLocator; +import org.springframework.webflow.test.MockParameterMap; +import org.springframework.webflow.test.execution.AbstractXmlFlowExecutionTests; + +public class SearchFlowExecutionTests extends AbstractXmlFlowExecutionTests { + + public void testStartFlow() { + ApplicationView view = applicationView(startFlow()); + assertCurrentStateEquals("enterCriteria"); + assertViewNameEquals("searchCriteria", view); + assertModelAttributeNotNull("searchCriteria", view); + } + + public void testCriteriaSubmitSuccess() { + startFlow(); + MockParameterMap parameters = new MockParameterMap(); + parameters.put("firstName", "Keith"); + parameters.put("lastName", "Donald"); + ApplicationView view = applicationView(signalEvent("search", parameters)); + assertCurrentStateEquals("displayResults"); + assertViewNameEquals("searchResults", view); + assertModelAttributeCollectionSize(1, "results", view); + } + + public void testCriteriaSubmitError() { + startFlow(); + signalEvent("search"); + assertCurrentStateEquals("enterCriteria"); + } + + public void testNewSearch() { + testCriteriaSubmitSuccess(); + ApplicationView view = applicationView(signalEvent("newSearch")); + assertCurrentStateEquals("enterCriteria"); + assertViewNameEquals("searchCriteria", view); + } + + public void testSelectValidResult() { + testCriteriaSubmitSuccess(); + MockParameterMap parameters = new MockParameterMap(); + parameters.put("id", "1"); + ApplicationView view = applicationView(signalEvent("select", parameters)); + assertCurrentStateEquals("displayResults"); + assertViewNameEquals("searchResults", view); + assertModelAttributeCollectionSize(1, "results", view); + } + + @Override + protected FlowDefinitionResource getFlowDefinitionResource() { + return createFlowDefinitionResource("src/main/webapp/WEB-INF/flows/search-flow.xml"); + } + + @Override + protected void registerMockServices(MockFlowServiceLocator serviceRegistry) { + Flow mockDetailFlow = new Flow("detail-flow"); + mockDetailFlow.setInputMapper(new AttributeMapper() { + public void map(Object source, Object target, MappingContext context) { + assertEquals("id of value 1 not provided as input by calling search flow", new Long(1), ((AttributeMap)source).get("id")); + } + }); + // test responding to finish result + new EndState(mockDetailFlow, "finish"); + + serviceRegistry.registerSubflow(mockDetailFlow); + serviceRegistry.registerBean("phonebook", new StubPhonebook()); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/readme.txt b/spring-webflow-samples/readme.txt new file mode 100644 index 00000000..b3cc7e00 --- /dev/null +++ b/spring-webflow-samples/readme.txt @@ -0,0 +1,39 @@ +/* + * spring-webflow-samples + * + * birthdate - demonstrates Spring Web Flow Struts 1.1 or > integration + * fileupload - demonstrates multipart file upload + * flowlauncher - demonstrates the different ways to launch flows from web pages + * itemlist - demonstrates application transactions and inline flows + * numberguess - demonstrates how to play a game using a flow + * phonebook - central sample demonstrating most features + * phonebook-portlet - the phonebook sample in a portlet environment (notice how the flow definitions do not change) + * sellitem - demonstrates a wizard with conditional transitions and continuations + * sellitem-jsf - the sellitem sample in a jsf environment + * shippingrate - demonstrates Spring Web Flow together with Ajax technology + */ + +Sample pre-requisites: +---------------------- +* JDK 1.5 or > must be installed with the JAVA_HOME variable set + +* Ant 1.6 or > must be installed and in your system path + +* Ivy 1.3 or >; if you already have Ivy installed into your ant lib path, it must be Ivy 1.3 or >. Ivy 1.2 or < won't work. + If you do not have Ivy installed, a compatible version will be picked up automatically from ../../common-build/lib + +* A Servlet 2.4 and JSP 2.0-capable servlet container must be installed for sample app deployment + - The samples all use jsp 2.0 to take advantage of EL ${expressions} for elegance. + +To build all samples: +--------------------- +1. cd to the ../build-spring-webflow directory + +2. run 'ant dist' to produce deployable .war files for all samples + Built .war files are placed in target/artifacts/war within each sample directory. + +To build an individual sample: +--------------------- +1. cd to the sample root directory + +2. run 'ant dist' to produce a deployable .war file within target/artifacts/war \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/.classpath b/spring-webflow-samples/sellitem-jsf/.classpath new file mode 100644 index 00000000..e978e0d4 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/spring-webflow-samples/sellitem-jsf/.cvsignore b/spring-webflow-samples/sellitem-jsf/.cvsignore new file mode 100644 index 00000000..16b87eb5 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/.cvsignore @@ -0,0 +1,8 @@ +target +lib +bak +build.properties +*.log +*.jpx.local* +*.iws +*.tws diff --git a/spring-webflow-samples/sellitem-jsf/.m7project b/spring-webflow-samples/sellitem-jsf/.m7project new file mode 100644 index 00000000..d7fcf628 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/.m7project @@ -0,0 +1,16 @@ + + + + src/webapp + com.m7.webapp.java + + + + + + + + + + + diff --git a/spring-webflow-samples/sellitem-jsf/.project b/spring-webflow-samples/sellitem-jsf/.project new file mode 100644 index 00000000..96e03659 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/.project @@ -0,0 +1,36 @@ + + + swf-sellitem-jsf + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + + org.springframework.ide.eclipse.core.springnature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + + diff --git a/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.jdt.core.prefs b/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..52db45d8 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.5 diff --git a/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.jdt.ui.prefs b/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..350fecbc --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Sat Nov 05 22:15:26 EST 2005 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.jst.common.project.facet.core.prefs b/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.jst.common.project.facet.core.prefs new file mode 100755 index 00000000..1973f017 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.jst.common.project.facet.core.prefs @@ -0,0 +1,4 @@ +#Mon Nov 13 17:23:47 PST 2006 +classpath.helper/org.eclipse.jdt.launching.JRE_CONTAINER/owners=jst.java\:5.0 +classpath.helper/org.eclipse.jst.server.core.container\:\:org.eclipse.jst.server.tomcat.runtimeTarget\:\:Apache\ Tomcat\ v5.5/owners=jst.web\:2.4 +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.wst.common.component b/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.wst.common.component new file mode 100755 index 00000000..98ca2d01 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.wst.common.component @@ -0,0 +1,87 @@ + + + + + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + + + + diff --git a/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.wst.common.project.facet.core.xml b/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100755 index 00000000..b7687079 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.wst.validation.prefs b/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 00000000..1c7d6317 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,6 @@ +#Mon Nov 13 17:29:33 PST 2006 +DELEGATES_PREFERENCE=delegateValidatorListorg.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator\=org.eclipse.wst.wsdl.validation.internal.eclipse.Validator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator\=org.eclipse.wst.xsd.core.internal.validation.eclipse.Validator; +USER_BUILD_PREFERENCE=enabledBuildValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_MANUAL_PREFERENCE=enabledManualValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_PREFERENCE=overrideGlobalPreferencesfalse +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/sellitem-jsf/.springBeans b/spring-webflow-samples/sellitem-jsf/.springBeans new file mode 100644 index 00000000..4e16fa70 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/.springBeans @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spring-webflow-samples/sellitem-jsf/build.xml b/spring-webflow-samples/sellitem-jsf/build.xml new file mode 100644 index 00000000..43d10b55 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/build.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/ivy.xml b/spring-webflow-samples/sellitem-jsf/ivy.xml new file mode 100644 index 00000000..fadc9b72 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/ivy.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/project.properties b/spring-webflow-samples/sellitem-jsf/project.properties new file mode 100644 index 00000000..f80ab3a7 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/project.properties @@ -0,0 +1,10 @@ +# properties defined in this file are overridable by a local build.properties in this project dir + +# The location of the common build system +common.build.dir=${basedir}/../../common-build + +javac.source=1.5 +javac.target=1.5 + +# Do not publish built artifacts to the integration repository +do.publish=false \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/etc/filter.properties b/spring-webflow-samples/sellitem-jsf/src/etc/filter.properties new file mode 100644 index 00000000..c54e5880 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/etc/filter.properties @@ -0,0 +1,33 @@ +# $Header$ + +# Contains filterable project settings. Setting placeholders in filterable project text +# files will be replaced with these values when the 'statics' build target is run. +# +# You may add static settings directly to this source file in the format: +# setting=value e.g MY_SETTING=myvalue +# This is appropriate usage if you know the setting value will never change. +# +# At build time this source file is copied to the ${target.dir} where additional +# dynamic settings may be appended using the task. Use this approach +# when a setting value depends on the build or the local user's environment. +# +# An example of this approach is shown below: +# +# build.xml +# +# +# +# +# +# +# +# +# This allows for dynamic replacement values that are sourced from local properties files to facilitate +# local user settings. +# +# To refer to filterable settings within project source files like config files, JSPs, or +# other text files use the standard ant placeholder format: +# @SETTING_NAME@ e.g, @MY_SETTING@ and @MY_LOCAL_SETTING@ +# +# Your settings: diff --git a/spring-webflow-samples/sellitem-jsf/src/etc/test-resources/log4j.properties b/spring-webflow-samples/sellitem-jsf/src/etc/test-resources/log4j.properties new file mode 100644 index 00000000..59974077 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/etc/test-resources/log4j.properties @@ -0,0 +1,20 @@ +log4j.rootCategory=WARN, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=${@PROJECT_WEBAPP_NAME@.root}/@PROJECT_WEBAPP_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout + +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n + +#Enable webflow debug logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG diff --git a/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/InMemoryDatabaseCreator.java b/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/InMemoryDatabaseCreator.java new file mode 100644 index 00000000..93c25caf --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/InMemoryDatabaseCreator.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import org.springframework.jdbc.core.support.JdbcDaoSupport; + +public class InMemoryDatabaseCreator extends JdbcDaoSupport { + + @Override + protected void initDao() throws Exception { + String createSales = "create table T_SALES (ID int not null identity primary key, ITEM_COUNT int not null, PRICE double NOT NULL, category VARCHAR(1) NOT NULL, SHIPPING_TYPE varchar(1))"; + getJdbcTemplate().execute(createSales); + } + +} diff --git a/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/JdbcSaleProcessor.java b/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/JdbcSaleProcessor.java new file mode 100644 index 00000000..7e4d3af9 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/JdbcSaleProcessor.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import org.springframework.jdbc.core.support.JdbcDaoSupport; + +public class JdbcSaleProcessor extends JdbcDaoSupport implements SaleProcessor { + public void process(Sale sale) { + getJdbcTemplate() + .update("insert into T_SALES values (?, ?, ?, ?, ?)", + new Object[] { null, sale.getPrice(), sale.getItemCount(), sale.getCategory(), + sale.getShippingType() }); + } +} diff --git a/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/Sale.java b/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/Sale.java new file mode 100644 index 00000000..1b9da03a --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/Sale.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import java.io.Serializable; + +import org.springframework.core.style.ToStringCreator; + +public class Sale implements Serializable { + + private double price; + + private int itemCount; + + private String category; + + private boolean shipping; + + private String shippingType; + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public int getItemCount() { + return itemCount; + } + + public void setItemCount(int itemCount) { + this.itemCount = itemCount; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public boolean isShipping() { + return shipping; + } + + public void setShipping(boolean shipping) { + this.shipping = shipping; + } + + public String getShippingType() { + return shippingType; + } + + public void setShippingType(String shippingType) { + this.shippingType = shippingType; + } + + // business logic methods + + /** + * Returns the base amount of the sale, without discount or delivery costs. + */ + public double getAmount() { + return price * itemCount; + } + + /** + * Returns the discount rate to apply. + */ + public double getDiscountRate() { + double discount = 0.02; + if ("A".equals(category)) { + if (itemCount >= 100) { + discount = 0.1; + } + } + else if ("B".equals(category)) { + if (itemCount >= 200) { + discount = 0.2; + } + } + return discount; + } + + /** + * Returns the savings because of the discount. + */ + public double getSavings() { + return getDiscountRate() * getAmount(); + } + + /** + * Returns the delivery cost. + */ + public double getDeliveryCost() { + double delCost = 0.0; + if ("S".equals(shippingType)) { + delCost = 10.0; + } + else if ("E".equals(shippingType)) { + delCost = 20.0; + } + return delCost; + } + + /** + * Returns the total cost of the sale, including discount and delivery cost. + */ + public double getTotalCost() { + return getAmount() + getDeliveryCost() - getSavings(); + } + + public String toString() { + return new ToStringCreator(this).append("price", price).append("itemCount", itemCount).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/SaleProcessor.java b/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/SaleProcessor.java new file mode 100644 index 00000000..16d95634 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/SaleProcessor.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import org.springframework.transaction.annotation.Transactional; + +@Transactional +public interface SaleProcessor { + + public void process(Sale sale); +} diff --git a/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/SaleValidator.java b/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/SaleValidator.java new file mode 100644 index 00000000..0ea1c392 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/SaleValidator.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +public class SaleValidator implements Validator { + + public boolean supports(Class clazz) { + return Sale.class.equals(clazz); + } + + public void validate(Object obj, Errors errors) { + Sale sale = (Sale)obj; + validatePriceAndItemCount(sale, errors); + } + + public void validatePriceAndItemCount(Sale sale, Errors errors) { + // the next two items are normallhy more appropriately handled by JSF + // field + // validation. We'll leave them here for safety + if (sale.getItemCount() <= 0) { + errors.rejectValue("itemCount", "tooLittle", "Item count must be greater than 0"); + } + if (sale.getPrice() <= 0.0) { + errors.rejectValue("price", "tooLittle", "Price must be greater than 0.0"); + } + + // perhaps an artificial example, but we want to show that in the JSF + // integration + // validators are best used for validation of field relationships. + // Individual fields + // are better validated with simple JSF field validation + if (sale.getItemCount() * sale.getPrice() > 1000000) + errors.reject("saleTooLarge", "total dollar value for sale above allowed limit"); + } +} diff --git a/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/SellItemAction.java b/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/SellItemAction.java new file mode 100644 index 00000000..379717e6 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/SellItemAction.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import org.springframework.webflow.action.AbstractAction; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +public class SellItemAction extends AbstractAction { + + // this does nothing. We're just showing how in the JSF integration, while + // an action is not needed for binding, you can still add in an action to do + // something if you need to + protected Event doExecute(RequestContext context) throws Exception { + Sale sale = (Sale)context.getFlowScope().getRequired("sale", Sale.class); + sale.getAmount(); + return success(); + } + +} \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/services-config.xml b/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/services-config.xml new file mode 100644 index 00000000..c7364659 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/java/org/springframework/webflow/samples/sellitem/services-config.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/classes/log4j.properties b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/classes/log4j.properties new file mode 100644 index 00000000..5ba9222c --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/classes/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable web flow logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/faces-config.xml b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/faces-config.xml new file mode 100644 index 00000000..0b31b305 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/faces-config.xml @@ -0,0 +1,33 @@ + + + + + + + + + org.springframework.web.jsf.DelegatingNavigationHandlerProxy + + + org.springframework.webflow.executor.jsf.FlowPropertyResolver + + + org.springframework.webflow.executor.jsf.FlowVariableResolver + + + org.springframework.web.jsf.DelegatingVariableResolver + + + + org.springframework.web.jsf.WebApplicationContextVariableResolver + + + + + + org.springframework.web.jsf.DelegatingPhaseListenerMulticaster + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/flows/sellitem-flow.xml b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/flows/sellitem-flow.xml new file mode 100644 index 00000000..487f3182 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/flows/sellitem-flow.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/web.xml b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..4af3701c --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,55 @@ + + + + + + webAppRootKey + swf-sellitem-jsf.root + + + + log4jConfigLocation + /WEB-INF/classes/log4j.properties + + + + contextConfigLocation + + classpath:org/springframework/webflow/samples/sellitem/services-config.xml + /WEB-INF/webflow-config.xml + + + + + org.springframework.web.context.ContextLoaderListener + + + + org.springframework.web.util.Log4jConfigListener + + + + + org.apache.myfaces.webapp.StartupServletContextListener + + + + + Faces Servlet + javax.faces.webapp.FacesServlet + 1 + + + + Faces Servlet + *.jsf + + + + index.jsp + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/webflow-config.xml b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/webflow-config.xml new file mode 100644 index 00000000..5db370a3 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/WEB-INF/webflow-config.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/categoryForm.jsp b/spring-webflow-samples/sellitem-jsf/src/main/webapp/categoryForm.jsp new file mode 100644 index 00000000..693902f5 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/categoryForm.jsp @@ -0,0 +1,45 @@ +<%@ include file="includeTop.jsp" %> + + + +
+
+

Select category

+ + + + + + + + + + + + + + + + + + + + + + +
Price:
Item count:
Category: + + + + + +
Is shipping required?: + +
+ +
+
+ +
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/costOverview.jsp b/spring-webflow-samples/sellitem-jsf/src/main/webapp/costOverview.jsp new file mode 100644 index 00000000..55d6678d --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/costOverview.jsp @@ -0,0 +1,59 @@ +<%@ include file="includeTop.jsp" %> + + + +
+
+

Purchase cost overview

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Price:${sale.price}
Item count:${sale.itemCount}
Category:${sale.category}
Shipping:${sale.shippingType}No shipping required: you're picking up the items
Base amount:${sale.amount}
Delivery cost:${sale.deliveryCost}
Discount:${sale.savings} (Discount rate: ${sale.discountRate})

Total cost:${sale.totalCost}
+
"> + +
+
+
+ +
+ +<%@ include file="includeBottom.jsp" %> diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/error.jsp b/spring-webflow-samples/sellitem-jsf/src/main/webapp/error.jsp new file mode 100644 index 00000000..65d8ab8c --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/error.jsp @@ -0,0 +1,17 @@ +<%@ include file="includeTop.jsp" %> + +
+
+ +
+

+ + Duplicate submit of the same transaction not allowed! + +

+

+ Sell a new item +

+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/images/spring-logo.jpg b/spring-webflow-samples/sellitem-jsf/src/main/webapp/images/spring-logo.jpg new file mode 100644 index 00000000..62be3983 Binary files /dev/null and b/spring-webflow-samples/sellitem-jsf/src/main/webapp/images/spring-logo.jpg differ diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/images/webflow-logo.jpg b/spring-webflow-samples/sellitem-jsf/src/main/webapp/images/webflow-logo.jpg new file mode 100644 index 00000000..ed76bae0 Binary files /dev/null and b/spring-webflow-samples/sellitem-jsf/src/main/webapp/images/webflow-logo.jpg differ diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/includeBottom.jsp b/spring-webflow-samples/sellitem-jsf/src/main/webapp/includeBottom.jsp new file mode 100644 index 00000000..3b0f1dd6 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/includeBottom.jsp @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/includeTop.jsp b/spring-webflow-samples/sellitem-jsf/src/main/webapp/includeTop.jsp new file mode 100644 index 00000000..b2361ae8 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/includeTop.jsp @@ -0,0 +1,25 @@ +<%@ page contentType="text/html" %> +<%@ page session="false" %> + +<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%> +<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%> + +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + + + +Sell an item + + + + + + + + diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/index.jsp b/spring-webflow-samples/sellitem-jsf/src/main/webapp/index.jsp new file mode 100644 index 00000000..91083c0e --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/index.jsp @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/intro.jsp b/spring-webflow-samples/sellitem-jsf/src/main/webapp/intro.jsp new file mode 100644 index 00000000..00786e8a --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/intro.jsp @@ -0,0 +1,81 @@ +<%@ page session="true" %> <%-- make sure we have a session --%> +<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h"%> +<%@ taglib uri="http://java.sun.com/jsf/core" prefix="f"%> + + + + + + +
Sell Item - A Spring Web Flow Sample (JSF Version)
+ +
+ +
+

+ + + +

+ +

+ This Spring web flow sample application implements the example application + discussed in the article + + Use continuations to develop complex Web applications. It illustrates + the following concepts: +

    +
  • + Spring Web Flow's JSF integration. +
  • +
  • + Using the flowId: command link prefix to let the view tell the web + flow controller which flow needs to be started. +
  • +
  • + Implementing a wizard using web flows. +
  • +
  • + Using OGNL based conditional expressions. +
  • +
+
    +
  • + Note on continuations: The original sellitem sample shows + continuations in use, in the words of the intro, "Using + continuations to make the flow completely stable, no matter + how browser navigation buttons are used."
    + This JSF version of sellitem is currently set to use normal + session storage.
    + The JSF Web Flow integration does support the continuation storages. + However, because JSF page components themselves have internal state, + it is not enough for Web Flow to be using continuation storage, the + JSF engine itself must be configured to use client-side or server-side + continuation style storage for the component state, instead of the + normal shared, Session based storage. We have not yet investigated + how to set MyFaces or the JSR RI to use client side storage, but + it is theoretically possible at least to the extent that the JSF + specification talks about JSF implementations offering it as an + option. We have seen no discussion of server-side continuation- + style storage for JSF component state
    + If you do configure your JSF engine for client-side storage of + component state, and set Web Flow to use client side continuation + storage it does mean that pages can not trigger the auto-creation of + flow variables on demand since the flow execution id needs to be known + to the page, and the id _is_ the storage for the flow state, a classic + chicken and egg situation. Just make sure any flow-scoped variables + are created ahead of time in the flow, before any JSF page component + tried to reference them. +
  • +
+

+
+ +
+ +
+ + +
+ + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/priceAndItemCountForm.jsp b/spring-webflow-samples/sellitem-jsf/src/main/webapp/priceAndItemCountForm.jsp new file mode 100644 index 00000000..005d2de6 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/priceAndItemCountForm.jsp @@ -0,0 +1,54 @@ +<%@ page contentType="text/html" %> +<%@ include file="includeTop.jsp"%> + + + +
+
+ + + + + +
${curError}
+
+
+
+ +

Enter price and item count

+
+ + + + + + + + + + + + + + +
Price: + + +    + +
Item count: + + + +    + +
+ +
+
+ +
+ +<%@ include file="includeBottom.jsp"%> diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/shippingDetailsForm.jsp b/spring-webflow-samples/sellitem-jsf/src/main/webapp/shippingDetailsForm.jsp new file mode 100644 index 00000000..64765688 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/shippingDetailsForm.jsp @@ -0,0 +1,44 @@ +<%@ include file="includeTop.jsp" %> + + + +
+
+

Enter shipping information

+
+ + + + + + + + + + + + + + + + + + + + + + + +
Price:
Item count:
Category:
Shipping:
Shipping type: + + + + +
+ +
+
+ +
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/main/webapp/style.css b/spring-webflow-samples/sellitem-jsf/src/main/webapp/style.css new file mode 100644 index 00000000..f4b0a64e --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/main/webapp/style.css @@ -0,0 +1,58 @@ +body { + width: 720px; + margin: 0px; + padding: 0px; +} + +div#logo { + width: 720px; + height: 73px; + background: #86AEA5; +} + +div#navigation { + width: 720px; + height: 15px; + background: #E2F3B8; + text-align: right; +} + +div#content { + width: 720px; + padding: 5px; +} + +div#insert { + width: 120; + float: right; + text-align: right; +} + +.buttonBar { + height: 1.5em; + text-align: right; +} + +div#copyright { + width: 720px; +} + +div#copyright p { + text-align: center; + font-family: Tahoma, sans-serif; + font-size: 75%; + color: div#336633; + margin-left: 5px; + font-weight: bold; + clear: both; +} + +.readOnly { + color: rgb(192, 192, 192); +} + +.error { + color: red; + font-weight: bold; + font-family: Arial, sans-serif; +} \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/test/java/log4j.properties b/spring-webflow-samples/sellitem-jsf/src/test/java/log4j.properties new file mode 100644 index 00000000..5ba9222c --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/test/java/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable web flow logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-webflow-samples/sellitem-jsf/src/test/java/org/springframework/webflow/samples/sellitem/SaleProcessorIntegrationTests.java b/spring-webflow-samples/sellitem-jsf/src/test/java/org/springframework/webflow/samples/sellitem/SaleProcessorIntegrationTests.java new file mode 100644 index 00000000..e62338c8 --- /dev/null +++ b/spring-webflow-samples/sellitem-jsf/src/test/java/org/springframework/webflow/samples/sellitem/SaleProcessorIntegrationTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; + +public class SaleProcessorIntegrationTests extends AbstractTransactionalDataSourceSpringContextTests { + + private SaleProcessor saleProcessor; + + public void setSaleProcessor(SaleProcessor saleProcessor) { + this.saleProcessor = saleProcessor; + } + + @Override + protected String[] getConfigLocations() { + return new String[] { "classpath:org/springframework/webflow/samples/sellitem/services-config.xml" }; + } + + public void testProcessSale() { + int beforeCount = jdbcTemplate.queryForInt("select count(*) from T_SALES"); + Sale sale = new Sale(); + sale.setItemCount(25); + sale.setPrice(100.0); + sale.setCategory("A"); + sale.setShippingType("Express"); + saleProcessor.process(sale); + int afterCount = jdbcTemplate.queryForInt("select count(*) from T_SALES"); + assertEquals("Wrong after count", beforeCount + 1, afterCount); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/.classpath b/spring-webflow-samples/sellitem/.classpath new file mode 100644 index 00000000..512a96ed --- /dev/null +++ b/spring-webflow-samples/sellitem/.classpath @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/spring-webflow-samples/sellitem/.cvsignore b/spring-webflow-samples/sellitem/.cvsignore new file mode 100644 index 00000000..16b87eb5 --- /dev/null +++ b/spring-webflow-samples/sellitem/.cvsignore @@ -0,0 +1,8 @@ +target +lib +bak +build.properties +*.log +*.jpx.local* +*.iws +*.tws diff --git a/spring-webflow-samples/sellitem/.project b/spring-webflow-samples/sellitem/.project new file mode 100644 index 00000000..9f2a0079 --- /dev/null +++ b/spring-webflow-samples/sellitem/.project @@ -0,0 +1,36 @@ + + + swf-sellitem + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + + org.springframework.ide.eclipse.core.springnature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + + diff --git a/spring-webflow-samples/sellitem/.settings/org.eclipse.jdt.core.prefs b/spring-webflow-samples/sellitem/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..52db45d8 --- /dev/null +++ b/spring-webflow-samples/sellitem/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.5 diff --git a/spring-webflow-samples/sellitem/.settings/org.eclipse.jdt.ui.prefs b/spring-webflow-samples/sellitem/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..350fecbc --- /dev/null +++ b/spring-webflow-samples/sellitem/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Sat Nov 05 22:15:26 EST 2005 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/spring-webflow-samples/sellitem/.settings/org.eclipse.jst.common.project.facet.core.prefs b/spring-webflow-samples/sellitem/.settings/org.eclipse.jst.common.project.facet.core.prefs new file mode 100755 index 00000000..1973f017 --- /dev/null +++ b/spring-webflow-samples/sellitem/.settings/org.eclipse.jst.common.project.facet.core.prefs @@ -0,0 +1,4 @@ +#Mon Nov 13 17:23:47 PST 2006 +classpath.helper/org.eclipse.jdt.launching.JRE_CONTAINER/owners=jst.java\:5.0 +classpath.helper/org.eclipse.jst.server.core.container\:\:org.eclipse.jst.server.tomcat.runtimeTarget\:\:Apache\ Tomcat\ v5.5/owners=jst.web\:2.4 +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/sellitem/.settings/org.eclipse.wst.common.component b/spring-webflow-samples/sellitem/.settings/org.eclipse.wst.common.component new file mode 100755 index 00000000..b6167d67 --- /dev/null +++ b/spring-webflow-samples/sellitem/.settings/org.eclipse.wst.common.component @@ -0,0 +1,66 @@ + + + + + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + + + + diff --git a/spring-webflow-samples/sellitem/.settings/org.eclipse.wst.common.project.facet.core.xml b/spring-webflow-samples/sellitem/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100755 index 00000000..b7687079 --- /dev/null +++ b/spring-webflow-samples/sellitem/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spring-webflow-samples/sellitem/.settings/org.eclipse.wst.validation.prefs b/spring-webflow-samples/sellitem/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 00000000..1c7d6317 --- /dev/null +++ b/spring-webflow-samples/sellitem/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,6 @@ +#Mon Nov 13 17:29:33 PST 2006 +DELEGATES_PREFERENCE=delegateValidatorListorg.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator\=org.eclipse.wst.wsdl.validation.internal.eclipse.Validator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator\=org.eclipse.wst.xsd.core.internal.validation.eclipse.Validator; +USER_BUILD_PREFERENCE=enabledBuildValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_MANUAL_PREFERENCE=enabledManualValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_PREFERENCE=overrideGlobalPreferencesfalse +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/sellitem/.springBeans b/spring-webflow-samples/sellitem/.springBeans new file mode 100644 index 00000000..39bb7234 --- /dev/null +++ b/spring-webflow-samples/sellitem/.springBeans @@ -0,0 +1,41 @@ + + + + xml + + + src/main/java/org/springframework/webflow/samples/sellitem/services-config.xml + src/main/webapp/WEB-INF/sellitem-servlet-config.xml + src/main/webapp/WEB-INF/flows/sellitem-beans.xml + src/main/webapp/WEB-INF/sellitem-webflow-config.xml + + + + serviceLayer + false + false + + src/main/java/org/springframework/webflow/samples/sellitem/services-config.xml + + + + sellitemFlow + false + false + + src/main/webapp/WEB-INF/flows/sellitem-beans.xml + + + + webapp + false + false + + src/main/java/org/springframework/webflow/samples/sellitem/services-config.xml + src/main/webapp/WEB-INF/flows/sellitem-beans.xml + src/main/webapp/WEB-INF/sellitem-servlet-config.xml + src/main/webapp/WEB-INF/sellitem-webflow-config.xml + + + + diff --git a/spring-webflow-samples/sellitem/build.xml b/spring-webflow-samples/sellitem/build.xml new file mode 100644 index 00000000..10106af0 --- /dev/null +++ b/spring-webflow-samples/sellitem/build.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/ivy.xml b/spring-webflow-samples/sellitem/ivy.xml new file mode 100644 index 00000000..fb35018e --- /dev/null +++ b/spring-webflow-samples/sellitem/ivy.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/project.properties b/spring-webflow-samples/sellitem/project.properties new file mode 100644 index 00000000..f80ab3a7 --- /dev/null +++ b/spring-webflow-samples/sellitem/project.properties @@ -0,0 +1,10 @@ +# properties defined in this file are overridable by a local build.properties in this project dir + +# The location of the common build system +common.build.dir=${basedir}/../../common-build + +javac.source=1.5 +javac.target=1.5 + +# Do not publish built artifacts to the integration repository +do.publish=false \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/etc/filter.properties b/spring-webflow-samples/sellitem/src/etc/filter.properties new file mode 100644 index 00000000..c54e5880 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/etc/filter.properties @@ -0,0 +1,33 @@ +# $Header$ + +# Contains filterable project settings. Setting placeholders in filterable project text +# files will be replaced with these values when the 'statics' build target is run. +# +# You may add static settings directly to this source file in the format: +# setting=value e.g MY_SETTING=myvalue +# This is appropriate usage if you know the setting value will never change. +# +# At build time this source file is copied to the ${target.dir} where additional +# dynamic settings may be appended using the task. Use this approach +# when a setting value depends on the build or the local user's environment. +# +# An example of this approach is shown below: +# +# build.xml +# +# +# +# +# +# +# +# +# This allows for dynamic replacement values that are sourced from local properties files to facilitate +# local user settings. +# +# To refer to filterable settings within project source files like config files, JSPs, or +# other text files use the standard ant placeholder format: +# @SETTING_NAME@ e.g, @MY_SETTING@ and @MY_LOCAL_SETTING@ +# +# Your settings: diff --git a/spring-webflow-samples/sellitem/src/etc/test-resources/log4j.properties b/spring-webflow-samples/sellitem/src/etc/test-resources/log4j.properties new file mode 100644 index 00000000..59974077 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/etc/test-resources/log4j.properties @@ -0,0 +1,20 @@ +log4j.rootCategory=WARN, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=${@PROJECT_WEBAPP_NAME@.root}/@PROJECT_WEBAPP_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout + +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n + +#Enable webflow debug logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG diff --git a/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/InMemoryDatabaseCreator.java b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/InMemoryDatabaseCreator.java new file mode 100644 index 00000000..93c25caf --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/InMemoryDatabaseCreator.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import org.springframework.jdbc.core.support.JdbcDaoSupport; + +public class InMemoryDatabaseCreator extends JdbcDaoSupport { + + @Override + protected void initDao() throws Exception { + String createSales = "create table T_SALES (ID int not null identity primary key, ITEM_COUNT int not null, PRICE double NOT NULL, category VARCHAR(1) NOT NULL, SHIPPING_TYPE varchar(1))"; + getJdbcTemplate().execute(createSales); + } + +} diff --git a/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/JdbcSaleProcessor.java b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/JdbcSaleProcessor.java new file mode 100644 index 00000000..8d13c716 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/JdbcSaleProcessor.java @@ -0,0 +1,25 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import org.springframework.jdbc.core.support.JdbcDaoSupport; + +public class JdbcSaleProcessor extends JdbcDaoSupport implements SaleProcessor { + public void process(Sale sale) { + getJdbcTemplate().update("insert into T_SALES values (?, ?, ?, ?, ?)", + new Object[] { null, sale.getPrice(), sale.getItemCount(), sale.getCategory(), sale.getShippingType() }); + } +} diff --git a/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/Sale.java b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/Sale.java new file mode 100644 index 00000000..0ffe8489 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/Sale.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import java.io.Serializable; +import java.util.Date; + +import org.springframework.core.style.ToStringCreator; + +public class Sale implements Serializable { + + private double price; + + private int itemCount; + + private String category; + + private boolean shipping; + + private String shippingType; + + private Date shipDate; + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public int getItemCount() { + return itemCount; + } + + public void setItemCount(int itemCount) { + this.itemCount = itemCount; + } + + public double getPrice() { + return price; + } + + public void setPrice(double price) { + this.price = price; + } + + public boolean isShipping() { + return shipping; + } + + public void setShipping(boolean shipping) { + this.shipping = shipping; + } + + public String getShippingType() { + return shippingType; + } + + public void setShippingType(String shippingType) { + this.shippingType = shippingType; + } + + public Date getShipDate() { + return shipDate; + } + + public void setShipDate(Date shipDate) { + this.shipDate = shipDate; + } + + // business logic methods + + /** + * Returns the base amount of the sale, without discount or delivery costs. + */ + public double getAmount() { + return price * itemCount; + } + + /** + * Returns the discount rate to apply. + */ + public double getDiscountRate() { + double discount = 0.02; + if ("A".equals(category)) { + if (itemCount >= 100) { + discount = 0.1; + } + } + else if ("B".equals(category)) { + if (itemCount >= 200) { + discount = 0.2; + } + } + return discount; + } + + /** + * Returns the savings because of the discount. + */ + public double getSavings() { + return getDiscountRate() * getAmount(); + } + + /** + * Returns the delivery cost. + */ + public double getDeliveryCost() { + double delCost = 0.0; + if ("S".equals(shippingType)) { + delCost = 10.0; + } + else if ("E".equals(shippingType)) { + delCost = 20.0; + } + return delCost; + } + + /** + * Returns the total cost of the sale, including discount and delivery cost. + */ + public double getTotalCost() { + return getAmount() + getDeliveryCost() - getSavings(); + } + + public String toString() { + return new ToStringCreator(this).append("price", price).append("itemCount", itemCount).append("shippingType", + shippingType).append("shipDate", shipDate).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/SaleProcessor.java b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/SaleProcessor.java new file mode 100644 index 00000000..16d95634 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/SaleProcessor.java @@ -0,0 +1,24 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import org.springframework.transaction.annotation.Transactional; + +@Transactional +public interface SaleProcessor { + + public void process(Sale sale); +} diff --git a/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/SaleValidator.java b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/SaleValidator.java new file mode 100644 index 00000000..8850395a --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/SaleValidator.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +public class SaleValidator implements Validator { + + public boolean supports(Class clazz) { + return Sale.class.equals(clazz); + } + + public void validate(Object obj, Errors errors) { + Sale sale = (Sale)obj; + validatePriceAndItemCount(sale, errors); + } + + public void validatePriceAndItemCount(Sale sale, Errors errors) { + if (sale.getItemCount() <= 0) { + errors.rejectValue("itemCount", "tooLittle", "Item count must be greater than 0"); + } + if (sale.getPrice() <= 0.0) { + errors.rejectValue("price", "tooLittle", "Price must be greater than 0.0"); + } + } +} \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/SellItemFlowExecutionListener.java b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/SellItemFlowExecutionListener.java new file mode 100644 index 00000000..3b9a175b --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/SellItemFlowExecutionListener.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.util.StringUtils; +import org.springframework.webflow.context.servlet.ServletExternalContext; +import org.springframework.webflow.definition.StateDefinition; +import org.springframework.webflow.execution.EnterStateVetoException; +import org.springframework.webflow.execution.FlowExecutionListenerAdapter; +import org.springframework.webflow.execution.RequestContext; + +public class SellItemFlowExecutionListener extends FlowExecutionListenerAdapter { + + public void stateEntering(RequestContext context, StateDefinition nextState) throws EnterStateVetoException { + String role = nextState.getAttributes().getString("role"); + if (StringUtils.hasText(role)) { + HttpServletRequest request = ((ServletExternalContext)context.getExternalContext()).getRequest(); + if (!request.isUserInRole(role)) { + throw new EnterStateVetoException(context.getActiveFlow().getId(), context.getCurrentState().getId(), + nextState.getId(), "State requires role '" + role + + "', but the authenticated user doesn't have it!"); + } + } + } +} diff --git a/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/SellItemPropertyEditorRegistrar.java b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/SellItemPropertyEditorRegistrar.java new file mode 100644 index 00000000..2256db4f --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/SellItemPropertyEditorRegistrar.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.propertyeditors.CustomDateEditor; + +public class SellItemPropertyEditorRegistrar implements PropertyEditorRegistrar { + + public void registerCustomEditors(PropertyEditorRegistry registry) { + registry.registerCustomEditor(Date.class, new CustomDateEditor(new SimpleDateFormat("MM/dd/yyyy"), true)); + } +} diff --git a/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/services-config.xml b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/services-config.xml new file mode 100644 index 00000000..a01dab44 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/java/org/springframework/webflow/samples/sellitem/services-config.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/classes/log4j.properties b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/classes/log4j.properties new file mode 100644 index 00000000..5ba9222c --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/classes/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable web flow logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/conversation-scope/sellitem-beans.xml b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/conversation-scope/sellitem-beans.xml new file mode 100644 index 00000000..66a4c1cc --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/conversation-scope/sellitem-beans.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/conversation-scope/sellitem-conversation-scope-flow.xml b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/conversation-scope/sellitem-conversation-scope-flow.xml new file mode 100644 index 00000000..b6850a3f --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/conversation-scope/sellitem-conversation-scope-flow.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/conversation-scope/shipping-conversation-scope-flow.xml b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/conversation-scope/shipping-conversation-scope-flow.xml new file mode 100644 index 00000000..2f12345f --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/conversation-scope/shipping-conversation-scope-flow.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/sellitem-beans.xml b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/sellitem-beans.xml new file mode 100644 index 00000000..2b828373 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/sellitem-beans.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/sellitem-flow.xml b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/sellitem-flow.xml new file mode 100644 index 00000000..a49fb3d2 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/sellitem-flow.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/shipping-flow.xml b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/shipping-flow.xml new file mode 100644 index 00000000..9bf52e40 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/shipping-flow.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/simple/sellitem-simple-flow.xml b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/simple/sellitem-simple-flow.xml new file mode 100644 index 00000000..8c06eaa1 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/flows/simple/sellitem-simple-flow.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/categoryForm.jsp b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/categoryForm.jsp new file mode 100644 index 00000000..6d132f90 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/categoryForm.jsp @@ -0,0 +1,48 @@ +<%@ include file="includeTop.jsp" %> + +
+
+

Select category

+ + + + + + + + + + + + + + + + + + + + +
Price:${sale.price}
Item count:${sale.itemCount}
Category: + + + +
Is shipping required?: + +
+ + +
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/costOverview.jsp b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/costOverview.jsp new file mode 100644 index 00000000..8dd43e98 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/costOverview.jsp @@ -0,0 +1,71 @@ +<%@ include file="includeTop.jsp" %> + +
+
+

Purchase cost overview

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Price:${sale.price}
Item count:${sale.itemCount}
Category:${sale.category}
Shipping Info: + + + + + + + + + + + +
Type:${sale.shippingType}
Date: + + ${status.value} + +
+
+ + No shipping required: you're picking up the items + +
+
Base amount:${sale.amount}
Delivery cost:${sale.deliveryCost}
Discount:${sale.savings} (Discount rate: ${sale.discountRate})

Total cost:${sale.totalCost}
+
"> + +
+
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/error.jsp b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/error.jsp new file mode 100644 index 00000000..65d8ab8c --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/error.jsp @@ -0,0 +1,17 @@ +<%@ include file="includeTop.jsp" %> + +
+
+ +
+

+ + Duplicate submit of the same transaction not allowed! + +

+

+ Sell a new item +

+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/includeBottom.jsp b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/includeBottom.jsp new file mode 100644 index 00000000..3b0f1dd6 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/includeBottom.jsp @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/includeTop.jsp b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/includeTop.jsp new file mode 100644 index 00000000..416cad5f --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/includeTop.jsp @@ -0,0 +1,22 @@ +<%@ page contentType="text/html" %> +<%@ page session="false" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> + + + +Sell an item + + + + + + + + diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/priceAndItemCountForm.jsp b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/priceAndItemCountForm.jsp new file mode 100644 index 00000000..ab220892 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/priceAndItemCountForm.jsp @@ -0,0 +1,27 @@ +<%@ include file="includeTop.jsp" %> + +
+
+

Enter price and item count

+
+ + + + + + + + + + + + + + +
Price:
Item count:
+ + +
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/shippingDetailsForm.jsp b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/shippingDetailsForm.jsp new file mode 100644 index 00000000..e6c9a61c --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/jsp/shippingDetailsForm.jsp @@ -0,0 +1,51 @@ +<%@ include file="includeTop.jsp" %> + +
+
+

Enter shipping information

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
Price:${sale.price}
Item count:${sale.itemCount}
Category:${sale.category}
Shipping:${sale.shipping}
Shipping type: + + + +
Ship date (DD/MM/YYYY): + +
+ + +
+
+ +<%@ include file="includeBottom.jsp" %> \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/sellitem-servlet-config.xml b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/sellitem-servlet-config.xml new file mode 100644 index 00000000..bf8369dc --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/sellitem-servlet-config.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/sellitem-webflow-config.xml b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/sellitem-webflow-config.xml new file mode 100644 index 00000000..df11ecea --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/sellitem-webflow-config.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/web.xml b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..7cba7ad3 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,54 @@ + + + + + + webAppRootKey + swf-sellitem.root + + + + contextConfigLocation + + classpath:org/springframework/webflow/samples/sellitem/services-config.xml + + + + + log4jConfigLocation + /WEB-INF/classes/log4j.properties + + + + org.springframework.web.context.ContextLoaderListener + + + + org.springframework.web.util.Log4jConfigListener + + + + sellitem + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + + /WEB-INF/sellitem-servlet-config.xml + /WEB-INF/sellitem-webflow-config.xml + + + + + + sellitem + *.htm + + + + index.jsp + + + \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/main/webapp/images/spring-logo.jpg b/spring-webflow-samples/sellitem/src/main/webapp/images/spring-logo.jpg new file mode 100644 index 00000000..62be3983 Binary files /dev/null and b/spring-webflow-samples/sellitem/src/main/webapp/images/spring-logo.jpg differ diff --git a/spring-webflow-samples/sellitem/src/main/webapp/images/webflow-logo.jpg b/spring-webflow-samples/sellitem/src/main/webapp/images/webflow-logo.jpg new file mode 100644 index 00000000..ed76bae0 Binary files /dev/null and b/spring-webflow-samples/sellitem/src/main/webapp/images/webflow-logo.jpg differ diff --git a/spring-webflow-samples/sellitem/src/main/webapp/index.jsp b/spring-webflow-samples/sellitem/src/main/webapp/index.jsp new file mode 100644 index 00000000..45801400 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/index.jsp @@ -0,0 +1,58 @@ +<%@ page session="true" %> <%-- make sure we have a session --%> + + +
Sell Item - A Spring Web Flow Sample
+ +
+ +
+

+ Sell Item +

+ +

+ This Spring Web Flow sample application implements the example application + discussed in the article + + Use continuations to develop complex Web applications. It illustrates + the following concepts: +

    +
  • + Using the "_flowId" request parameter to let the view tell the web + flow controller which flow needs to be started. +
  • +
  • + Implementing a wizard using web flows. +
  • +
  • + Use of the FormAction to perform form processing, including the + FormAction's "setupForm" method to install custom property editors for + formatting text field values (shipDate). +
  • +
  • + Using continuations to make the flow completely stable, no matter + how browser navigation buttons are used. +
  • +
  • + Using "conversation invalidation after completion" to prevent duplicate submits + of the same sale while taking advantage of continuations to allow back button + usage while the application transaction is in process. +
  • +
  • + "Always redirect on pause" to benefit from the POST+REDIRECT+GET pattern with no special coding. +
  • +
  • + Using OGNL based conditional expressions. +
  • +
  • + Use of subflows to compose a multi-step business process from independently reusable modules. +
  • +
+

+
+ +
+ +
+ + diff --git a/spring-webflow-samples/sellitem/src/main/webapp/style.css b/spring-webflow-samples/sellitem/src/main/webapp/style.css new file mode 100644 index 00000000..f4b0a64e --- /dev/null +++ b/spring-webflow-samples/sellitem/src/main/webapp/style.css @@ -0,0 +1,58 @@ +body { + width: 720px; + margin: 0px; + padding: 0px; +} + +div#logo { + width: 720px; + height: 73px; + background: #86AEA5; +} + +div#navigation { + width: 720px; + height: 15px; + background: #E2F3B8; + text-align: right; +} + +div#content { + width: 720px; + padding: 5px; +} + +div#insert { + width: 120; + float: right; + text-align: right; +} + +.buttonBar { + height: 1.5em; + text-align: right; +} + +div#copyright { + width: 720px; +} + +div#copyright p { + text-align: center; + font-family: Tahoma, sans-serif; + font-size: 75%; + color: div#336633; + margin-left: 5px; + font-weight: bold; + clear: both; +} + +.readOnly { + color: rgb(192, 192, 192); +} + +.error { + color: red; + font-weight: bold; + font-family: Arial, sans-serif; +} \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/test/java/log4j.properties b/spring-webflow-samples/sellitem/src/test/java/log4j.properties new file mode 100644 index 00000000..5ba9222c --- /dev/null +++ b/spring-webflow-samples/sellitem/src/test/java/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable web flow logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/test/java/org/springframework/webflow/samples/sellitem/SaleProcessorIntegrationTests.java b/spring-webflow-samples/sellitem/src/test/java/org/springframework/webflow/samples/sellitem/SaleProcessorIntegrationTests.java new file mode 100644 index 00000000..e62338c8 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/test/java/org/springframework/webflow/samples/sellitem/SaleProcessorIntegrationTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import org.springframework.test.AbstractTransactionalDataSourceSpringContextTests; + +public class SaleProcessorIntegrationTests extends AbstractTransactionalDataSourceSpringContextTests { + + private SaleProcessor saleProcessor; + + public void setSaleProcessor(SaleProcessor saleProcessor) { + this.saleProcessor = saleProcessor; + } + + @Override + protected String[] getConfigLocations() { + return new String[] { "classpath:org/springframework/webflow/samples/sellitem/services-config.xml" }; + } + + public void testProcessSale() { + int beforeCount = jdbcTemplate.queryForInt("select count(*) from T_SALES"); + Sale sale = new Sale(); + sale.setItemCount(25); + sale.setPrice(100.0); + sale.setCategory("A"); + sale.setShippingType("Express"); + saleProcessor.process(sale); + int afterCount = jdbcTemplate.queryForInt("select count(*) from T_SALES"); + assertEquals("Wrong after count", beforeCount + 1, afterCount); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/sellitem/src/test/java/org/springframework/webflow/samples/sellitem/SellItemFlowExecutionTests.java b/spring-webflow-samples/sellitem/src/test/java/org/springframework/webflow/samples/sellitem/SellItemFlowExecutionTests.java new file mode 100644 index 00000000..c8fb10a2 --- /dev/null +++ b/spring-webflow-samples/sellitem/src/test/java/org/springframework/webflow/samples/sellitem/SellItemFlowExecutionTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.sellitem; + +import org.easymock.EasyMock; +import org.springframework.webflow.definition.registry.FlowDefinitionResource; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.test.MockFlowServiceLocator; +import org.springframework.webflow.test.MockParameterMap; +import org.springframework.webflow.test.execution.AbstractXmlFlowExecutionTests; + +public class SellItemFlowExecutionTests extends AbstractXmlFlowExecutionTests { + + private String flowDir = "src/main/webapp/WEB-INF/flows"; + + private SaleProcessor saleProcessor; + + @Override + protected FlowDefinitionResource getFlowDefinitionResource() { + return createFlowDefinitionResource(flowDir, "sellitem-flow.xml"); + } + + public void testStartFlow() { + ApplicationView selectedView = applicationView(startFlow()); + assertModelAttributeNotNull("sale", selectedView); + assertViewNameEquals("priceAndItemCountForm", selectedView); + } + + public void testSubmitPriceAndItemCount() { + testStartFlow(); + MockParameterMap parameters = new MockParameterMap(); + parameters.put("itemCount", "4"); + parameters.put("price", "25"); + ApplicationView selectedView = applicationView(signalEvent("submit", parameters)); + assertViewNameEquals("categoryForm", selectedView); + } + + public void testSubmitCategoryForm() { + testSubmitPriceAndItemCount(); + MockParameterMap parameters = new MockParameterMap(); + parameters.put("category", "A"); + ApplicationView selectedView = applicationView(signalEvent("submit", parameters)); + assertViewNameEquals("costOverview", selectedView); + assertFlowExecutionEnded(); + } + + public void testSubmitCategoryFormWithShipping() { + testSubmitPriceAndItemCount(); + MockParameterMap parameters = new MockParameterMap(); + parameters.put("category", "A"); + parameters.put("shipping", "true"); + ApplicationView selectedView = applicationView(signalEvent("submit", parameters)); + assertViewNameEquals("shippingDetailsForm", selectedView); + } + + public void testSubmitShippingDetailsForm() { + testSubmitCategoryFormWithShipping(); + + saleProcessor.process((Sale)getRequiredFlowAttribute("sale", Sale.class)); + EasyMock.replay(saleProcessor); + + MockParameterMap parameters = new MockParameterMap(); + parameters.put("shippingType", "E"); + parameters.put("shipDate", "12/06/2006"); + ApplicationView selectedView = applicationView(signalEvent("submit", parameters)); + assertViewNameEquals("costOverview", selectedView); + assertFlowExecutionEnded(); + + EasyMock.verify(saleProcessor); + } + + @Override + protected void registerMockServices(MockFlowServiceLocator serviceRegistry) { + saleProcessor = EasyMock.createMock(SaleProcessor.class); + serviceRegistry.registerBean("saleProcessor", saleProcessor); + + // we'll use real shipping flow + FlowDefinitionResource shipping = createFlowDefinitionResource(flowDir, "shipping-flow.xml"); + serviceRegistry.registerSubflow(createFlow(shipping, serviceRegistry)); + } +} \ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/.classpath b/spring-webflow-samples/shippingrate/.classpath new file mode 100644 index 00000000..731cc472 --- /dev/null +++ b/spring-webflow-samples/shippingrate/.classpath @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/spring-webflow-samples/shippingrate/.cvsignore b/spring-webflow-samples/shippingrate/.cvsignore new file mode 100644 index 00000000..16b87eb5 --- /dev/null +++ b/spring-webflow-samples/shippingrate/.cvsignore @@ -0,0 +1,8 @@ +target +lib +bak +build.properties +*.log +*.jpx.local* +*.iws +*.tws diff --git a/spring-webflow-samples/shippingrate/.project b/spring-webflow-samples/shippingrate/.project new file mode 100644 index 00000000..54d7e3cb --- /dev/null +++ b/spring-webflow-samples/shippingrate/.project @@ -0,0 +1,36 @@ + + + swf-shippingrate + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.wst.common.project.facet.core.builder + + + + + org.eclipse.wst.validation.validationbuilder + + + + + org.springframework.ide.eclipse.core.springbuilder + + + + + + org.springframework.ide.eclipse.core.springnature + org.eclipse.wst.common.project.facet.core.nature + org.eclipse.jdt.core.javanature + org.eclipse.wst.common.modulecore.ModuleCoreNature + org.eclipse.jem.workbench.JavaEMFNature + + diff --git a/spring-webflow-samples/shippingrate/.settings/org.eclipse.jdt.core.prefs b/spring-webflow-samples/shippingrate/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..52db45d8 --- /dev/null +++ b/spring-webflow-samples/shippingrate/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,8 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.5 +org.eclipse.jdt.core.compiler.compliance=1.5 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.source=1.5 diff --git a/spring-webflow-samples/shippingrate/.settings/org.eclipse.jdt.ui.prefs b/spring-webflow-samples/shippingrate/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..31c851b5 --- /dev/null +++ b/spring-webflow-samples/shippingrate/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Mon Nov 13 17:29:33 PST 2006 +eclipse.preferences.version=1 +internal.default.compliance=default diff --git a/spring-webflow-samples/shippingrate/.settings/org.eclipse.jst.common.project.facet.core.prefs b/spring-webflow-samples/shippingrate/.settings/org.eclipse.jst.common.project.facet.core.prefs new file mode 100755 index 00000000..1973f017 --- /dev/null +++ b/spring-webflow-samples/shippingrate/.settings/org.eclipse.jst.common.project.facet.core.prefs @@ -0,0 +1,4 @@ +#Mon Nov 13 17:23:47 PST 2006 +classpath.helper/org.eclipse.jdt.launching.JRE_CONTAINER/owners=jst.java\:5.0 +classpath.helper/org.eclipse.jst.server.core.container\:\:org.eclipse.jst.server.tomcat.runtimeTarget\:\:Apache\ Tomcat\ v5.5/owners=jst.web\:2.4 +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/shippingrate/.settings/org.eclipse.wst.common.component b/spring-webflow-samples/shippingrate/.settings/org.eclipse.wst.common.component new file mode 100755 index 00000000..12bd5897 --- /dev/null +++ b/spring-webflow-samples/shippingrate/.settings/org.eclipse.wst.common.component @@ -0,0 +1,51 @@ + + + + + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + +uses + + + + + diff --git a/spring-webflow-samples/shippingrate/.settings/org.eclipse.wst.common.project.facet.core.xml b/spring-webflow-samples/shippingrate/.settings/org.eclipse.wst.common.project.facet.core.xml new file mode 100755 index 00000000..b7687079 --- /dev/null +++ b/spring-webflow-samples/shippingrate/.settings/org.eclipse.wst.common.project.facet.core.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/spring-webflow-samples/shippingrate/.settings/org.eclipse.wst.validation.prefs b/spring-webflow-samples/shippingrate/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 00000000..1c7d6317 --- /dev/null +++ b/spring-webflow-samples/shippingrate/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,6 @@ +#Mon Nov 13 17:29:33 PST 2006 +DELEGATES_PREFERENCE=delegateValidatorListorg.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator\=org.eclipse.wst.wsdl.validation.internal.eclipse.Validator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator\=org.eclipse.wst.xsd.core.internal.validation.eclipse.Validator; +USER_BUILD_PREFERENCE=enabledBuildValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_MANUAL_PREFERENCE=enabledManualValidatorListorg.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.jst.j2ee.internal.web.validation.UIWarValidator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator;org.eclipse.wst.common.componentcore.internal.ModuleCoreValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator; +USER_PREFERENCE=overrideGlobalPreferencesfalse +eclipse.preferences.version=1 diff --git a/spring-webflow-samples/shippingrate/.springBeans b/spring-webflow-samples/shippingrate/.springBeans new file mode 100644 index 00000000..86214c5d --- /dev/null +++ b/spring-webflow-samples/shippingrate/.springBeans @@ -0,0 +1,26 @@ + + + + src/main/webapp/WEB-INF/shippingrate-servlet.xml + src/main/java/org/springframework/webflow/samples/shippingrate/domain/services.xml + + + + services + false + false + + src/main/java/org/springframework/webflow/samples/shippingrate/domain/services.xml + + + + webapp + false + false + + src/main/java/org/springframework/webflow/samples/shippingrate/domain/services.xml + src/main/webapp/WEB-INF/shippingrate-servlet.xml + + + + diff --git a/spring-webflow-samples/shippingrate/build.xml b/spring-webflow-samples/shippingrate/build.xml new file mode 100644 index 00000000..b0c3bc6b --- /dev/null +++ b/spring-webflow-samples/shippingrate/build.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/ivy.xml b/spring-webflow-samples/shippingrate/ivy.xml new file mode 100644 index 00000000..29c753f4 --- /dev/null +++ b/spring-webflow-samples/shippingrate/ivy.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/project.properties b/spring-webflow-samples/shippingrate/project.properties new file mode 100644 index 00000000..7d989e52 --- /dev/null +++ b/spring-webflow-samples/shippingrate/project.properties @@ -0,0 +1,10 @@ +# properties defined in this file are overridable by a local build.properties in this project dir + +# The location of the common build system +common.build.dir=${basedir}/../../common-build + +javac.source=1.3 +javac.target=1.3 + +# Do not publish built artifacts to the integration repository +do.publish=false \ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/etc/filter.properties b/spring-webflow-samples/shippingrate/src/etc/filter.properties new file mode 100644 index 00000000..c54e5880 --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/etc/filter.properties @@ -0,0 +1,33 @@ +# $Header$ + +# Contains filterable project settings. Setting placeholders in filterable project text +# files will be replaced with these values when the 'statics' build target is run. +# +# You may add static settings directly to this source file in the format: +# setting=value e.g MY_SETTING=myvalue +# This is appropriate usage if you know the setting value will never change. +# +# At build time this source file is copied to the ${target.dir} where additional +# dynamic settings may be appended using the task. Use this approach +# when a setting value depends on the build or the local user's environment. +# +# An example of this approach is shown below: +# +# build.xml +# +# +# +# +# +# +# +# +# This allows for dynamic replacement values that are sourced from local properties files to facilitate +# local user settings. +# +# To refer to filterable settings within project source files like config files, JSPs, or +# other text files use the standard ant placeholder format: +# @SETTING_NAME@ e.g, @MY_SETTING@ and @MY_LOCAL_SETTING@ +# +# Your settings: diff --git a/spring-webflow-samples/shippingrate/src/etc/test-resources/log4j.properties b/spring-webflow-samples/shippingrate/src/etc/test-resources/log4j.properties new file mode 100644 index 00000000..59974077 --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/etc/test-resources/log4j.properties @@ -0,0 +1,20 @@ +log4j.rootCategory=WARN, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=${@PROJECT_WEBAPP_NAME@.root}/@PROJECT_WEBAPP_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout + +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n + +#Enable webflow debug logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG diff --git a/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/Rate.java b/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/Rate.java new file mode 100644 index 00000000..9782070f --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/Rate.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.shippingrate.domain; + +import java.io.Serializable; +import java.math.BigDecimal; + +public class Rate implements Serializable { + + private BigDecimal value; + + public Rate(BigDecimal value) { + this.value = value; + } + + public double getDoubleValue() { + return value.doubleValue(); + } + + public boolean equals(Object o) { + if (!(o instanceof Rate)) { + return false; + } + return value.equals(((Rate)o).value); + } + + public int hashCode() { + return value.hashCode(); + } + + public String toString() { + return value.toString(); + } +} diff --git a/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/RateCriteria.java b/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/RateCriteria.java new file mode 100644 index 00000000..6e4881e9 --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/RateCriteria.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.shippingrate.domain; + +import java.io.Serializable; + +public class RateCriteria implements Serializable { + + private boolean residential = true; + + private String senderZipCode; + + private String receiverZipCode; + + private String senderCountryCode; + + private String receiverCountryCode; + + private int packageType = -1; + + private double packageWeight; + + public int getPackageType() { + return packageType; + } + + public void setPackageType(int packageType) { + this.packageType = packageType; + } + + public double getPackageWeight() { + return packageWeight; + } + + public void setPackageWeight(double packageWeight) { + this.packageWeight = packageWeight; + } + + public String getReceiverCountryCode() { + return receiverCountryCode; + } + + public void setReceiverCountryCode(String receiverCountryCode) { + this.receiverCountryCode = receiverCountryCode; + } + + public String getReceiverZipCode() { + return receiverZipCode; + } + + public void setReceiverZipCode(String receiverZipCode) { + this.receiverZipCode = receiverZipCode; + } + + public boolean isResidential() { + return residential; + } + + public void setResidential(boolean residential) { + this.residential = residential; + } + + public String getSenderCountryCode() { + return senderCountryCode; + } + + public void setSenderCountryCode(String senderCountryCode) { + this.senderCountryCode = senderCountryCode; + } + + public String getSenderZipCode() { + return senderZipCode; + } + + public void setSenderZipCode(String senderZipCode) { + this.senderZipCode = senderZipCode; + } +} \ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/RateCriteriaValidator.java b/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/RateCriteriaValidator.java new file mode 100644 index 00000000..5b49c8f3 --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/RateCriteriaValidator.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.shippingrate.domain; + +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; +import org.springframework.validation.Validator; + +public class RateCriteriaValidator implements Validator { + + public boolean supports(Class clazz) { + return RateCriteria.class.isAssignableFrom(clazz); + } + + public void validate(Object obj, Errors errors) { + RateCriteria criteria = (RateCriteria)obj; + validateSender(criteria, errors); + validateReceiver(criteria, errors); + validatePackageDetails(criteria, errors); + } + + public void validateSender(RateCriteria query, Errors errors) { + if (!StringUtils.hasText(query.getSenderCountryCode()) || query.getSenderCountryCode().equals("null")) { + errors.rejectValue("senderCountryCode", "senderCountryCodeRequired", "Sender country code is required"); + } + if (!StringUtils.hasText(query.getSenderZipCode())) { + errors.rejectValue("senderZipCode", "senderZipCodeRequired", "Sender zip code is required"); + } + } + + public void validateReceiver(RateCriteria query, Errors errors) { + if (!StringUtils.hasText(query.getReceiverCountryCode()) || query.getReceiverCountryCode().equals("null")) { + errors.rejectValue("receiverCountryCode", "receiverCountryCodeRequired", + "Receiver country code is required"); + } + if (!StringUtils.hasText(query.getReceiverZipCode())) { + errors.rejectValue("receiverZipCode", "receiverZipCodeRequired", "Receiver zip code is required"); + } + } + + public void validatePackageDetails(RateCriteria query, Errors errors) { + if (query.getPackageType() < 0) { + errors.rejectValue("packageType", "packageTypeRequired", "Package type is required"); + } + if (query.getPackageWeight() <= 0) { + errors.rejectValue("packageWeight", "packageWeightRequired", "Package weight is required"); + } + } +} diff --git a/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/RateService.java b/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/RateService.java new file mode 100644 index 00000000..a349bbda --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/RateService.java @@ -0,0 +1,27 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.shippingrate.domain; + +import java.util.Map; + +public interface RateService { + + public Map getCountries(); + + public Map getPackageTypes(); + + public Rate getRate(RateCriteria criteria); +} \ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/StubRateService.java b/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/StubRateService.java new file mode 100644 index 00000000..a79da4cf --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/StubRateService.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.samples.shippingrate.domain; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; + +public class StubRateService implements RateService { + + public Map getCountries() { + Map countries = new HashMap(); + countries.put("US", "United States"); + countries.put("CA", "Canada"); + return countries; + } + + public Map getPackageTypes() { + Map packageTypes = new HashMap(); + packageTypes.put("1", "Letter Envelope"); + packageTypes.put("2", "Express Box"); + packageTypes.put("3", "Tube"); + return packageTypes; + } + + public Rate getRate(RateCriteria criteria) { + return new Rate(new BigDecimal("1.39")); + } +} diff --git a/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/services.xml b/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/services.xml new file mode 100644 index 00000000..5eb0b382 --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/java/org/springframework/webflow/samples/shippingrate/domain/services.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/classes/MessageResources.properties b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/classes/MessageResources.properties new file mode 100644 index 00000000..dac6b789 --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/classes/MessageResources.properties @@ -0,0 +1 @@ +typeMismatch.rateCriteria.packageWeight=Package weight must be a number diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/classes/log4j.properties b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/classes/log4j.properties new file mode 100644 index 00000000..5ba9222c --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/classes/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootCategory=WARN, stdout + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +# Enable web flow logging +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/flows/getRate-flow.xml b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/flows/getRate-flow.xml new file mode 100644 index 00000000..2789c329 --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/flows/getRate-flow.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/selectCustomer.jsp b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/selectCustomer.jsp new file mode 100644 index 00000000..b4116808 --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/selectCustomer.jsp @@ -0,0 +1,41 @@ +<%@ page contentType="text/html" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + +
+ + + + + + ${status.errorMessage} + + + +
+ Select your customer profile + +
+ + + + checked>
+
+ + + + checked>
+
+ + + +
+ +
+ + + +
\ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/selectPackageDetails.jsp b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/selectPackageDetails.jsp new file mode 100644 index 00000000..250b5f66 --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/selectPackageDetails.jsp @@ -0,0 +1,46 @@ +<%@ page contentType="text/html" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + +
+ + + +
+ Select package details + + + + + + ${status.errorMessage} + +
+
+ + + + + + ${status.errorMessage} + +
+
+ + + +
+ +
+ + + +
\ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/selectReceiver.jsp b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/selectReceiver.jsp new file mode 100644 index 00000000..197bf5db --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/selectReceiver.jsp @@ -0,0 +1,43 @@ +<%@ page contentType="text/html" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + +
+ + + +
+ Select receiver location + + + + + ${status.errorMessage} + +
+
+ + + + + ${status.errorMessage} + +
+
+ + +
+ + + +
+ +
\ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/selectSender.jsp b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/selectSender.jsp new file mode 100644 index 00000000..1afdc31b --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/selectSender.jsp @@ -0,0 +1,43 @@ +<%@ page contentType="text/html" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + +
+ + + +
+ Select sender location + + + + + ${status.errorMessage} + +
+
+ + + + + ${status.errorMessage} + +
+
+ + +
+ +
+ + + +
\ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/showRate.jsp b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/showRate.jsp new file mode 100644 index 00000000..d37a3fe5 --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/jsp/showRate.jsp @@ -0,0 +1,13 @@ +<%@ page contentType="text/html" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + +
+ +
+ Rate details +
+ +
\ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/shippingrate-servlet.xml b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/shippingrate-servlet.xml new file mode 100644 index 00000000..c32b92b8 --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/shippingrate-servlet.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + MessageResources + + + \ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/web.xml b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 00000000..5a379a2f --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,33 @@ + + + + + + contextConfigLocation + + classpath:org/springframework/webflow/samples/shippingrate/domain/services.xml + + + + + org.springframework.web.context.ContextLoaderListener + + + + shippingrate + org.springframework.web.servlet.DispatcherServlet + + + + shippingrate + *.htm + + + + index.jsp + + + \ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/images/spring-logo.jpg b/spring-webflow-samples/shippingrate/src/main/webapp/images/spring-logo.jpg new file mode 100644 index 00000000..62be3983 Binary files /dev/null and b/spring-webflow-samples/shippingrate/src/main/webapp/images/spring-logo.jpg differ diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/images/webflow-logo.jpg b/spring-webflow-samples/shippingrate/src/main/webapp/images/webflow-logo.jpg new file mode 100644 index 00000000..ed76bae0 Binary files /dev/null and b/spring-webflow-samples/shippingrate/src/main/webapp/images/webflow-logo.jpg differ diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/index.jsp b/spring-webflow-samples/shippingrate/src/main/webapp/index.jsp new file mode 100644 index 00000000..12eba4b3 --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/webapp/index.jsp @@ -0,0 +1,51 @@ + + + Shipping Rate - An Ajax-enabled Spring Web Flow Sample + + + + +
Shipping Rate - An Ajax-enabled Spring Web Flow Sample
+ +
+ +
+

+ This sample application demonstrates use of Spring Web Flow + in combination with Ajaxian techniques. Specfically, it illustrates a + wizard embedded in a zone of this page that makes Ajax calls to the server to + participate in a executing flow. To complete processing, this wizard takes + the details about a shipment and calls a service to get the shipping rate. +

+ The techniques demonstrated are: +

    +
  • + Using a JavaScript component to submit regular forms through an AJAX request, and inserting the HTML + received from the server into a DIV tag. +
  • +
  • + Using the "_flowId" request parameter to let the view tell the web + flow controller which flow needs to be started. +
  • +
  • + Implementing a wizard using Spring Web Flow. +
  • +
+

+

+ Note: this sample has been tested successfully on Internet Explorer 6 and Safari 2.0.3. There are currently known Javascript issues with use on Firefox 1.5. +

+
+ +
+ +
+ +
+ + + \ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/prototype.js b/spring-webflow-samples/shippingrate/src/main/webapp/prototype.js new file mode 100644 index 00000000..0e85338b --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/webapp/prototype.js @@ -0,0 +1,1781 @@ +/* Prototype JavaScript framework, version 1.4.0 + * (c) 2005 Sam Stephenson + * + * Prototype is freely distributable under the terms of an MIT-style license. + * For details, see the Prototype web site: http://prototype.conio.net/ + * +/*--------------------------------------------------------------------------*/ + +var Prototype = { + Version: '1.4.0', + ScriptFragment: '(?:)((\n|\r|.)*?)(?:<\/script>)', + + emptyFunction: function() {}, + K: function(x) {return x} +} + +var Class = { + create: function() { + return function() { + this.initialize.apply(this, arguments); + } + } +} + +var Abstract = new Object(); + +Object.extend = function(destination, source) { + for (property in source) { + destination[property] = source[property]; + } + return destination; +} + +Object.inspect = function(object) { + try { + if (object == undefined) return 'undefined'; + if (object == null) return 'null'; + return object.inspect ? object.inspect() : object.toString(); + } catch (e) { + if (e instanceof RangeError) return '...'; + throw e; + } +} + +Function.prototype.bind = function() { + var __method = this, args = $A(arguments), object = args.shift(); + return function() { + return __method.apply(object, args.concat($A(arguments))); + } +} + +Function.prototype.bindAsEventListener = function(object) { + var __method = this; + return function(event) { + return __method.call(object, event || window.event); + } +} + +Object.extend(Number.prototype, { + toColorPart: function() { + var digits = this.toString(16); + if (this < 16) return '0' + digits; + return digits; + }, + + succ: function() { + return this + 1; + }, + + times: function(iterator) { + $R(0, this, true).each(iterator); + return this; + } +}); + +var Try = { + these: function() { + var returnValue; + + for (var i = 0; i < arguments.length; i++) { + var lambda = arguments[i]; + try { + returnValue = lambda(); + break; + } catch (e) {} + } + + return returnValue; + } +} + +/*--------------------------------------------------------------------------*/ + +var PeriodicalExecuter = Class.create(); +PeriodicalExecuter.prototype = { + initialize: function(callback, frequency) { + this.callback = callback; + this.frequency = frequency; + this.currentlyExecuting = false; + + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + if (!this.currentlyExecuting) { + try { + this.currentlyExecuting = true; + this.callback(); + } finally { + this.currentlyExecuting = false; + } + } + } +} + +/*--------------------------------------------------------------------------*/ + +function $() { + var elements = new Array(); + + for (var i = 0; i < arguments.length; i++) { + var element = arguments[i]; + if (typeof element == 'string') + element = document.getElementById(element); + + if (arguments.length == 1) + return element; + + elements.push(element); + } + + return elements; +} +Object.extend(String.prototype, { + stripTags: function() { + return this.replace(/<\/?[^>]+>/gi, ''); + }, + + stripScripts: function() { + return this.replace(new RegExp(Prototype.ScriptFragment, 'img'), ''); + }, + + extractScripts: function() { + var matchAll = new RegExp(Prototype.ScriptFragment, 'img'); + var matchOne = new RegExp(Prototype.ScriptFragment, 'im'); + return (this.match(matchAll) || []).map(function(scriptTag) { + return (scriptTag.match(matchOne) || ['', ''])[1]; + }); + }, + + evalScripts: function() { + return this.extractScripts().map(eval); + }, + + escapeHTML: function() { + var div = document.createElement('div'); + var text = document.createTextNode(this); + div.appendChild(text); + return div.innerHTML; + }, + + unescapeHTML: function() { + var div = document.createElement('div'); + div.innerHTML = this.stripTags(); + return div.childNodes[0] ? div.childNodes[0].nodeValue : ''; + }, + + toQueryParams: function() { + var pairs = this.match(/^\??(.*)$/)[1].split('&'); + return pairs.inject({}, function(params, pairString) { + var pair = pairString.split('='); + params[pair[0]] = pair[1]; + return params; + }); + }, + + toArray: function() { + return this.split(''); + }, + + camelize: function() { + var oStringList = this.split('-'); + if (oStringList.length == 1) return oStringList[0]; + + var camelizedString = this.indexOf('-') == 0 + ? oStringList[0].charAt(0).toUpperCase() + oStringList[0].substring(1) + : oStringList[0]; + + for (var i = 1, len = oStringList.length; i < len; i++) { + var s = oStringList[i]; + camelizedString += s.charAt(0).toUpperCase() + s.substring(1); + } + + return camelizedString; + }, + + inspect: function() { + return "'" + this.replace('\\', '\\\\').replace("'", '\\\'') + "'"; + } +}); + +String.prototype.parseQuery = String.prototype.toQueryParams; + +var $break = new Object(); +var $continue = new Object(); + +var Enumerable = { + each: function(iterator) { + var index = 0; + try { + this._each(function(value) { + try { + iterator(value, index++); + } catch (e) { + if (e != $continue) throw e; + } + }); + } catch (e) { + if (e != $break) throw e; + } + }, + + all: function(iterator) { + var result = true; + this.each(function(value, index) { + result = result && !!(iterator || Prototype.K)(value, index); + if (!result) throw $break; + }); + return result; + }, + + any: function(iterator) { + var result = true; + this.each(function(value, index) { + if (result = !!(iterator || Prototype.K)(value, index)) + throw $break; + }); + return result; + }, + + collect: function(iterator) { + var results = []; + this.each(function(value, index) { + results.push(iterator(value, index)); + }); + return results; + }, + + detect: function (iterator) { + var result; + this.each(function(value, index) { + if (iterator(value, index)) { + result = value; + throw $break; + } + }); + return result; + }, + + findAll: function(iterator) { + var results = []; + this.each(function(value, index) { + if (iterator(value, index)) + results.push(value); + }); + return results; + }, + + grep: function(pattern, iterator) { + var results = []; + this.each(function(value, index) { + var stringValue = value.toString(); + if (stringValue.match(pattern)) + results.push((iterator || Prototype.K)(value, index)); + }) + return results; + }, + + include: function(object) { + var found = false; + this.each(function(value) { + if (value == object) { + found = true; + throw $break; + } + }); + return found; + }, + + inject: function(memo, iterator) { + this.each(function(value, index) { + memo = iterator(memo, value, index); + }); + return memo; + }, + + invoke: function(method) { + var args = $A(arguments).slice(1); + return this.collect(function(value) { + return value[method].apply(value, args); + }); + }, + + max: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (value >= (result || value)) + result = value; + }); + return result; + }, + + min: function(iterator) { + var result; + this.each(function(value, index) { + value = (iterator || Prototype.K)(value, index); + if (value <= (result || value)) + result = value; + }); + return result; + }, + + partition: function(iterator) { + var trues = [], falses = []; + this.each(function(value, index) { + ((iterator || Prototype.K)(value, index) ? + trues : falses).push(value); + }); + return [trues, falses]; + }, + + pluck: function(property) { + var results = []; + this.each(function(value, index) { + results.push(value[property]); + }); + return results; + }, + + reject: function(iterator) { + var results = []; + this.each(function(value, index) { + if (!iterator(value, index)) + results.push(value); + }); + return results; + }, + + sortBy: function(iterator) { + return this.collect(function(value, index) { + return {value: value, criteria: iterator(value, index)}; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }).pluck('value'); + }, + + toArray: function() { + return this.collect(Prototype.K); + }, + + zip: function() { + var iterator = Prototype.K, args = $A(arguments); + if (typeof args.last() == 'function') + iterator = args.pop(); + + var collections = [this].concat(args).map($A); + return this.map(function(value, index) { + iterator(value = collections.pluck(index)); + return value; + }); + }, + + inspect: function() { + return '#'; + } +} + +Object.extend(Enumerable, { + map: Enumerable.collect, + find: Enumerable.detect, + select: Enumerable.findAll, + member: Enumerable.include, + entries: Enumerable.toArray +}); +var $A = Array.from = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) { + return iterable.toArray(); + } else { + var results = []; + for (var i = 0; i < iterable.length; i++) + results.push(iterable[i]); + return results; + } +} + +Object.extend(Array.prototype, Enumerable); + +Array.prototype._reverse = Array.prototype.reverse; + +Object.extend(Array.prototype, { + _each: function(iterator) { + for (var i = 0; i < this.length; i++) + iterator(this[i]); + }, + + clear: function() { + this.length = 0; + return this; + }, + + first: function() { + return this[0]; + }, + + last: function() { + return this[this.length - 1]; + }, + + compact: function() { + return this.select(function(value) { + return value != undefined || value != null; + }); + }, + + flatten: function() { + return this.inject([], function(array, value) { + return array.concat(value.constructor == Array ? + value.flatten() : [value]); + }); + }, + + without: function() { + var values = $A(arguments); + return this.select(function(value) { + return !values.include(value); + }); + }, + + indexOf: function(object) { + for (var i = 0; i < this.length; i++) + if (this[i] == object) return i; + return -1; + }, + + reverse: function(inline) { + return (inline !== false ? this : this.toArray())._reverse(); + }, + + shift: function() { + var result = this[0]; + for (var i = 0; i < this.length - 1; i++) + this[i] = this[i + 1]; + this.length--; + return result; + }, + + inspect: function() { + return '[' + this.map(Object.inspect).join(', ') + ']'; + } +}); +var Hash = { + _each: function(iterator) { + for (key in this) { + var value = this[key]; + if (typeof value == 'function') continue; + + var pair = [key, value]; + pair.key = key; + pair.value = value; + iterator(pair); + } + }, + + keys: function() { + return this.pluck('key'); + }, + + values: function() { + return this.pluck('value'); + }, + + merge: function(hash) { + return $H(hash).inject($H(this), function(mergedHash, pair) { + mergedHash[pair.key] = pair.value; + return mergedHash; + }); + }, + + toQueryString: function() { + return this.map(function(pair) { + return pair.map(encodeURIComponent).join('='); + }).join('&'); + }, + + inspect: function() { + return '#'; + } +} + +function $H(object) { + var hash = Object.extend({}, object || {}); + Object.extend(hash, Enumerable); + Object.extend(hash, Hash); + return hash; +} +ObjectRange = Class.create(); +Object.extend(ObjectRange.prototype, Enumerable); +Object.extend(ObjectRange.prototype, { + initialize: function(start, end, exclusive) { + this.start = start; + this.end = end; + this.exclusive = exclusive; + }, + + _each: function(iterator) { + var value = this.start; + do { + iterator(value); + value = value.succ(); + } while (this.include(value)); + }, + + include: function(value) { + if (value < this.start) + return false; + if (this.exclusive) + return value < this.end; + return value <= this.end; + } +}); + +var $R = function(start, end, exclusive) { + return new ObjectRange(start, end, exclusive); +} + +var Ajax = { + getTransport: function() { + return Try.these( + function() {return new ActiveXObject('Msxml2.XMLHTTP')}, + function() {return new ActiveXObject('Microsoft.XMLHTTP')}, + function() {return new XMLHttpRequest()} + ) || false; + }, + + activeRequestCount: 0 +} + +Ajax.Responders = { + responders: [], + + _each: function(iterator) { + this.responders._each(iterator); + }, + + register: function(responderToAdd) { + if (!this.include(responderToAdd)) + this.responders.push(responderToAdd); + }, + + unregister: function(responderToRemove) { + this.responders = this.responders.without(responderToRemove); + }, + + dispatch: function(callback, request, transport, json) { + this.each(function(responder) { + if (responder[callback] && typeof responder[callback] == 'function') { + try { + responder[callback].apply(responder, [request, transport, json]); + } catch (e) {} + } + }); + } +}; + +Object.extend(Ajax.Responders, Enumerable); + +Ajax.Responders.register({ + onCreate: function() { + Ajax.activeRequestCount++; + }, + + onComplete: function() { + Ajax.activeRequestCount--; + } +}); + +Ajax.Base = function() {}; +Ajax.Base.prototype = { + setOptions: function(options) { + this.options = { + method: 'post', + asynchronous: true, + parameters: '' + } + Object.extend(this.options, options || {}); + }, + + responseIsSuccess: function() { + return this.transport.status == undefined + || this.transport.status == 0 + || (this.transport.status >= 200 && this.transport.status < 300); + }, + + responseIsFailure: function() { + return !this.responseIsSuccess(); + } +} + +Ajax.Request = Class.create(); +Ajax.Request.Events = + ['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete']; + +Ajax.Request.prototype = Object.extend(new Ajax.Base(), { + initialize: function(url, options) { + this.transport = Ajax.getTransport(); + this.setOptions(options); + this.request(url); + }, + + request: function(url) { + var parameters = this.options.parameters || ''; + if (parameters.length > 0) parameters += '&_='; + + try { + this.url = url; + if (this.options.method == 'get' && parameters.length > 0) + this.url += (this.url.match(/\?/) ? '&' : '?') + parameters; + + Ajax.Responders.dispatch('onCreate', this, this.transport); + + this.transport.open(this.options.method, this.url, + this.options.asynchronous); + + if (this.options.asynchronous) { + this.transport.onreadystatechange = this.onStateChange.bind(this); + setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10); + } + + this.setRequestHeaders(); + + var body = this.options.postBody ? this.options.postBody : parameters; + this.transport.send(this.options.method == 'post' ? body : null); + + } catch (e) { + this.dispatchException(e); + } + }, + + setRequestHeaders: function() { + var requestHeaders = + ['X-Requested-With', 'XMLHttpRequest', + 'X-Prototype-Version', Prototype.Version]; + + if (this.options.method == 'post') { + requestHeaders.push('Content-type', + 'application/x-www-form-urlencoded'); + + /* Force "Connection: close" for Mozilla browsers to work around + * a bug where XMLHttpReqeuest sends an incorrect Content-length + * header. See Mozilla Bugzilla #246651. + */ + if (this.transport.overrideMimeType) + requestHeaders.push('Connection', 'close'); + } + + if (this.options.requestHeaders) + requestHeaders.push.apply(requestHeaders, this.options.requestHeaders); + + for (var i = 0; i < requestHeaders.length; i += 2) + this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]); + }, + + onStateChange: function() { + var readyState = this.transport.readyState; + if (readyState != 1) + this.respondToReadyState(this.transport.readyState); + }, + + header: function(name) { + try { + return this.transport.getResponseHeader(name); + } catch (e) {} + }, + + evalJSON: function() { + try { + return eval(this.header('X-JSON')); + } catch (e) {} + }, + + evalResponse: function() { + try { + return eval(this.transport.responseText); + } catch (e) { + this.dispatchException(e); + } + }, + + respondToReadyState: function(readyState) { + var event = Ajax.Request.Events[readyState]; + var transport = this.transport, json = this.evalJSON(); + + if (event == 'Complete') { + try { + (this.options['on' + this.transport.status] + || this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')] + || Prototype.emptyFunction)(transport, json); + } catch (e) { + this.dispatchException(e); + } + + if ((this.header('Content-type') || '').match(/^text\/javascript/i)) + this.evalResponse(); + } + + try { + (this.options['on' + event] || Prototype.emptyFunction)(transport, json); + Ajax.Responders.dispatch('on' + event, this, transport, json); + } catch (e) { + this.dispatchException(e); + } + + /* Avoid memory leak in MSIE: clean up the oncomplete event handler */ + if (event == 'Complete') + this.transport.onreadystatechange = Prototype.emptyFunction; + }, + + dispatchException: function(exception) { + (this.options.onException || Prototype.emptyFunction)(this, exception); + Ajax.Responders.dispatch('onException', this, exception); + } +}); + +Ajax.Updater = Class.create(); + +Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), { + initialize: function(container, url, options) { + this.containers = { + success: container.success ? $(container.success) : $(container), + failure: container.failure ? $(container.failure) : + (container.success ? null : $(container)) + } + + this.transport = Ajax.getTransport(); + this.setOptions(options); + + var onComplete = this.options.onComplete || Prototype.emptyFunction; + this.options.onComplete = (function(transport, object) { + this.updateContent(); + onComplete(transport, object); + }).bind(this); + + this.request(url); + }, + + updateContent: function() { + var receiver = this.responseIsSuccess() ? + this.containers.success : this.containers.failure; + var response = this.transport.responseText; + + if (!this.options.evalScripts) + response = response.stripScripts(); + + if (receiver) { + if (this.options.insertion) { + new this.options.insertion(receiver, response); + } else { + Element.update(receiver, response); + } + } + + if (this.responseIsSuccess()) { + if (this.onComplete) + setTimeout(this.onComplete.bind(this), 10); + } + } +}); + +Ajax.PeriodicalUpdater = Class.create(); +Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), { + initialize: function(container, url, options) { + this.setOptions(options); + this.onComplete = this.options.onComplete; + + this.frequency = (this.options.frequency || 2); + this.decay = (this.options.decay || 1); + + this.updater = {}; + this.container = container; + this.url = url; + + this.start(); + }, + + start: function() { + this.options.onComplete = this.updateComplete.bind(this); + this.onTimerEvent(); + }, + + stop: function() { + this.updater.onComplete = undefined; + clearTimeout(this.timer); + (this.onComplete || Prototype.emptyFunction).apply(this, arguments); + }, + + updateComplete: function(request) { + if (this.options.decay) { + this.decay = (request.responseText == this.lastText ? + this.decay * this.options.decay : 1); + + this.lastText = request.responseText; + } + this.timer = setTimeout(this.onTimerEvent.bind(this), + this.decay * this.frequency * 1000); + }, + + onTimerEvent: function() { + this.updater = new Ajax.Updater(this.container, this.url, this.options); + } +}); +document.getElementsByClassName = function(className, parentElement) { + var children = ($(parentElement) || document.body).getElementsByTagName('*'); + return $A(children).inject([], function(elements, child) { + if (child.className.match(new RegExp("(^|\\s)" + className + "(\\s|$)"))) + elements.push(child); + return elements; + }); +} + +/*--------------------------------------------------------------------------*/ + +if (!window.Element) { + var Element = new Object(); +} + +Object.extend(Element, { + visible: function(element) { + return $(element).style.display != 'none'; + }, + + toggle: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + Element[Element.visible(element) ? 'hide' : 'show'](element); + } + }, + + hide: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = 'none'; + } + }, + + show: function() { + for (var i = 0; i < arguments.length; i++) { + var element = $(arguments[i]); + element.style.display = ''; + } + }, + + remove: function(element) { + element = $(element); + element.parentNode.removeChild(element); + }, + + update: function(element, html) { + $(element).innerHTML = html.stripScripts(); + setTimeout(function() {html.evalScripts()}, 10); + }, + + getHeight: function(element) { + element = $(element); + return element.offsetHeight; + }, + + classNames: function(element) { + return new Element.ClassNames(element); + }, + + hasClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).include(className); + }, + + addClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).add(className); + }, + + removeClassName: function(element, className) { + if (!(element = $(element))) return; + return Element.classNames(element).remove(className); + }, + + // removes whitespace-only text node children + cleanWhitespace: function(element) { + element = $(element); + for (var i = 0; i < element.childNodes.length; i++) { + var node = element.childNodes[i]; + if (node.nodeType == 3 && !/\S/.test(node.nodeValue)) + Element.remove(node); + } + }, + + empty: function(element) { + return $(element).innerHTML.match(/^\s*$/); + }, + + scrollTo: function(element) { + element = $(element); + var x = element.x ? element.x : element.offsetLeft, + y = element.y ? element.y : element.offsetTop; + window.scrollTo(x, y); + }, + + getStyle: function(element, style) { + element = $(element); + var value = element.style[style.camelize()]; + if (!value) { + if (document.defaultView && document.defaultView.getComputedStyle) { + var css = document.defaultView.getComputedStyle(element, null); + value = css ? css.getPropertyValue(style) : null; + } else if (element.currentStyle) { + value = element.currentStyle[style.camelize()]; + } + } + + if (window.opera && ['left', 'top', 'right', 'bottom'].include(style)) + if (Element.getStyle(element, 'position') == 'static') value = 'auto'; + + return value == 'auto' ? null : value; + }, + + setStyle: function(element, style) { + element = $(element); + for (name in style) + element.style[name.camelize()] = style[name]; + }, + + getDimensions: function(element) { + element = $(element); + if (Element.getStyle(element, 'display') != 'none') + return {width: element.offsetWidth, height: element.offsetHeight}; + + // All *Width and *Height properties give 0 on elements with display none, + // so enable the element temporarily + var els = element.style; + var originalVisibility = els.visibility; + var originalPosition = els.position; + els.visibility = 'hidden'; + els.position = 'absolute'; + els.display = ''; + var originalWidth = element.clientWidth; + var originalHeight = element.clientHeight; + els.display = 'none'; + els.position = originalPosition; + els.visibility = originalVisibility; + return {width: originalWidth, height: originalHeight}; + }, + + makePositioned: function(element) { + element = $(element); + var pos = Element.getStyle(element, 'position'); + if (pos == 'static' || !pos) { + element._madePositioned = true; + element.style.position = 'relative'; + // Opera returns the offset relative to the positioning context, when an + // element is position relative but top and left have not been defined + if (window.opera) { + element.style.top = 0; + element.style.left = 0; + } + } + }, + + undoPositioned: function(element) { + element = $(element); + if (element._madePositioned) { + element._madePositioned = undefined; + element.style.position = + element.style.top = + element.style.left = + element.style.bottom = + element.style.right = ''; + } + }, + + makeClipping: function(element) { + element = $(element); + if (element._overflow) return; + element._overflow = element.style.overflow; + if ((Element.getStyle(element, 'overflow') || 'visible') != 'hidden') + element.style.overflow = 'hidden'; + }, + + undoClipping: function(element) { + element = $(element); + if (element._overflow) return; + element.style.overflow = element._overflow; + element._overflow = undefined; + } +}); + +var Toggle = new Object(); +Toggle.display = Element.toggle; + +/*--------------------------------------------------------------------------*/ + +Abstract.Insertion = function(adjacency) { + this.adjacency = adjacency; +} + +Abstract.Insertion.prototype = { + initialize: function(element, content) { + this.element = $(element); + this.content = content.stripScripts(); + + if (this.adjacency && this.element.insertAdjacentHTML) { + try { + this.element.insertAdjacentHTML(this.adjacency, this.content); + } catch (e) { + if (this.element.tagName.toLowerCase() == 'tbody') { + this.insertContent(this.contentFromAnonymousTable()); + } else { + throw e; + } + } + } else { + this.range = this.element.ownerDocument.createRange(); + if (this.initializeRange) this.initializeRange(); + this.insertContent([this.range.createContextualFragment(this.content)]); + } + + setTimeout(function() {content.evalScripts()}, 10); + }, + + contentFromAnonymousTable: function() { + var div = document.createElement('div'); + div.innerHTML = '' + this.content + '
'; + return $A(div.childNodes[0].childNodes[0].childNodes); + } +} + +var Insertion = new Object(); + +Insertion.Before = Class.create(); +Insertion.Before.prototype = Object.extend(new Abstract.Insertion('beforeBegin'), { + initializeRange: function() { + this.range.setStartBefore(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, this.element); + }).bind(this)); + } +}); + +Insertion.Top = Class.create(); +Insertion.Top.prototype = Object.extend(new Abstract.Insertion('afterBegin'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(true); + }, + + insertContent: function(fragments) { + fragments.reverse(false).each((function(fragment) { + this.element.insertBefore(fragment, this.element.firstChild); + }).bind(this)); + } +}); + +Insertion.Bottom = Class.create(); +Insertion.Bottom.prototype = Object.extend(new Abstract.Insertion('beforeEnd'), { + initializeRange: function() { + this.range.selectNodeContents(this.element); + this.range.collapse(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.appendChild(fragment); + }).bind(this)); + } +}); + +Insertion.After = Class.create(); +Insertion.After.prototype = Object.extend(new Abstract.Insertion('afterEnd'), { + initializeRange: function() { + this.range.setStartAfter(this.element); + }, + + insertContent: function(fragments) { + fragments.each((function(fragment) { + this.element.parentNode.insertBefore(fragment, + this.element.nextSibling); + }).bind(this)); + } +}); + +/*--------------------------------------------------------------------------*/ + +Element.ClassNames = Class.create(); +Element.ClassNames.prototype = { + initialize: function(element) { + this.element = $(element); + }, + + _each: function(iterator) { + this.element.className.split(/\s+/).select(function(name) { + return name.length > 0; + })._each(iterator); + }, + + set: function(className) { + this.element.className = className; + }, + + add: function(classNameToAdd) { + if (this.include(classNameToAdd)) return; + this.set(this.toArray().concat(classNameToAdd).join(' ')); + }, + + remove: function(classNameToRemove) { + if (!this.include(classNameToRemove)) return; + this.set(this.select(function(className) { + return className != classNameToRemove; + }).join(' ')); + }, + + toString: function() { + return this.toArray().join(' '); + } +} + +Object.extend(Element.ClassNames.prototype, Enumerable); +var Field = { + clear: function() { + for (var i = 0; i < arguments.length; i++) + $(arguments[i]).value = ''; + }, + + focus: function(element) { + $(element).focus(); + }, + + present: function() { + for (var i = 0; i < arguments.length; i++) + if ($(arguments[i]).value == '') return false; + return true; + }, + + select: function(element) { + $(element).select(); + }, + + activate: function(element) { + element = $(element); + element.focus(); + if (element.select) + element.select(); + } +} + +/*--------------------------------------------------------------------------*/ + +var Form = { + serialize: function(form) { + var elements = Form.getElements($(form)); + var queryComponents = new Array(); + + for (var i = 0; i < elements.length; i++) { + var queryComponent = Form.Element.serialize(elements[i]); + if (queryComponent) + queryComponents.push(queryComponent); + } + + return queryComponents.join('&'); + }, + + getElements: function(form) { + form = $(form); + var elements = new Array(); + + for (tagName in Form.Element.Serializers) { + var tagElements = form.getElementsByTagName(tagName); + for (var j = 0; j < tagElements.length; j++) + elements.push(tagElements[j]); + } + return elements; + }, + + getInputs: function(form, typeName, name) { + form = $(form); + var inputs = form.getElementsByTagName('input'); + + if (!typeName && !name) + return inputs; + + var matchingInputs = new Array(); + for (var i = 0; i < inputs.length; i++) { + var input = inputs[i]; + if ((typeName && input.type != typeName) || + (name && input.name != name)) + continue; + matchingInputs.push(input); + } + + return matchingInputs; + }, + + disable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.blur(); + element.disabled = 'true'; + } + }, + + enable: function(form) { + var elements = Form.getElements(form); + for (var i = 0; i < elements.length; i++) { + var element = elements[i]; + element.disabled = ''; + } + }, + + findFirstElement: function(form) { + return Form.getElements(form).find(function(element) { + return element.type != 'hidden' && !element.disabled && + ['input', 'select', 'textarea'].include(element.tagName.toLowerCase()); + }); + }, + + focusFirstElement: function(form) { + Field.activate(Form.findFirstElement(form)); + }, + + reset: function(form) { + $(form).reset(); + } +} + +Form.Element = { + serialize: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) { + var key = encodeURIComponent(parameter[0]); + if (key.length == 0) return; + + if (parameter[1].constructor != Array) + parameter[1] = [parameter[1]]; + + return parameter[1].map(function(value) { + return key + '=' + encodeURIComponent(value); + }).join('&'); + } + }, + + getValue: function(element) { + element = $(element); + var method = element.tagName.toLowerCase(); + var parameter = Form.Element.Serializers[method](element); + + if (parameter) + return parameter[1]; + } +} + +Form.Element.Serializers = { + input: function(element) { + switch (element.type.toLowerCase()) { + case 'submit': + case 'hidden': + case 'password': + case 'text': + return Form.Element.Serializers.textarea(element); + case 'checkbox': + case 'radio': + return Form.Element.Serializers.inputSelector(element); + } + return false; + }, + + inputSelector: function(element) { + if (element.checked) + return [element.name, element.value]; + }, + + textarea: function(element) { + return [element.name, element.value]; + }, + + select: function(element) { + return Form.Element.Serializers[element.type == 'select-one' ? + 'selectOne' : 'selectMany'](element); + }, + + selectOne: function(element) { + var value = '', opt, index = element.selectedIndex; + if (index >= 0) { + opt = element.options[index]; + value = opt.value; + if (!value && !('value' in opt)) + value = opt.text; + } + return [element.name, value]; + }, + + selectMany: function(element) { + var value = new Array(); + for (var i = 0; i < element.length; i++) { + var opt = element.options[i]; + if (opt.selected) { + var optValue = opt.value; + if (!optValue && !('value' in opt)) + optValue = opt.text; + value.push(optValue); + } + } + return [element.name, value]; + } +} + +/*--------------------------------------------------------------------------*/ + +var $F = Form.Element.getValue; + +/*--------------------------------------------------------------------------*/ + +Abstract.TimedObserver = function() {} +Abstract.TimedObserver.prototype = { + initialize: function(element, frequency, callback) { + this.frequency = frequency; + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + this.registerCallback(); + }, + + registerCallback: function() { + setInterval(this.onTimerEvent.bind(this), this.frequency * 1000); + }, + + onTimerEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + } +} + +Form.Element.Observer = Class.create(); +Form.Element.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.Observer = Class.create(); +Form.Observer.prototype = Object.extend(new Abstract.TimedObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); + +/*--------------------------------------------------------------------------*/ + +Abstract.EventObserver = function() {} +Abstract.EventObserver.prototype = { + initialize: function(element, callback) { + this.element = $(element); + this.callback = callback; + + this.lastValue = this.getValue(); + if (this.element.tagName.toLowerCase() == 'form') + this.registerFormCallbacks(); + else + this.registerCallback(this.element); + }, + + onElementEvent: function() { + var value = this.getValue(); + if (this.lastValue != value) { + this.callback(this.element, value); + this.lastValue = value; + } + }, + + registerFormCallbacks: function() { + var elements = Form.getElements(this.element); + for (var i = 0; i < elements.length; i++) + this.registerCallback(elements[i]); + }, + + registerCallback: function(element) { + if (element.type) { + switch (element.type.toLowerCase()) { + case 'checkbox': + case 'radio': + Event.observe(element, 'click', this.onElementEvent.bind(this)); + break; + case 'password': + case 'text': + case 'textarea': + case 'select-one': + case 'select-multiple': + Event.observe(element, 'change', this.onElementEvent.bind(this)); + break; + } + } + } +} + +Form.Element.EventObserver = Class.create(); +Form.Element.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.Element.getValue(this.element); + } +}); + +Form.EventObserver = Class.create(); +Form.EventObserver.prototype = Object.extend(new Abstract.EventObserver(), { + getValue: function() { + return Form.serialize(this.element); + } +}); +if (!window.Event) { + var Event = new Object(); +} + +Object.extend(Event, { + KEY_BACKSPACE: 8, + KEY_TAB: 9, + KEY_RETURN: 13, + KEY_ESC: 27, + KEY_LEFT: 37, + KEY_UP: 38, + KEY_RIGHT: 39, + KEY_DOWN: 40, + KEY_DELETE: 46, + + element: function(event) { + return event.target || event.srcElement; + }, + + isLeftClick: function(event) { + return (((event.which) && (event.which == 1)) || + ((event.button) && (event.button == 1))); + }, + + pointerX: function(event) { + return event.pageX || (event.clientX + + (document.documentElement.scrollLeft || document.body.scrollLeft)); + }, + + pointerY: function(event) { + return event.pageY || (event.clientY + + (document.documentElement.scrollTop || document.body.scrollTop)); + }, + + stop: function(event) { + if (event.preventDefault) { + event.preventDefault(); + event.stopPropagation(); + } else { + event.returnValue = false; + event.cancelBubble = true; + } + }, + + // find the first node with the given tagName, starting from the + // node the event was triggered on; traverses the DOM upwards + findElement: function(event, tagName) { + var element = Event.element(event); + while (element.parentNode && (!element.tagName || + (element.tagName.toUpperCase() != tagName.toUpperCase()))) + element = element.parentNode; + return element; + }, + + observers: false, + + _observeAndCache: function(element, name, observer, useCapture) { + if (!this.observers) this.observers = []; + if (element.addEventListener) { + this.observers.push([element, name, observer, useCapture]); + element.addEventListener(name, observer, useCapture); + } else if (element.attachEvent) { + this.observers.push([element, name, observer, useCapture]); + element.attachEvent('on' + name, observer); + } + }, + + unloadCache: function() { + if (!Event.observers) return; + for (var i = 0; i < Event.observers.length; i++) { + Event.stopObserving.apply(this, Event.observers[i]); + Event.observers[i][0] = null; + } + Event.observers = false; + }, + + observe: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.attachEvent)) + name = 'keydown'; + + this._observeAndCache(element, name, observer, useCapture); + }, + + stopObserving: function(element, name, observer, useCapture) { + var element = $(element); + useCapture = useCapture || false; + + if (name == 'keypress' && + (navigator.appVersion.match(/Konqueror|Safari|KHTML/) + || element.detachEvent)) + name = 'keydown'; + + if (element.removeEventListener) { + element.removeEventListener(name, observer, useCapture); + } else if (element.detachEvent) { + element.detachEvent('on' + name, observer); + } + } +}); + +/* prevent memory leaks in IE */ +Event.observe(window, 'unload', Event.unloadCache, false); +var Position = { + // set to true if needed, warning: firefox performance problems + // NOT neeeded for page scrolling, only if draggable contained in + // scrollable elements + includeScrollOffsets: false, + + // must be called before calling withinIncludingScrolloffset, every time the + // page is scrolled + prepare: function() { + this.deltaX = window.pageXOffset + || document.documentElement.scrollLeft + || document.body.scrollLeft + || 0; + this.deltaY = window.pageYOffset + || document.documentElement.scrollTop + || document.body.scrollTop + || 0; + }, + + realOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.scrollTop || 0; + valueL += element.scrollLeft || 0; + element = element.parentNode; + } while (element); + return [valueL, valueT]; + }, + + cumulativeOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + } while (element); + return [valueL, valueT]; + }, + + positionedOffset: function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + element = element.offsetParent; + if (element) { + p = Element.getStyle(element, 'position'); + if (p == 'relative' || p == 'absolute') break; + } + } while (element); + return [valueL, valueT]; + }, + + offsetParent: function(element) { + if (element.offsetParent) return element.offsetParent; + if (element == document.body) return element; + + while ((element = element.parentNode) && element != document.body) + if (Element.getStyle(element, 'position') != 'static') + return element; + + return document.body; + }, + + // caches x/y coordinate pair to use with overlap + within: function(element, x, y) { + if (this.includeScrollOffsets) + return this.withinIncludingScrolloffsets(element, x, y); + this.xcomp = x; + this.ycomp = y; + this.offset = this.cumulativeOffset(element); + + return (y >= this.offset[1] && + y < this.offset[1] + element.offsetHeight && + x >= this.offset[0] && + x < this.offset[0] + element.offsetWidth); + }, + + withinIncludingScrolloffsets: function(element, x, y) { + var offsetcache = this.realOffset(element); + + this.xcomp = x + offsetcache[0] - this.deltaX; + this.ycomp = y + offsetcache[1] - this.deltaY; + this.offset = this.cumulativeOffset(element); + + return (this.ycomp >= this.offset[1] && + this.ycomp < this.offset[1] + element.offsetHeight && + this.xcomp >= this.offset[0] && + this.xcomp < this.offset[0] + element.offsetWidth); + }, + + // within must be called directly before + overlap: function(mode, element) { + if (!mode) return 0; + if (mode == 'vertical') + return ((this.offset[1] + element.offsetHeight) - this.ycomp) / + element.offsetHeight; + if (mode == 'horizontal') + return ((this.offset[0] + element.offsetWidth) - this.xcomp) / + element.offsetWidth; + }, + + clone: function(source, target) { + source = $(source); + target = $(target); + target.style.position = 'absolute'; + var offsets = this.cumulativeOffset(source); + target.style.top = offsets[1] + 'px'; + target.style.left = offsets[0] + 'px'; + target.style.width = source.offsetWidth + 'px'; + target.style.height = source.offsetHeight + 'px'; + }, + + page: function(forElement) { + var valueT = 0, valueL = 0; + + var element = forElement; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + + // Safari fix + if (element.offsetParent==document.body) + if (Element.getStyle(element,'position')=='absolute') break; + + } while (element = element.offsetParent); + + element = forElement; + do { + valueT -= element.scrollTop || 0; + valueL -= element.scrollLeft || 0; + } while (element = element.parentNode); + + return [valueL, valueT]; + }, + + clone: function(source, target) { + var options = Object.extend({ + setLeft: true, + setTop: true, + setWidth: true, + setHeight: true, + offsetTop: 0, + offsetLeft: 0 + }, arguments[2] || {}) + + // find page position of source + source = $(source); + var p = Position.page(source); + + // find coordinate system to use + target = $(target); + var delta = [0, 0]; + var parent = null; + // delta [0,0] will do fine with position: fixed elements, + // position:absolute needs offsetParent deltas + if (Element.getStyle(target,'position') == 'absolute') { + parent = Position.offsetParent(target); + delta = Position.page(parent); + } + + // correct by body offsets (fixes Safari) + if (parent == document.body) { + delta[0] -= document.body.offsetLeft; + delta[1] -= document.body.offsetTop; + } + + // set position + if(options.setLeft) target.style.left = (p[0] - delta[0] + options.offsetLeft) + 'px'; + if(options.setTop) target.style.top = (p[1] - delta[1] + options.offsetTop) + 'px'; + if(options.setWidth) target.style.width = source.offsetWidth + 'px'; + if(options.setHeight) target.style.height = source.offsetHeight + 'px'; + }, + + absolutize: function(element) { + element = $(element); + if (element.style.position == 'absolute') return; + Position.prepare(); + + var offsets = Position.positionedOffset(element); + var top = offsets[1]; + var left = offsets[0]; + var width = element.clientWidth; + var height = element.clientHeight; + + element._originalLeft = left - parseFloat(element.style.left || 0); + element._originalTop = top - parseFloat(element.style.top || 0); + element._originalWidth = element.style.width; + element._originalHeight = element.style.height; + + element.style.position = 'absolute'; + element.style.top = top + 'px';; + element.style.left = left + 'px';; + element.style.width = width + 'px';; + element.style.height = height + 'px';; + }, + + relativize: function(element) { + element = $(element); + if (element.style.position == 'relative') return; + Position.prepare(); + + element.style.position = 'relative'; + var top = parseFloat(element.style.top || 0) - (element._originalTop || 0); + var left = parseFloat(element.style.left || 0) - (element._originalLeft || 0); + + element.style.top = top + 'px'; + element.style.left = left + 'px'; + element.style.height = element._originalHeight; + element.style.width = element._originalWidth; + } +} + +// Safari returns margins on body which is incorrect if the child is absolutely +// positioned. For performance reasons, redefine Position.cumulativeOffset for +// KHTML/WebKit only. +if (/Konqueror|Safari|KHTML/.test(navigator.userAgent)) { + Position.cumulativeOffset = function(element) { + var valueT = 0, valueL = 0; + do { + valueT += element.offsetTop || 0; + valueL += element.offsetLeft || 0; + if (element.offsetParent == document.body) + if (Element.getStyle(element, 'position') == 'absolute') break; + + element = element.offsetParent; + } while (element); + + return [valueL, valueT]; + } +} \ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/style.css b/spring-webflow-samples/shippingrate/src/main/webapp/style.css new file mode 100644 index 00000000..fd4e8b5e --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/webapp/style.css @@ -0,0 +1,60 @@ +body { + width: 720px; + margin: 0px; + padding: 0px; + font-size: 10px; +} + +div#logo { + width: 720px; + height: 65px; + background: #86AEA5; +} + +div#navigation { + width: 720px; + height: 20px; + background: #E2F3B8; + text-align: right; +} + +div#content { + width: 720px; + padding: 5px; +} + +div#insert { + width: 120; + float: right; + text-align: right; +} + +.buttonBar { + height: 1.5em; + text-align: right; +} + +div#copyright { + width: 720px; +} + +div#copyright p { + text-align: center; + font-family: Tahoma, sans-serif; + font-size: 90%; + color: div#336633; + margin-left: 5px; + font-weight: bold; + clear: both; +} + +.readOnly { + color: rgb(192, 192, 192); +} + +.error { + color: red; + font-weight: bold; + font-family: Arial, sans-serif; + font-size: 90%; +} \ No newline at end of file diff --git a/spring-webflow-samples/shippingrate/src/main/webapp/swf_ajax.js b/spring-webflow-samples/shippingrate/src/main/webapp/swf_ajax.js new file mode 100644 index 00000000..136862ab --- /dev/null +++ b/spring-webflow-samples/shippingrate/src/main/webapp/swf_ajax.js @@ -0,0 +1,64 @@ + var SimpleRequest = function(targetElementId, url, method, parameters) { + var targetElement = $(targetElementId); + if (targetElement == null) { + throw 'Target element is null!'; + } + if (url == null) { + throw 'URL has to be provided'; + } + if (method == null) { + method = 'get'; + } else if (method != 'get' && method != 'post') { + throw 'Method should be get or post'; + } + var myAjax = new Ajax.Updater( + { success: targetElement }, + url, + { + method: method, + parameters: parameters, + onFailure: errFunc, + evalScripts: true + }); + }; + + function formRequest(formElementId) { + Event.observe(formElementId, 'submit', handleSubmitEvent, true); + } + + function handleSubmitEvent(event) { + var formElement = Event.element(event); + if (formElement.tagName.toLowerCase() != 'form') { + throw 'Element ' + formElement + ' is not a FORM element!'; + } + var method = formElement.method; + if (method == null) { + method = 'get'; + } + var url = formElement.action; + if (url == null) { + throw 'No action defined on ' + formElement; + } + try { + Event.stop(event); + var myRequest = new Ajax.Updater( + { success: formElement.parentNode }, + url, + { + method: method, + parameters: Form.serialize(formElement), + evalScripts: true, + onFailure: errFunc + }); + } finally { + return false; + } + } + + var handlerFunc = function(t) { + alert(t.responseText); + } + + var errFunc = function(t) { + alert('Error ' + t.status + ' -- ' + t.statusText); + } \ No newline at end of file diff --git a/spring-webflow/.classpath b/spring-webflow/.classpath new file mode 100644 index 00000000..19de29c7 --- /dev/null +++ b/spring-webflow/.classpath @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-webflow/.cvsignore b/spring-webflow/.cvsignore new file mode 100644 index 00000000..11b2db89 --- /dev/null +++ b/spring-webflow/.cvsignore @@ -0,0 +1,9 @@ +target +lib +bak +build.properties +*.log +*.jpx.local* +*.iws +*.tws +doc diff --git a/spring-webflow/.project b/spring-webflow/.project new file mode 100644 index 00000000..aec06846 --- /dev/null +++ b/spring-webflow/.project @@ -0,0 +1,21 @@ + + + spring-webflow + + + build-spring-webflow + common-build + repository + spring-binding + + + + org.eclipse.jdt.core.javabuilder + + + + + + org.eclipse.jdt.core.javanature + + diff --git a/spring-webflow/.settings/org.eclipse.jdt.core.prefs b/spring-webflow/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000..065770a6 --- /dev/null +++ b/spring-webflow/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,74 @@ +#Wed Oct 04 14:37:01 EDT 2006 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.3 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.3 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.doc.comment.support=enabled +org.eclipse.jdt.core.compiler.problem.annotationSuperInterface=warning +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.autoboxing=ignore +org.eclipse.jdt.core.compiler.problem.deprecation=warning +org.eclipse.jdt.core.compiler.problem.deprecationInDeprecatedCode=disabled +org.eclipse.jdt.core.compiler.problem.deprecationWhenOverridingDeprecatedMethod=disabled +org.eclipse.jdt.core.compiler.problem.discouragedReference=warning +org.eclipse.jdt.core.compiler.problem.emptyStatement=ignore +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.fallthroughCase=ignore +org.eclipse.jdt.core.compiler.problem.fieldHiding=ignore +org.eclipse.jdt.core.compiler.problem.finalParameterBound=warning +org.eclipse.jdt.core.compiler.problem.finallyBlockNotCompletingNormally=warning +org.eclipse.jdt.core.compiler.problem.forbiddenReference=error +org.eclipse.jdt.core.compiler.problem.hiddenCatchBlock=warning +org.eclipse.jdt.core.compiler.problem.incompatibleNonInheritedInterfaceMethod=warning +org.eclipse.jdt.core.compiler.problem.incompleteEnumSwitch=ignore +org.eclipse.jdt.core.compiler.problem.indirectStaticAccess=ignore +org.eclipse.jdt.core.compiler.problem.invalidJavadoc=warning +org.eclipse.jdt.core.compiler.problem.invalidJavadocTags=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsDeprecatedRef=disabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsNotVisibleRef=enabled +org.eclipse.jdt.core.compiler.problem.invalidJavadocTagsVisibility=protected +org.eclipse.jdt.core.compiler.problem.localVariableHiding=ignore +org.eclipse.jdt.core.compiler.problem.methodWithConstructorName=warning +org.eclipse.jdt.core.compiler.problem.missingDeprecatedAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingJavadocComments=ignore +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsOverriding=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocCommentsVisibility=public +org.eclipse.jdt.core.compiler.problem.missingJavadocTags=ignore +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsOverriding=disabled +org.eclipse.jdt.core.compiler.problem.missingJavadocTagsVisibility=public +org.eclipse.jdt.core.compiler.problem.missingOverrideAnnotation=ignore +org.eclipse.jdt.core.compiler.problem.missingSerialVersion=ignore +org.eclipse.jdt.core.compiler.problem.noEffectAssignment=warning +org.eclipse.jdt.core.compiler.problem.noImplicitStringConversion=warning +org.eclipse.jdt.core.compiler.problem.nonExternalizedStringLiteral=ignore +org.eclipse.jdt.core.compiler.problem.nullReference=ignore +org.eclipse.jdt.core.compiler.problem.overridingPackageDefaultMethod=warning +org.eclipse.jdt.core.compiler.problem.parameterAssignment=ignore +org.eclipse.jdt.core.compiler.problem.possibleAccidentalBooleanAssignment=ignore +org.eclipse.jdt.core.compiler.problem.rawTypeReference=ignore +org.eclipse.jdt.core.compiler.problem.specialParameterHidingField=disabled +org.eclipse.jdt.core.compiler.problem.staticAccessReceiver=warning +org.eclipse.jdt.core.compiler.problem.suppressWarnings=enabled +org.eclipse.jdt.core.compiler.problem.syntheticAccessEmulation=ignore +org.eclipse.jdt.core.compiler.problem.typeParameterHiding=warning +org.eclipse.jdt.core.compiler.problem.uncheckedTypeOperation=warning +org.eclipse.jdt.core.compiler.problem.undocumentedEmptyBlock=ignore +org.eclipse.jdt.core.compiler.problem.unhandledWarningToken=warning +org.eclipse.jdt.core.compiler.problem.unnecessaryElse=ignore +org.eclipse.jdt.core.compiler.problem.unnecessaryTypeCheck=warning +org.eclipse.jdt.core.compiler.problem.unqualifiedFieldAccess=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownException=ignore +org.eclipse.jdt.core.compiler.problem.unusedDeclaredThrownExceptionWhenOverriding=disabled +org.eclipse.jdt.core.compiler.problem.unusedImport=warning +org.eclipse.jdt.core.compiler.problem.unusedLabel=warning +org.eclipse.jdt.core.compiler.problem.unusedLocal=warning +org.eclipse.jdt.core.compiler.problem.unusedParameter=ignore +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenImplementingAbstract=disabled +org.eclipse.jdt.core.compiler.problem.unusedParameterWhenOverridingConcrete=disabled +org.eclipse.jdt.core.compiler.problem.unusedPrivateMember=warning +org.eclipse.jdt.core.compiler.problem.varargsArgumentNeedCast=warning +org.eclipse.jdt.core.compiler.source=1.3 diff --git a/spring-webflow/.settings/org.eclipse.jdt.ui.prefs b/spring-webflow/.settings/org.eclipse.jdt.ui.prefs new file mode 100644 index 00000000..52be497b --- /dev/null +++ b/spring-webflow/.settings/org.eclipse.jdt.ui.prefs @@ -0,0 +1,3 @@ +#Wed Oct 04 14:35:34 EDT 2006 +eclipse.preferences.version=1 +internal.default.compliance=user diff --git a/spring-webflow/.settings/org.eclipse.wst.validation.prefs b/spring-webflow/.settings/org.eclipse.wst.validation.prefs new file mode 100644 index 00000000..68bb37d5 --- /dev/null +++ b/spring-webflow/.settings/org.eclipse.wst.validation.prefs @@ -0,0 +1,6 @@ +#Fri May 05 18:13:37 EDT 2006 +DELEGATES_PREFERENCE=delegateValidatorListorg.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator\=org.eclipse.wst.wsdl.validation.internal.eclipse.Validator;org.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator\=org.eclipse.wst.xsd.core.internal.validation.eclipse.Validator; +USER_BUILD_PREFERENCE=enabledBuildValidatorListorg.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator; +USER_MANUAL_PREFERENCE=enabledManualValidatorListorg.eclipse.wst.xsd.core.internal.validation.eclipse.XSDDelegatingValidator;org.eclipse.wst.xml.core.internal.validation.eclipse.Validator;org.eclipse.jst.jsp.core.internal.validation.JSPELValidator;org.eclipse.wst.wsdl.validation.internal.eclipse.WSDLDelegatingValidator;org.eclipse.wst.html.internal.validation.HTMLValidator;org.eclipse.wst.wsi.ui.internal.WSIMessageValidator;org.eclipse.jst.jsp.core.internal.validation.JSPJavaValidator;org.eclipse.wst.dtd.core.internal.validation.eclipse.Validator;org.eclipse.jst.jsp.core.internal.validation.JSPDirectiveValidator; +USER_PREFERENCE=overrideGlobalPreferencesfalse +eclipse.preferences.version=1 diff --git a/spring-webflow/build.xml b/spring-webflow/build.xml new file mode 100644 index 00000000..1b98a038 --- /dev/null +++ b/spring-webflow/build.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/spring-webflow/changelog.txt b/spring-webflow/changelog.txt new file mode 100644 index 00000000..cf06bc15 --- /dev/null +++ b/spring-webflow/changelog.txt @@ -0,0 +1,860 @@ +SPRING WEB FLOW (SWF) CHANGELOG +=============================== +http://www.springframework.org + +Changes in version 1.0.1 () +------------------------------------- + +General +* In Ivy, JUnit is no longer a default dependency. There is now a new 'testing' configuration that + includes JUnit as a dependency (SWF-213). + +Package org.springframework.webflow.action +* FormAction methods getFormObject and getFormErrors are no longer final (SWF-222). +* When FormAction detects that the existing Errors instance is invalid, it will now copy over + existing field and object errors to the newly created Errors instance (SWF-219). + +Package org.springframework.webflow.config +* Added get/setMaxConversations and get/setMaxContinuations methods to FlowExecutorFactoryBean. +* Fixed bug in FlowExecutorFactoryBean where the ClientContinuationFlowExecutionRepository was being + configured with the SessionBindingConversationManager by default (SWF-216). +* The conversationManager argument of the createExecutionRepository hook method in the + FlowExecutorFactoryBean is now possibly null. Review your implementation if you were overriding + this method! +* Fixed JDK 1.3 compatibility issue in FlowSystemDefaults (SWF-227). + +Package org.springframework.webflow.context +* ExternalContextHolder now uses a ThreadLocal instead of an InheritableThreadLocal (SWF-206). + +Package org.springframework.webflow.conversation +* The default value of the maxConversations property of the SessionBindingConversationManager is + now 5. It used to be -1 (unlimited). +* Added getConversationIdGenerator and getMaxConversations methods to SessionBindingConversationManager. +* Reduced log level from INFO to DEBUG. + +Package org.springframework.webflow.definition +* Added debug logging to FlowDefinitionRegistryImpl. + +Package org.springframework.webflow.engine +* The methods getMatchingCriteria, getExecutionCriteria, getTargetStateResolver, and canExecute + of the Transition class have been made public (SWF-209). +* Added debug logging to FlowAssembler, RefreshableFlowDefinitionHolder and FlowExecutionImplFactory. +* The "alwaysRedirectOnPause" flow execution attribute value can now also be a String. +* Added toString implementations to XmlFlowBuilder and AbstractFlowBuilder. +* Added support for the "name" attribute to the and elements in the flow XML. +* Fixed bug in method isFlowElement of XmlFlowBuilder to make it namespace-aware (SWF-226). +* TransitionExecutingStateExceptionHandler now does full logging of the exceptions that it handles. +* Fixed bug in BaseFlowServiceLocator where it was not allowing user to override the default web flow + converters (SWF-220). + +Package org.springframework.webflow.execution.factory +* The getHolder method on ConditionalFlowExecutionListenerLoader is now private. It has a package + private return type, so there is no use in it being protected. + +Package org.springframework.webflow.execution.repository +* Fixed a bug in FlowExecutionContinuationGroup. It was not correctly handling an update of a + continuation (SWF-211). +* The default value for the maxContinuations property of the ContinuationFlowExecutionRepository + is now 30. It used to be -1 (unlimited). +* Method getConversationManager of AbstractConversationFlowExecutionRepository is now public. +* Added debug logging to the FlowExecutionRepository implementations. + +Package org.springframework.webflow.executor +* Added debug logging to FlowExecutorImpl. + +Package org.springframework.webflow.samples +* Made sample projects Eclipse Dynamic Web Projects for easy deployment from Eclipse. + +Changes in version 1.0 (25.10.2006) +----------------------------------- + +Package org.springframework.webflow.action +* Fixed bug in FormAction where the binder was not properly initialized in all cases (SWF-193). + +Package org.springframework.webflow.definition.registry +* Added createFlowDefinitionRegistry hook method to AbstractFlowDefinitionRegistryFactoryBean. + +Package org.springframework.webflow.engine +* Removed END_STATE_VIEW_FLAG_PARAMETER from TextToViewSelector. As a result, the endingViewSelector + method of AbstractFlowBuilder was also removed. +* Removed newDefaultFlowServiceLocator method from AbstractFlowBuildingFlowRegistryFactoryBean. +* Fixed bug in RefreshableFlowDefinitionHolder. It was not correctly reloading flows when resources + changed. +* Added "input-attribute" and "output-attribute" convenience elements to "input-mapper" and + "output-mapper" elements, respectively. +* Renamed DefaultFlowAttributeMapper to ConfigurableFlowAttributeMapper for clarity and to avoid confusion. +* Restored TargetStateResolver to support dynamic transitions. + +Package org.springframework.webflow.executor +* FlowExecutorImpl now updates the flow execution in the flow execution repository after a refresh, + without generating a new key. This ensures that side effects of render actions live beyond the + current request. +* Refactored FlowExecutorArgumentExtractor into an interface and introduced FlowExecutorArgumentExposer + and FlowExecutorArgumentHandler. What the FlowExecutorArgumentExtractor used to be is now + RequestParameterFlowExecutorArgumentHandler. RequestPathFlowExecutorArgumentExtractor was renamed + to RequestPathFlowExecutorArgumentHandler. + The flow controllers (FlowController, FlowAction, PortletFlowController and FlowPhaseListener) now + use a FlowExecutorArgumentHandler instead of just a FlowExecutorArgumentExtractor. The corresponding + "argumentExtractor" properties of the flow controllers were renamed to "argumentHandler". +* Fixed bug in FlowPhaseListener where flow execution was being saved twice on redirects out of the + control of the JSF lifecycle due to obeying response complete lifecycle semantics incorrectly (SWF 181). + +Package org.springframework.webflow.test +* Introduced createExternalContext hook method in AbstractFlowExecutionTests. + +Changes in version 1.0 RC4 (04.10.2006) +--------------------------------------- + +General +* Restructured codebase into distinct layers and subsystems upon in-depth SonarJ analysis of architecture. + Notable restructuring: + - Extracted stable "Flow Definition" API to webflow.definition package from root webflow package + - Extracted stable "Flow Definition Registry" subsystem to webflow.definition.registry. + - Extracted stable "Flow Execution" API to webflow.execution package from root webflow package. + - Extracted stable "External Context" subsystem to webflow.context from root webflow package. + - Extracted "Flow Execution Engine" implementation to webflow.engine package from root webflow package, + decoupling stable Flow Definition, Flow Definition Registry, Flow Execution, Flow Execution Repository, + and Flow Executor subsystems from the more volatile engine implementation. + - Moved webflow.builder subsystem into the webflow.engine implementation package. + - Extracted the XML-based Flow Builder into webflow.builder.xml sub package. + - Extracted generic data structures into webflow.core.collection from root webflow package. + - Extracted generic "Conversation Manager" subsystem into webflow.conversation from execution.repository. + - Factored out test support code into "engine" and "execution" subpackages, for unit testing + flow artifacts and system testing flow executions respectively. + Restructuring effort successfully reduced project average component dependency from 48 to 27 (45%), + largely by isolating and encapsulating the complexity of the Spring Web Flow engine implementation. + See reference documentation and JavaDoc overview for layer and subsystem descriptions. + See root spring-webflow project directory for SonarJ architecture document - "webflow-architecture.xml" +* Refined and polished method throws exception signatures in various places. +* JavaDoc enhancements all over the source code. +* Significant reference documentation updates. + +Package org.springframework.webflow.action +* Added EvaluateAction for evaluating Expression objects against the RequestContext as part of Action execution. +* Added SetAction for setting attribute values in a configured scope on Action execution. +* Added note about the "final" nature of FormAction's action methods. In general, do not override action methods, + instead have your flow call multiple methods in a chain or have your action method delegate to an explict overridable hook. +* Added support for reinstalling custom property editors on FormAction.setupForm if necessary; for + example, because the errors collection was serialized and transient editors were lost. +* Factored out generic ResultEventFactory and ResultEventFactorySelector helpers. +* Collapsed default RC3 FormAction.createFormObject behavior into "loadFormObject" for simplictly and + renamed "loadFormObject" to simply "createFormObject" for consistency. This leaves a single hook + -- createFormObject -- to customize how the form object instance is created/retrieved. +* Extracted FormAction.registerPropertyEditors(PropertyEditorRegistry) hook, now called by initBinder + on bind and initFormErrors on setupForm. +* Renamed MethodResultSpecification to ActionResultExposer indicating its more general applicability. +* Removed GuardedAction as it was not being used. +* Removed automatic adaption of a Spring-managed prototype bean to a flow-scoped bean. Overloaded usage + of prototype semantics was confusing particular in a Spring 2.0 environment with the notion of custom + bean scopes. If you need flow-scoped beans, use flow variables (see 'var' element). You can call + methods on a flow-scoped bean using an evaluate action (see 'evaluate-action' element and Numberguess + sample application). +* Removed experimental and now unused bean action related classes BeanFactoryBeanInvokingAction, + BeanStatePersister, and related Memento persister classes. Flow variables with evaluate actions + provide a better solution. In the future we may provide support for restoring transient variable + references on flow execution restoration. + +Package org.springframework.webflow.config +* Introduced new Spring 2.0 webflow-config XML namespace, significantly simplifying user configuration + of the flow execution engine. See spring-webflow-config.xsd and sample applications for more information. +* Introduced Spring 1.2 compatible FlowExecutorFactoryBean for easing configuration of the flow + execution engine. See itemlist sample for an example of how to use. +* Introduced FlowSystemDefaults encapsulating system-wide default settings. These defaults include: + - repositoryType = 'continuation' + - alwaysRedirectOnPause = 'true' + Defaults may be overridden on a flow executor basis. The defaults themselves are also configurable. + +Package org.springframework.webflow.core +* Factored out "AttributeMap" interface and renamed default implementation to "LocalAttributeMap". +* Factored out "ParameterMap" interface and renamed default implementation to "LocalParameterMap". +* Factored out "MutableAttributeMap" interface from "AttributeMap" for modifiable maps. +* Added AttributeMapBindingListener abstraction for listening to bind and unbind events in supported attribute maps + such as the external context session map. + +Package org.springframework.webflow.context +* Added support for accessing a PortletRequest.USER_INFO map as context.getUserInfoMap(). +* Added support for MultipartActionRequest parameters for fileupload support in a Portlet environment. +* Introduced ExternalContextHolder for setting a thread-bound ExternalContext. Used by FlowExecutionRepository + implementations that need to access external state such as the sessionMap. +* Promoted the "globalSessionMap" property to the normalized ExternalContext interface. +* Factored out generic Map decorators to spring-binding collection package. + +Package org.springframework.webflow.conversation +* Renamed ConversationService to ConversationManager. +* Added SessionBindingConversationManager for storing conversations in the session map. +* Fixed bug where an IllegalMonitorException could occur on implicit conversation unlock on 'end'. + +Package org.springframework.webflow.definition +* Introduced stable FlowDefinition abstractions, useful for reasoning on flow definition metadata + to support tooling and other runtime introspection. +* Relocated FlowLocator and related exception types from execution to registry subpackage. +* Renamed FlowLocator to FlowDefinitionLocator for consistency. +* Renamed FlowRegistry to FlowDefinitionRegistry for consistency. +* Renamed FlowRegistrar to FlowDefinitionRegistrar for consistency. +* Renamed ExternalizedFlowDefinition to FlowDefinitionResource for clarity. +* Reworked FlowDefinitionRegistrar interface, simplifying and decoupling it from the concept of a FlowServiceLocator. +* Removed FlowDefinitionRegistryFactoryBean as it was not used. + +Package org.springframework.webflow.engine +* Introduced full support for XML schema, see builder/xml/spring-webflow.xsd +* Removed support for XML DTD, removing spring-webflow.dtd. +* Significantly enhanced in-line XML element and attribute documentation for benefit of .xsd users. +* Reworked support for "bean invoking actions". Introduced new tag. +* Added support for elements for evaluating arbitrary flow expressions as an action using OGNL. +* Added support for elements for setting attributes in a scope type as an action. +* Added support for 'required' mappings. A required mapping will report an error if its source expression evaluates to a null value. +* Added 'renderActionList' property to ViewState, for executing behavior before a view selection is rendered. +* Added support for within a element, for executing behavior + when the view state makes a renderable view selection. +* Added the ability to invoke a list of actions before executing an exception handling transition. +* Added several AbstractFlowBuilder.addViewState convenience operations. +* BaseFlowBuilder now resets its internal flow reference on a call to dispose. +* FlowAssembler.assembleFlow now returns the constructed flow for clarity. This also clarifies the API in that it is no longer + required to call getFlow on a FlowBuilder that has already been disposed by the FlowAssembler. +* Replaced STATE_TYPE_CONTEXT_PARAMETER parameter used by the TextToViewSelector by END_STATE_VIEW_FLAG_PARAMETER which just + has a boolean value (a flag). This makes the contract a bit clearer. As a result, the VIEW_STATE_TYPE and END_STATE_TYPE + constants in TextToViewSelector were also dropped. +* Moved ImmutableFlowAttributeMapper to builder and made package-private. Not a general support class. +* Reworked FlowExecution transient state restoration behavior, now using + a dedicated FlowExecutionImplStateRestorer. +* Added support for EndState transitions on exception, typically thrown as party of an entry action. + Transition.execute now accepts the base State type now, no longer requiring a TransitionableState reference. +* Made execution state restorer strategy always apply, updated execution impl types to keep transient + and serialized references in sync for VM clustering purposes. +* Extracted DocumentLoader strategy interface from XmlFlowBuilder to isolate dependency on DOM/SAX + DocumentBuilder factory logic. +* Fixed bug where flow execution restoration would fail when the active flow was an in-line flow + of a subflow. +* TransitionExecutingStateExceptionHandler now exposes exceptions in flash scope now so they live through + a view redirect and any subsequent refreshes. + +Package org.springframework.webflow.execution +* Added support for "flow execution attributes" for setting system parameters that can affect flow execution. +* Added new "flash" scope type for storing attributes for the duration of a request plus a redirect + and any subsequent view refreshes. More specifically, flash is cleared when the next user event + is signaled. +* Added support for listening to "exceptionThrown" events in a FlowExecution - see FlowExecutionListener. +* Decoupled the central RequestContext interface from the flow execution engine implementation. +* Renamed the "flow" property of FlowExecutionContext to "definition" for consistency. +* Renamed the "flow" property of FlowSession to "definition" for consistency. +* Renamed FlowRedirect to FlowDefinitionRedirect for clarity. +* Made all modifiable execution data maps "MutableAttributeMap" references: this includes + "request scope", "flow scope", and "conversation scope". Umodifiable maps are simply plain + "AttributeMap" references: this includes request context attributes, flow execution system + attributes, and flow definition attributes. +* Introduced FlowExecutionFactory for encapsulating the construction of a FlowExecution from a FlowDefinition. +* Moved generic FlowExcutionFactory support code into "factory" subpackage. +* Fixed inconsistency in continuation id parsing within ClientContinuationFlowExecutionRepository. +* Removed FlowExecutionRepositoryFactory abstraction, simplifying overall use of the + FlowExecutionRepository subsystem. +* Improved support for customization of FlowExecutionContinuation creation via the FlowExecutionContinuationFactory. +* Event id-based transition matching expressions are now case sensitive. + +Package org.springframework.webflow.executor +* Decoupled executor subsystem from the flow execution engine implementation and assumptions about engine defaults. +* Removed 'alwaysRedirectOnPause' property in favor of dedicated 'alwaysRedirectOnPause' flow execution attribute. +* Changed RequestPathFlowExecutorArgumentExtractor to append keyDelimiter property as path element instead of + request parameter for flow execution URLs. +* Renamed FlowExecutor.signalEvent to FlowExecutor.resume for clarity and revised order of parameters accordingly. + +Package org.springframework.webflow.samples +* Updated sample applications to take advantage of new features including: + - New spring-webflow-config Spring 2.0 XML Schema + - "Render actions" (Shipping rate) + - "Always redirect on pause" (Sellitem, Itemlist) + - "Bean actions" (Phonebook, Sellitem) + - "Flow variables" and "Evaluate actions" (Numberguess) + - "Flash scope" and "Set" actions (Fileupload) + Please review samples to see these features in action! + +Package org.springframework.webflow.test +* Changed AbstractXmlFlowExecutionTests 'getFlowDefinition' hook to 'getFlowDefinitionResource' returning + a FlowDefinitionResource. See phonebook and sellitem src/test trees for an example. +* Added several convenience flow definition resource factory methods. +* Removed startFlow variants in AbstractFlowExecutionTests that accept a flow execution listener, call + setFlowExecutionListener(...) instead. +* Added convenient registerMockServices hook to AbstractExternalizedFlowExecutionTests. + +Changes in version 1.0 RC3 (23.6.2006) +-------------------------------------- + +Package org.springframework.webflow +* Renamed TransitionSet.transitionMatches to "hasMatchingTransition". +* A flow variable can now be annotated with arbitrary properties to influence behaviour. +* Factored out shared behavior of TransitionSet and StateExceptionHandlerSet into CollectionUtils. +* Renamed FlowExecutionControlContext to RequestControlContext to avoid confusion with the FlowExecutionContext + and highlight the relationship with the RequestContext. +* Renamed FlowArtifactException to FlowArtifactLookupException and relocated to builder package. +* Renamed StateException to FlowExecutionException. Revised semantics to ensure it is serializable. +* Renamed StateExceptionHandler to FlowExecutionExceptionHandler. +* Renamed StateExceptionHandlerSet to FlowExecutionHandlerSet. +* Removed FlowExecutionStatistics, merging its only useful method (isActive) into the FlowExecutionContext interface +* Removed unnecessary Event argument from Flow.onEvent and TransitionableState.onEvent. +* Removed CannotExecuteTransitionException, as its use reflected an illegal state. +* Removed NoSuchFlowStateException, as its use reflected an illegal argument. +* Made all exceptions honor serializable contract, removing dependencies on internal transient arguments such as Flow, State, and Transition + in favor of serializable artifact identifiers such as the flowId and stateId. + +Package org.springframework.webflow.action +* Renamed FormAction.VALIDATOR_METHOD_PROPERTY to VALIDATOR_METHOD_ATTRIBUTE for consistency with other + context attributes. +* Removed FormAction.validateUsingValidatorMethod property. If you don't want validation, don't call bindAndValidate() + or validate() but just bind(). Updated samples accordingly. +* Removed AbstractAction.setEventFactorySupport(). In the unlikely case that you want to use an custom subclass of + EventFactorySupport, just override AbstractAction.getEventFactorySupport(). +* Removed FormActionMethods since it served little purpose and was very closely linked to the way FormAction + implemented form handling. +* Renamed ResultSpecification to MethodResultSpecification for clarity. The property AbstractBeanInvokingAction.resultSpecification + was also renamed accordingly. + +Package org.springframework.webflow.builder +* Reapplied 1.0 rc2 fix for inline flow parsing, which was lost in SourceForge CVS issues occurring between 7/5/06 and 15/5/06 + +Package org.springframework.webflow.execution +* Renamed ConversationService.begin to "beginConversation". +* Removed implicit unlock call in LocalConversationService.removeFlowExecution +* Fixed a bug where ConversationEntry.lock field was serializable--it should be transient. +* Made ConversationServiceException extend NestedRuntimeException, since it is a distinct subsystem root exception. +* Added FlowExecutionRestorationFailureException. +* Added BadlyFormattedFlowExecutionKeyException. +* NoSuchConversationException now triggers a NoSuchFlowExecution exception. +* InvalidContinuationIdException, ContinuationNotFoundException, and ContinuationUnmarshalException + now trigger a FlowExecutionRestorationException. + +Package org.springframework.webflow.executor +* Refined PortletFlowController to no longer set flowExecutionKey render parameter when an execution terminates. +* Removed the temporary workaround accounting for differences between SWF view names and JSF view ids. Now + by default it is expected that the SWF view name match the JSF view id exactly. A custom ViewIdMapper can be + configured to configure a specific mapping strategy. +* Renamed ViewIdResolver to ViewIdMapper. + +Package org.springframework.webflow.executor.support +* Removed the appendFlowInputAttributesToRequestPath flag in RequestPathFlowExecutorArgumentExtractor since it didn't offer + a ready-to-use system--you would need to do custom coding to make it work completely. + +Package org.springframework.webflow.registry +* Made NoSuchFlowDefinitionException a standalone exception of the flow registry subsystem. + +Package org.springframework.webflow.samples +* Simplified 'flowLauncher' to take advantage of latest attribute mapping features. +* Simplified 'fileUpload' to take advantage of convenient MultipartFile parameter access support. +* Refined 'birthdate', 'sellitem', and 'sellitem-jsf' to use simpler "bind" methods instead of 'bindAndValidate' with a 'validateUsingValidatorMethod=true' flag. +* Refined sellitem-jsf to demonstrate default view name behavior within a JSF environment--that is, + where view-state 'view' names should match JSF view ids exactly. + +Package org.springframework.webflow.support +* Fixed bug in FlowRedirectSelector where a NPE was thrown on null redirect expression. +* Removed RedirectType. Classes using it now have a simple boolean redirect flag. + +Changes in version 1.0 RC2 (09.6.2006) +-------------------------------------- + +Overall +* Moved project source control (with history preserved) to sourceforge subversion (SVN) repository at: + https://svn.sourceforge.net/svnroot/springframework/spring-projects/trunk +* Updated projects structure to maven2 standard (e.g. src/main/java, src/test/java). +* Spring-projects JAR repository structure now follows maven2 standard. + +Package org.springframework.webflow +* Removed 'requiredType' not null check on AttributeMap accessor methods; if no requiredType is specified + the type assert is simply not performed. +* Fixed bug in Flow.end where outputMapper source and target arguments were backwards. +* Removed unused constructors in AttributeMap. +* Added CollectionUtils.singleEntryMap for easily populating a UnmodifiableAttributeMap with a single attribute. +* AttributeMap, UnmodifiableAttributeMap, and ParameterMap implement equals and hashCode now. + +Package org.springframework.webflow.action +* Moved MultiAction.METHOD_ATTRIBUTE constant to AnnotatedAction.METHOD_ATTRIBUTE. +* Made FormAction.formObjectScope default ScopeType.FLOW instead of ScopeType.REQUEST. +* Improved FormAction debug logging. +* Removed FormAction.bindOnSetupForm flag for simplicity--if you need to perform a bind operation before + entering a view state simply invoke the 'bind' action method from your flow definition. +* Removed FormAction.validateOnBinding flag for simplicity--if you need to dynamically calculate if + validation should occur after binding on 'bindAndValidate' override validationEnabled(RequestContext). +* Removed 'exposeFormObject' action method from FormAction; use 'setupForm' instead. + +Package org.springframework.webflow.builder +* Fixed bug related to inline-flow parsing where inline flow artifacts were not assembled properly. +* Added not null asserts to setters allowing overriding of required flow builder services. +* Improved DTD documentation describing input-mapper and output-mapper element usage. +* Relaxed 'isMultiAction' assert in XmlFlowBuilder to simply 'isAction', allowing provision of the 'method' + annotated action attribute with any target Action implementation, not just MultiActions. +* Removed 'isMultiAction' tester on FlowServiceLocator as it is no longer needed by the builders. +* Renamed 'resultName' and 'resultScope' element attributes to 'result-name' and + 'result-scope', respectively, for consistency with other attribute and element names. +* Removed support for special "conversationRedirect:" 'view' prefix. + +Package org.springframework.webflow.context +* Enhanced PortletExternalContext to provide access to the "globalSessionMap" for accessing attributes in + the session's APPLICATION_SCOPE. + +Package org.springframework.webflow.execution +* Fixed bug in FlowExecutionImpl toString, where a NPE could be thrown if toString was called during + a FlowExecution.start (after becoming active but before having the current state set). +* Added various not null asserts to setters allowing overriding of required flow execution repository services. +* Changed ContinuationFlowExecutionRepositoryCreator's default 'maxContinuations' property to 0, setting + no upper bound on the number of continuations that can exist per conversation by default. +* Renamed EmptyFlowExecutionListenerLoader to StaticFlowExecutionListenerLoader and improved to accept + a static listener array for convenient listener loading without conditionals. +* Reworked FlowExecutionRepository interface to encapsulate the flow execution key generation strategy + as well as the additional (and optional) concept of a conversation. +* Factored out conversation code into a portable conversation management subsystem, see repository.conversation. + This generic abstraction is usable outside of SWF and may be promoted to a top-level project in the future. +* Renamed SimpleFlowExecutionRepository to DefaultFlowExecutionRepository. +* Removed 'CannotContinueConversationException' from root repository package. Now the + 'NoSuchFlowExecutionException' is thrown consistently by the repository implementations + for both invalid conversation and continuation identifiers with the 'cause' property + noting exactly what happened. +* Moved FlowExecutionRepositoryCreator to the support package and made implementations private + inner classes of their containing factories, reflecting the role as a helper. + +Package org.springframework.webflow.executor +* Simplified FlowExecutor interface, exposing simple types to clients and encapsulating dependencies + on internal types (as a good facade should). +* Removed all support for conversation "refresh" and redirect, decoupling the optional concept of + a conversation from that of a flow execution. To achieve the 1.0 EA and 1.0 RC1 equivalent semantics + of a "conversation redirect" use a flow execution redirects with FlowExecutionRepository that uses + the same FlowExecutionKey for the duration of the FlowExecution. +* Refined support for context-relative external redirects; now by default external URLs that begin with + a leading '/' are treated as context-relative. The FlowExecutorArgumentExtractor.redirectContextRelative + property allows for overriding this default. +* Fixed bug in RequestPathFlowExecutorArgumentExtractor.createFlowUrl where flow redirect input + was not being appended to the redirect URL correctly. +* Fixed bug where JSFPhaseListener was resetting the UIViewRoot even if the view did not change as + part of a resubmission. +* Added support for populating the FlowExecution inputMap from ExternalContext attributes during a launch operation. + This allows for a flow to be passed input from clients that start it, and subsequently + map the input into its local scope using a input-mapper. +* Portlet flow controller now refreshes a flow execution on render request if necessary to support browser refresh. +* Portlet flow controller can now perform external redirects using ActionResponse.sendRedirect(url). + +Package org.springframework.webflow.registry +* Improved ExternalizedFlowRegistrar, extracting a factory method for creating an ExternalizedFlowDefinition + from a valid Resource location, and a template method for calculating if the location is actually a flow resource. +* Fixed a bug in AbstractFlowRegistryFactoryBean where a NPE would occur when configuring custom DefaultFlowServiceLocator services. +* Generally improved pluggability of core FlowBuilder services with the AbstractFlowRegistryFactoryBean, + including the FlowArtifactFactory, BeanInvokingActionFactory, ExpressionParser, and parent ConversionService implementations. + +Package org.springframework.webflow.support +* Improved TransitionExecutingStateExceptionHandler to consider exception superclasses in handler match. +* Improved TransitionExecutingStateExceptionHandler to expose "rootCauseException" as request scoped attribute +* Added TransitionExecutingStateExceptionHandler debug level logging. +* Changed "handledStateException" attribute to simply "stateException". +* Fixed bug in DefaultExpressionParserFactory where Ognl would be loaded before explict classpath check. +* Removed ConversationRedirect. + +Package org.springframework.webflow.test +* Added flowExecution not null asserts to signalEvent and refresh AbstractFlowExecutionTests operations. +* Removed AbstractFlowExecutionTests.conversationRedirect method. + +Changes in version 1.0 RC1 (03.5.2006) +-------------------------------------- + +Package org.springframework.webflow +* Added explict Flow variable support to this package; see Flow.addVariable and Flow.start +* Fixed a bug in ParameterMap related to multi-valued parameter access +* ParameterMap.get now returns the first element for a multi-valued parameter instead of throwing an exception +* Refined the semantics of flow input attribute mapping. All input attributes passed into a flow + by a caller must now be explictly mapped by the flow. To achieve this each Flow can be configured + with an inputMapper; see Flow.setInputMapper and Flow.start. +* Removed the 'action' property of the DecisionState. Use an ActionState if the purpose of the + state is to execute an Action and respond to its result. Consider using a custom ResultEventFactory + to customize how the result event is created for bean invoking actions. + +Package org.springframework.webflow.action +* Removed FlowVariableCreatingAction, superceded by variable support added to core package. +* Extracted ResultObjectBasedEventFactory and SuccessEventFactory implementations of the ResultEventFactory interface. +* Introduced MementoBeanStatePersister for saving and restoring action bean state from a flow-scoped managed memento. + +Package org.springframework.webflow.builder +* Added a 'bean' attribute to the 'var' element, for delegating to a Spring bean factory for flow variable creation. +* Made the 'bean' and 'class' attributes optional of the 'var' element; by default the 'name' attribute will be treated as the name + of a prototype bean in the BeanFactory to use to create the flow variable value. +* Added 'input-mapper' and 'output-mapper' elements to the 'flow' element, for mapping input and output attributes respectively. +* Changed the 'attribute-mapper' element of the 'subflow-state' element to be consistent with the new + 'input-mapper' and 'output-mapper' elements, for mapping input and output attributes to and from a subflow, respectively. +* Changed the 'output-attribute' element of the 'end-state' element to be consistent with the new + 'output-mapper' elements, for output attributes specific to a flow outcome. +* Added an 'on-exception' attribute to the 'transition' element, for executing a state transition as part of + state or flow exception handler logic. This supercedes use of the 'class' and 'to' attributes of the + 'exception-handler' element. +* Streamlined the 'exception-handler' attribute to support attaching custom exception handler implementations only. + Favor use of for attaching transition executing state exception handlers. +* Added special detection for "stateful actions"; beans marked singleton=false (non-singleton prototypes) are treated + as stateful actions, with their instances managed directly in flow scope by default. Beans implementing MementoOriginator + are treated as stateful actions responsible for creating mementos that house their state managed in flow scope. +* Added special detection for action "result event factories"; bean methods that return a scalar 'value object' are + configured with a ResultObjectBasedEventFactory, otherwise a SuccessEventFactory is used. +* Added back the state type context attribute to TextToViewSelector, to aid in creating the correct ViewSelector + implementation based on the state type (ViewState or EndState) in use. +* Changed the 'on' attribute of the 'transition' element to be optional. +* Enhanced the 'to' attribute of the 'transition' element ${expression} capable; the resolved string expression(s) are + treated as the target state of the transition. +* Enhanced the 'view' attribute of the 'view-state' and 'end-state' elements to be fully ${expression} capable; previously + expressions could only be defined for redirect parameters. +* Renamed the FlowArtifactFactory interface as it existed in 1.0 EA to FlowServiceLocator. Factored out two new + "services" from the original interface: FlowArtifactFactory, for encapsulating the construction of core flow elements + like State and Transition, and BeanInvokingActionFactory, for encapsulating the construction of an Action that invokes + a method on a bean when executed. +* Refined the FlowBuilder interface, adding builder methods for each type of major Flow construct. +* Reworked AbstractFlowBuilder and XmlFlowBuilder to bring them in-line with the FlowServiceLocator and FlowBuilder refinements. +* XmlFlowBuilder now respects deployment within a WebApplicationContext now, supporting access to the + ServletContext from within beans deployed in flow-local contexts. + +Package org.springframework.webflow.context +* Changed the String-keyed map attribute iteration construct from 'Enumeration' to 'Iterator'. + +Package org.springframework.webflow.execution +* Simplified the ConditionalFlowExecutionListenerLoader configuration interface. +* Refined the FlowExecutionListener interface, improving the sessionStarting and eventSignaled method signatures. +* Fixed a bug in ContinuationFlowExecutionRepository related to conversation scope restoration (which could cause a NPE). +* Fixed a bug in FlowExecutionImpl related to nested exception handling. +* Added an 'input' Attribute argument to the FlowExecution.start operation, for passing input into a starting + flow execution in a consistent manner. +* Added support "refreshing" a paused flow execution, an idempotent operation used to support flow execution redirects. +* Introduced an EventId value object for describing an external event that has been signaled. +* Streamlined FlowExecutionRepository interface to remove experimental 'getCurrentViewSelection' and + 'setCurrentViewSelection' methods. + +Package org.springframework.webflow.executor +* Added 'appendFlowInputAttributesToRequestPath' property to RequestPathFlowExecutorArgumentExtractor to control + if flow redirect input is appended to the URL request path or by use of named query parameters. +* Added "redirectOnPause" attribute to FlowExecutorImpl to allow global enforcement of flow execution redirects for paused flows. +* FlowExecutorArgumentExtractor now throws typed argument extraction exceptions to report illegal arguments provided by clients. +* Changed JSF "resume flow" behavior to no longer require an eventId; if no eventId is provided in a request the current view will be refreshed. +* Changed JSF "launch" flow behavior to no longer require a nav handler outcome; if the _flowId parameter + is provided in the request the FlowPhaseListener will launch the new flow execution on RESTORE_VIEW instead. +* Fixed a situation where a NullPointerException could occur within JSF's FlowPhaseListener. +* Added support for conversation redirects, external redirects, and flow redirects within a JSF environment. +* Misc polishing + +Package org.springframework.webflow.registry +* Fixed a race condition within RefreshableFlowHolder pertaining to flow assembly. +* Added 'builderValidating' and 'entityResolver' properties of XmlFlowRegistryFactoryBean configuration interface. +* Reworked to bring registry subsystem in-line with changes in builder system. All "flow services" may be + configured by Spring now by setting properties of a FlowRegistryFactoryBean. +* Added setters to AbstractFlowRegistryFactoryBean for configuring common flow builder services. +* Misc polishing + +Package org.springframework.webflow.support +* Added two concrete FlowVariable types, SimpleFlowVariable and BeanFactoryFlowVariable. +* Added FlowExecutionRedirect, for redirecting to a "current" ApplicationView of a FlowExecution. + This facilitates post+redirect+get semantics with unique resource URLs for refreshing each + flow execution continuation (allowing back and refresh button use without page caching). +* Factored out a RedirectType enumeration, as there are now two redirect options: FlowExecutionRedirect and + ConversationRedirect. +* Added ImmutableFlowAttributeMapper. + +Package org.springframework.webflow.test +* Misc polishing + +Package org.springframework.webflow.samples +* Made all samples Spring IDE projects. +* Made the number guess "games" fully stateful, 100% decoupling game logic from SWF APIs. + +Changes in version 1.0 Early Access (02.3.2006) +----------------------------------------------- + +Overall +* The proposal to the community for Spring Web Flow 1.0; the next release will be 1.0 RC1 + after a fixed user evaluation period +* Introduced the reference manual in HTML and PDF form, see "docs" directory. +* Added unit tests, with total test coverage above 70%. +* Added extensive JavaDoc enhancements. + +Package org.springframework.webflow +* Added support for custom start, signal event, end, and handleException behavior to the Flow class. +* Added state exception handling support at the State level. States may now be configured with a set of + one or more StateExceptionHandler objects for responding to exceptions that occur within the state of a flow execution. +* Added state exception handling support at the Flow level. Flows may now be configured with a set of + one or more StateExeceptionHandler objects, for responding to exceptions that occur within a state but are not handled by that state. +* Added support for "global transitions" at the Flow level. Flows may now be configured with a set + of one or more Transition objects that are inherited by all states of the Flow. +* Added Transition TargetStateResolver strategy, allowing for dynamic target state calculation. +* Added the ExternalContext facade for providing a normalized interface about external clients who have called into + the Spring Web Flow system. +* Added ParameterMap and AttributeMap map decorators, for strongly-typed Map access support. +* Renamed ViewDescriptor and ViewDescriptorCreator to ViewSelection and ViewSelector, respectively, for clarity and consistency with lexicon. + Also made ViewSelection immutable, as a value object created by a selector. +* Introduced a ViewSelection hierarchy for each of the supported response types: ApplicationView (forward), + ConversationRedirect, FlowRedirect, ExternalRedirect, and NullView, respectively. +* Renamed AnnotatedObject properties and Event parameters to "attributes", respectively, for consistency. + +Package org.springframework.webflow.action +* Added support for invoking strongly typed methods on arbitrary beans (POJOs) as a SWF Action. + SWF is now capable of invoking an abitrary java.lang.Object method + like 'search(SearchCriteria critera)', when using a "bean invoking action" as an + alternative to implementing the 'execute(RequestContext context)' Action interface or + extending MultiAction. SWF is also capable of exposing return values on those bean methods + in request or flow scope, as well as responding to method exceptions. + See the Phonebook sample app for an example. +* Added BeanFactoryBeanInvokingAction, an action that can invoke any instance method on any bean managed by the Spring bean factory. +* Added LocalBeanInvokingAction, an action that can invoke any instance method on any bean. +* Added "CompositeEvent" support for composite actions that may generate multiple action result events. +* Added "validateUsingValidatorMethod" property to FormAction, useful for supporting piecemeal validation as part of a wizard flow. +* Added ActionUtils utility class, useful when implementing actions. +* Added a convenience constructor to the Event class that takes a single parameter name/value. Mainly for use in unit tests. +* Added a convenience constructor to the FormAction class that takes the formObjectClass name. +* Added logic in FormAction to automatically calculate a camelcase formObjectName from the formObjectClass if the name is not set. +* Added StatefulActionProxy, an action that delegates to a stateful action managed in flow scope. +* Added convenience methods for retrieving action execution properties to the AbstractAction class. +* Improved "validatorMethod" handling in FormAction: if no formObjectClass is specified, the invoked validator method + should have a signature matching "public void ${validatorMethod}(Object obj, Errors errors)". When you do specify a + formObjectClass, everything works like before, e.g. the signature will be + "public void ${validatorMethod}(${formObjectClass} obj, Errors errors)". +* Removed DelegatingAction in favor of StatefulActionProxy and bean invoking actions. + +Package org.springframework.webflow.builder +* Added support for "global-transitions", transitions attached at the flow level and inherited by all states. +* Added support for "inline-flows", local flow definitions fully nested within another flow definition. +* Added support for locally flow scoped artifacts, importable as bean definitions using the "import" element. +* Added support for action "resultName" and "resultScope" properties, for exposing POJO method return values + as attributes in a flow execution scope automatically. See 'action' element in XML DTD. +* Added support for automatic creation of flow variables during flow startup, see 'var' element in XML DTD. +* Added "redirect" attribute to view-state for requesting conversational redirects that permit browser refresh. +* Added support for mapping subflow output attributes into parent flow collections. See 'output' element in XML DTD. +* Added AbstractFlowBuilder addDecisionState(...) methods for completeness. +* Introduced the FlowArtifactFactory, for accessing externally managed flow artifacts during the flow building process. +* Changed AbstractFlowBuilder addSubflowState(...) methods to return the added SubflowState, consistent with other add methods. +* Reworked FlowBuilder, externalizing flow id assignment from the builder itself (assignment is now a responsibility of the director). +* Renamed the "config" package to "builder" for clarity. +* Removed flow artifact creation and autowiring support in favor of artifact lookup by id (which is more compelling now with flow-definition scoped artifact support). + +Package org.springframework.webflow.context +* Introduced webflow.context package for housing common ExternalContext implementations. +* Introduced ExternalContext implementations for HTTP Servlet and JSR 168 Portlet environments. + This adds support for accessing request, session, and application variables from within SWF + in a consistent manner (regardless of the environment in which SWF is called). + +Package org.springframework.webflow.execution +* Added paused, resumed, and sessionEnding FlowExecutionListener callbacks. +* Added UidGenerator strategy, providing a plugin point for generating unique keys. +* Introduced FlowExecutionRepository subsystem, for tracking ongoing conversations between browsers and + the Spring Web Flow system. This subsystem obsoletes both the FlowExecutionStorage and TransactionSynchronizer + infrastructure, as it provides the capabilities of both in a single system. +* Added support for continuation-based repositories (aka 'continuation servers'), + including a built-in capability to limit the max number of continuations allowed per conversation + as well as automatically invalidate all continuations associated with a conversation that has ended + ("conversation invalidation after completion"). Explicit support for conversation and continuation + expiry, as well as metadata-driven continuation invalidation strategies is under consideration. +* Added support for tracking and accessing the "current view selection" of a conversation accessible under + a bookmarkable conversation URL. +* Added for conversation locking, allowing exclusive access to a conversation contended for my multiple + concurrent threads. +* Reworked the flow execution package, decoupling the Event abstraction from the notion of an ExternalContext. +* Reworked the FlowExecution interface, making central start and signalEvent operations more explicit. It is also now + impossible for external actors to signal an event in another state other than the current state. + Also removed the "rehydrate" method from the public interface, as it's an implementation detail to + internal actors that create and restore flow executions. +* Removed FlowExecutionStorage infrastructure, replaced by an enhanced FlowExecutionRepository subsystem. +* Removed TransactionSynchronizer abstraction, which has been obsoleted since such a capability is being built + into the FlowExecutionRepository implementations directly. +* Removed support for invoking flows in arbitrary states other than the current state for security reasons. + Support for a single, externally referenceable navigation state per flow is under consideration. +* Removed ExpiredFlowCleanupFilter, as a more powerful expiry capability built into the + FlowExecutionRepository subsystem is under consideration. + +Package org.springframework.webflow.executor +* Introduced the webflow.executor package, the highest-layer subsystem of SWF for driving the execution of flows. +* Renamed FlowExecutionManager to FlowExecutor; FlowExecutor is now a central facade interface + defining the SWF system boundary for typical clients. FlowExecutorImpl is the default implementation; FlowLocator is now a required dependency. +* Added ResponseInstruction, for allowing strongly-typed access to a flow execution context when preparing + a response to issue. +* Added the FlowExecutorArgumentExtractor helper for extracting arguments needed by FlowExecutor implementations + such as the flowId, flowExecutionKey, and eventId. +* Renamed all instances of "flowExecutionId" with "flowExecutionKey", for consistency with the FlowExecutionKey class. + Note: this change affects the views of existing SWF applications, requiring a rename of the + _flowExcutionId input parameter to _flowExcutionKey. +* Added RequestPathFlowExecutorParameterExtractor, for extracting parameters from the request URL. + This faciliates flows being launched in REST-style, for example http://localhost/springair/reservations/booking. +* Introduced JSF flow executor integration, see the "jsf" package. +* Reinstated the Spring Portlet MVC flow executor integration, see the "mvc" package and "phonebook-portlet" sample. + Note: the portlet MVC support requires Spring 2.0. + +Package org.springframework.webflow.registry +* Added the FlowRegistry subsystem in webflow.registry, for registering groups of refreshable Flow definitions. + With this addition, XML flow definitions may now be refreshed from their registries at runtime by using a + JMX client like Sun's jConsole that ships standard with JDK 1.5. +* Added a refreshable FlowRegistryImpl, for managing a reloadable registry of flow definitions, + typically populated by an XmlFlowRegistrar or custom FlowRegistrar. See XmlFlowRegistryFactoryBean + as a convenient mechanism for populating a FlowRegistry using a standard Spring bean definition. +* Added RefreshableFlowHolder, capable of automatically detecting changes on externalized flow + definitions and refreshing those definitions without requiring a container restart. + +Package org.springframework.webflow.test +* Improved FlowExecution test support with AbstractFlowExecutionTests. +* Decoupled flow execution test infrastructure from a first class dependency on spring-mock and + AbstractTransactionalDataSourceSpringContextTests; this allows for testing flow executions and + their associated artifacts in isolation without a dependency on the container, and also makes it + easier to test a flow execution with a mock service-layer. See Phonebook and Sellitem src/test + tree for an example. +* Added convenient AbstractXmlFlowExecutionTests, for easy testing of xml-based flow definitions. +* Small improvements in MockRequestContext to make it easier to use. + +Package org.springframework.webflow.samples +* Changed numberguess sample application to use the new StatefulActionProxy. +* Added sellitem-jsf sample. +* Added phonebook-portlet sample. +* Added shippingrate sample, showing Spring Web Flow together with Ajax technology. + +Changes in version PR5 (28.7.2005) +---------------------------------- + +* Renamed the static helpers in the ServletEvent class to match with their instance counterparts: e.g. getHttpServletRequest() + was renamed to getRequest(). +* Renamed the addSubFlowState(...) methods in AbstractFlowBuilder to addSubflowState(...) to be consistent with the class name. +* Reorganized packages to remove all cyclic dependencies. Most of the changes were in packages not typically used by end users + of the system. As a result the impact on existing applications should be limited. Changes to look out for are: + FlowConversionService is now in package org.springframework.webflow.convert, ExpiredFlowCleanupFilter is now in package + org.springframework.webflow.execution.servlet and FlowExecutionListenerAdapter moved to the package + org.springframework.webflow.execution. +* Fixed JDK 1.3 compatability issues. +* Fixed several bugs reported in JIRA. +* Several improvements in FlowAction (the SWF-Struts integration). +* Fixed Struts 1.1 compatability issues. +* Minor changes in the SWF DTD: the "property" element is now always the first sub element of its containing parent element. +* Added several unit tests, e.g. for FormAction. +* Drastically improved FormAction. This also involved some minor refactorings in the FormObjectAccessor. +* Improved AttributeMapperAction. +* Improved JavaDoc. +* Miscellaneous code cleanup, especially in spring-binding. + +Changes in version PR4 (17.7.2005) +---------------------------------- + +* Top level package rename from org.springframework.web.flow to org.springframework.webflow. +* Changes to make this SWF compatible with Spring 1.2.2 and later. +* Moved to a new build system based on Ant and Ivy. +* Temporarily dropped Portlet support untill it is included in Spring 1.3. +* Added state entry and exit actions. +* Added some additional callbacks to the FlowExecutionListener: loaded(), saved() and removed(). +* Greatly enhanced expression language support. +* Enhanced attribute mapper type conversion support. +* Added SessionTransactionSynchronizer. +* Added several convenience actions: CompositeAction, DelegatingAction and GuardedAction. +* Introduced basic JMX monitoring capabilities, to be improved in future releases. +* Better and simpler Struts integration. Check the "BirthDate" sample for an example. +* Refactored FormObjectAccessor so that the getters return null instead of throwing an exception when the form object or Errors + instance cannot be found. +* Refactored FlowExecutionListenerList to no longer depend on the closure support in the sandbox. As a result, the + iteratorTemplate() method was removed. +* Fixed NullPointerException in Flow.getTransitionableState(String). +* The FlowExecutionManager now allows you to register FlowExecutionListener s for executions of particular flows. This makes it + easy to have a single manager managing all the flow executions in your app and still have listeners that apply to just a + specific flow. To make this possible, the FlowExecutionListenerCriteria were introduced. +* The TransitionableState now has a reenter() method that will be called when the state is re-entered. This allows you to have + different behaviour when the state is first entered, or re-entered. +* Fixed problem with 'form states': view states that use a setup action and a bindAndValidate when transitioning out of the state. + The validation errors generated by the bindAndValidate were being overwritten by the setupForm action when the view state + re-entered. +* Removed hardwired dependency on LinkedHashSet, for use with JDK 1.3. +* The ongoing flow execution is now exposed to the view as a FlowExecutionContext instance with name flowExecutionContext in the + model. Existing apps should change the use of flowExecution in their views to flowExecutionContext from now on. +* Miscellaneous code cleanup and JavaDoc enhancements. + +Changes in version PR3 (22.5.2005) +---------------------------------- + +* Renamed EventParameterMapperAction to AttributeMapperAction. +* Dynamic (pluggable) view selection and model population capability for view states. +* View state setup actions. +* View forward and redirect expressions. +* Subflow attribute mapping expressions. +* Support for primitive/complex property types (besides string) using new type conversion infrastructure. +* Enhanced flow execution listener lifecycle methods resumed, paused. +* Pluggable expression evaluation capability. +* Pluggability for all core definition objects: Flow, State, Transition, action, ... +* Pluggability of transaction synchronizer interface for custom application-transaction demarcation. +* A lot of general refining and polishing – package structure should be stable now. +* Flow attribute mapping is now possible from a XML definition using the new "input", and "output" mapping elements within the + "attribute-mapper" element (see the spring-webflow.dtd). +* FlowController now provides a setFlow(Flow) method for convenience. +* Renamed XmlFlowBuilder.resource to "location" for consistency. +* Introduced convenient XmlFlowFactoryBean. +* Introduced flow properties. +* Introduced state properties. +* Introduced transition properties. +* The last transition to execute is now available for access via the RequestContext, allowing states and/or actions to reason on + transition properties. +* Renamed AbstractAction.doExecuteAction to AbstractAction.doExecute for consistency and conciseness. +* Fixed bug where an incoming transaction token was searched for in the last event on the request context. The search now always + uses the originating event of the request context. +* Added support for transition actions. These actions can also be used as transition execution criteria. +* Made Struts FlowExecutionStorage strategy pluggable. +* Refactored ActionStateAction into AnnotatedAction. This has several advantages: it completely decouples Action from ActionState + and avoids error prone lookup of action properties by the action itself. +* Fixed bug in ExternalEvent.searchForParameter(). +* Simplified sample app package structure, collapsing unnecessary packages. +* Added some extra mapping configuration methods to ParameterizableFlowAttributeMapper. +* Added some convenience constructors to EventParameterMapperAction. +* Fixed bug in Struts integration where Errors instance was not exposed via BindingActionForm adapter correctly. +* Birthdate sample now is fully Struts-based, using Struts html form taglibs in the JSPs. +* Added StrutsEvent, for easy access to a Struts ActionForm and ActionMapping from Web Flow action code. +* Added Flow Launcher sample application illustrating different ways of launching flows with input parameters. +* Fixed bug where flowExecution.start(event) was mapping all starting event parameters into flow scope. +* Added getFormObject() and getFormErrors() methods that search both request and flow scope to FormObjectAccessor class. +* Reworked FormObjectAccessor to alias form object and error instances under well-defined names. +* Added getOrCreateAttribute method to Scope class. +* Added assertAttributePresent method to Scope class. +* Added Number Guess sample application, with two number guess games demonstrating flow-scoped history. +* The FormAction now uses the WebDataBinder (introduced in Spring 1.2 RC2) to properly support HTML checkboxes. +* Fixed bug in FlowController and PortletFlowController where the flowLocator property of the default flow execution manager was + not getting initialized. +* Improved build scripts for sample applications. +* Fixed bug in PortletFlowController. As a result it now requires a Portlet session, which it will create if none exists. +* Miscellaneous code cleanup and JavaDoc enhancements. + +Changes in version PR2 (11.4.2005) +---------------------------------- + +* Added sample flow execution tests to PhoneBook sample app. +* ViewDescriptor is now an AttributeSource. +* Added Sell Item sample application, demonstrating a wizard using continuations to preserve use of back/refresh browser buttons. + This sample application also demonstrates the use of OGNL based transitional criteria. +* Introduced TransitionCriteriaCreator, used by a FlowBuilder to create a transition criteria object based on an encoded string + representation. Two TransitionCriteriaCreator implementations are provided out-of-the-box: + * SimpleTransitionCriteriaCreator, the default, that does exact eventId matching or "*" wildcard matching (like in SWF + preview 1). + * OgnlTransitionCriteriaCreator, that parses an OGNL expression expressing a condition to be evaluated in the request context + (e.g. ${lastEvent.id=='success' and flowScope.sale.shipping}). +* Improved Phonebook sample deployment configuration, for easily reusing configuration between test and production environments. +* Introduced MockFlowExecutionListener to support writing unit tests. +* Added Portlet support. This also led to some minor refactoring, e.g. the abstract class ExternalEvent was introduced. +* Improved package dependencies in sandbox classes shipped in webflow-support.jar - mainly package moves from 'util' to 'core'. +* Reimplemented MultiAction using new utility class: DispatchMethodInvoker. +* Added containsProperty() method to ActionStateAction. +* Birthdate sample now demonstrates Spring Web Flow Struts integration. +* Added some convenience methods to AbstractAction: getProperty(), containsProperty(), getActionStateAction() and getActionState(). +* The FlowController no longer requires a pre-existing HTTP session by forcing the "requiresSession" property to true. HTTP + session access is now done via the HttpSessionFlowExecutionStorage, which provides a "createSession" property (which defaults to + true, instructing the creation of a new session if no existing session is found). If you're not using an HTTP session backed + flow execution storage, there is of course no need for an HTTP session at all. +* Introduced convenience XML attribute for action-state: method="foo" +* Renamed the ActionStateAction executeMethodName property to just method for simplicity. +* Renamed FormAction.bindOnNewForm to bindOnSetupForm for consistency. Also renamed FormAction.suppressValidation to + FormAction.validationEnabled to use positive logic. +* Added FormAction.validate(RequestContext, Object, Errors) hook for easy customization of validation logic. +* Added an ActionStateAction validatorMethod property, to specify a specific validation method to be invoked on the configured + Validator used by the FormAction. This allows piecemeal validation to support wizard pages, for example. The validation method + must be of the form public void (Object formObject, Errors errors). +* Reworked flow execution management (via FlowExecutionManager) and introduced pluggable flow execution storage strategies (via + FlowExecutionStorage). This introduces many exciting features and possibilities: + * Integration with Portlets and other frameworks is very trivial now, no need to implement a custom manager. +* You now have the option to store flow execution state in any backing data store, e.g. the HttpSession (the default), a database, + serialized files, ... + * You may now store execution state client side if you want - no HTTP session required. + * You can select to use a continuations based storage strategy, basically turning Spring Web Flow into a continuation driven + system. On top of that, you can choose between client side or server side continuation storage. The continuation storage + strategies also support GZIP compression. +* Renamed TransitionableState.getRequiredTransition() to TransitionableState.transitionFor(). +* Removed TransitionableState.executeTransition() in favor of TransitionableState.transitionFor(context).execute(context). +* Removed hardwired dependency on LinkedHashSet, for use with JDK 1.3. +* Added dispose() method to FlowBuilder to release any resources held by the builder. +* Removed unused methods from AbstractFlowBuilder: attributeMapperId() and eventId(). +* Default "cacheSeconds" for FlowController is now 0 (no caching). +* Miscellaneous code cleanup and JavaDoc enhancements. + +Changes in version PR1 (30.3.2005) +---------------------------------- + +* First public preview release. \ No newline at end of file diff --git a/spring-webflow/docs/reference/.cvsignore b/spring-webflow/docs/reference/.cvsignore new file mode 100644 index 00000000..1f635d87 --- /dev/null +++ b/spring-webflow/docs/reference/.cvsignore @@ -0,0 +1,2 @@ +lib +target diff --git a/spring-webflow/docs/reference/images/admons/blank.png b/spring-webflow/docs/reference/images/admons/blank.png new file mode 100644 index 00000000..764bf4f0 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/blank.png differ diff --git a/spring-webflow/docs/reference/images/admons/caution.gif b/spring-webflow/docs/reference/images/admons/caution.gif new file mode 100644 index 00000000..d9f5e5b1 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/caution.gif differ diff --git a/spring-webflow/docs/reference/images/admons/caution.png b/spring-webflow/docs/reference/images/admons/caution.png new file mode 100644 index 00000000..5b7809ca Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/caution.png differ diff --git a/spring-webflow/docs/reference/images/admons/caution.tif b/spring-webflow/docs/reference/images/admons/caution.tif new file mode 100644 index 00000000..4a282948 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/caution.tif differ diff --git a/spring-webflow/docs/reference/images/admons/draft.png b/spring-webflow/docs/reference/images/admons/draft.png new file mode 100644 index 00000000..0084708c Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/draft.png differ diff --git a/spring-webflow/docs/reference/images/admons/home.gif b/spring-webflow/docs/reference/images/admons/home.gif new file mode 100644 index 00000000..6784f5bb Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/home.gif differ diff --git a/spring-webflow/docs/reference/images/admons/home.png b/spring-webflow/docs/reference/images/admons/home.png new file mode 100644 index 00000000..cbb711de Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/home.png differ diff --git a/spring-webflow/docs/reference/images/admons/important.gif b/spring-webflow/docs/reference/images/admons/important.gif new file mode 100644 index 00000000..6795d9a8 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/important.gif differ diff --git a/spring-webflow/docs/reference/images/admons/important.png b/spring-webflow/docs/reference/images/admons/important.png new file mode 100644 index 00000000..12c90f60 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/important.png differ diff --git a/spring-webflow/docs/reference/images/admons/important.tif b/spring-webflow/docs/reference/images/admons/important.tif new file mode 100644 index 00000000..184de637 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/important.tif differ diff --git a/spring-webflow/docs/reference/images/admons/next.gif b/spring-webflow/docs/reference/images/admons/next.gif new file mode 100644 index 00000000..aa1516e6 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/next.gif differ diff --git a/spring-webflow/docs/reference/images/admons/next.png b/spring-webflow/docs/reference/images/admons/next.png new file mode 100644 index 00000000..45835bf8 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/next.png differ diff --git a/spring-webflow/docs/reference/images/admons/note.gif b/spring-webflow/docs/reference/images/admons/note.gif new file mode 100644 index 00000000..f329d359 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/note.gif differ diff --git a/spring-webflow/docs/reference/images/admons/note.png b/spring-webflow/docs/reference/images/admons/note.png new file mode 100644 index 00000000..d0c3c645 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/note.png differ diff --git a/spring-webflow/docs/reference/images/admons/note.tif b/spring-webflow/docs/reference/images/admons/note.tif new file mode 100644 index 00000000..08644d6b Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/note.tif differ diff --git a/spring-webflow/docs/reference/images/admons/prev.gif b/spring-webflow/docs/reference/images/admons/prev.gif new file mode 100644 index 00000000..64ca8f3c Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/prev.gif differ diff --git a/spring-webflow/docs/reference/images/admons/prev.png b/spring-webflow/docs/reference/images/admons/prev.png new file mode 100644 index 00000000..cf24654f Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/prev.png differ diff --git a/spring-webflow/docs/reference/images/admons/tip.gif b/spring-webflow/docs/reference/images/admons/tip.gif new file mode 100644 index 00000000..823f2b41 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/tip.gif differ diff --git a/spring-webflow/docs/reference/images/admons/tip.png b/spring-webflow/docs/reference/images/admons/tip.png new file mode 100644 index 00000000..5c4aab3b Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/tip.png differ diff --git a/spring-webflow/docs/reference/images/admons/tip.tif b/spring-webflow/docs/reference/images/admons/tip.tif new file mode 100644 index 00000000..4a3d8c75 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/tip.tif differ diff --git a/spring-webflow/docs/reference/images/admons/toc-blank.png b/spring-webflow/docs/reference/images/admons/toc-blank.png new file mode 100644 index 00000000..6ffad17a Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/toc-blank.png differ diff --git a/spring-webflow/docs/reference/images/admons/toc-minus.png b/spring-webflow/docs/reference/images/admons/toc-minus.png new file mode 100644 index 00000000..abbb020c Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/toc-minus.png differ diff --git a/spring-webflow/docs/reference/images/admons/toc-plus.png b/spring-webflow/docs/reference/images/admons/toc-plus.png new file mode 100644 index 00000000..941312ce Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/toc-plus.png differ diff --git a/spring-webflow/docs/reference/images/admons/up.gif b/spring-webflow/docs/reference/images/admons/up.gif new file mode 100644 index 00000000..aabc2d01 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/up.gif differ diff --git a/spring-webflow/docs/reference/images/admons/up.png b/spring-webflow/docs/reference/images/admons/up.png new file mode 100644 index 00000000..07634de2 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/up.png differ diff --git a/spring-webflow/docs/reference/images/admons/warning.gif b/spring-webflow/docs/reference/images/admons/warning.gif new file mode 100644 index 00000000..3adf1912 Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/warning.gif differ diff --git a/spring-webflow/docs/reference/images/admons/warning.png b/spring-webflow/docs/reference/images/admons/warning.png new file mode 100644 index 00000000..1c33db8f Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/warning.png differ diff --git a/spring-webflow/docs/reference/images/admons/warning.tif b/spring-webflow/docs/reference/images/admons/warning.tif new file mode 100644 index 00000000..7b6611ec Binary files /dev/null and b/spring-webflow/docs/reference/images/admons/warning.tif differ diff --git a/spring-webflow/docs/reference/images/callouts/1.gif b/spring-webflow/docs/reference/images/callouts/1.gif new file mode 100644 index 00000000..0d669771 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/1.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/1.png b/spring-webflow/docs/reference/images/callouts/1.png new file mode 100644 index 00000000..7d473430 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/1.png differ diff --git a/spring-webflow/docs/reference/images/callouts/10.gif b/spring-webflow/docs/reference/images/callouts/10.gif new file mode 100644 index 00000000..fb50b06d Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/10.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/10.png b/spring-webflow/docs/reference/images/callouts/10.png new file mode 100644 index 00000000..997bbc82 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/10.png differ diff --git a/spring-webflow/docs/reference/images/callouts/11.gif b/spring-webflow/docs/reference/images/callouts/11.gif new file mode 100644 index 00000000..9f5dba4f Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/11.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/11.png b/spring-webflow/docs/reference/images/callouts/11.png new file mode 100644 index 00000000..ce47dac3 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/11.png differ diff --git a/spring-webflow/docs/reference/images/callouts/12.gif b/spring-webflow/docs/reference/images/callouts/12.gif new file mode 100644 index 00000000..a373d0b4 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/12.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/12.png b/spring-webflow/docs/reference/images/callouts/12.png new file mode 100644 index 00000000..31daf4e2 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/12.png differ diff --git a/spring-webflow/docs/reference/images/callouts/13.gif b/spring-webflow/docs/reference/images/callouts/13.gif new file mode 100644 index 00000000..b00b1637 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/13.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/13.png b/spring-webflow/docs/reference/images/callouts/13.png new file mode 100644 index 00000000..14021a89 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/13.png differ diff --git a/spring-webflow/docs/reference/images/callouts/14.gif b/spring-webflow/docs/reference/images/callouts/14.gif new file mode 100644 index 00000000..6d6642ee Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/14.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/14.png b/spring-webflow/docs/reference/images/callouts/14.png new file mode 100644 index 00000000..64014b75 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/14.png differ diff --git a/spring-webflow/docs/reference/images/callouts/15.gif b/spring-webflow/docs/reference/images/callouts/15.gif new file mode 100644 index 00000000..cdd7072d Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/15.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/15.png b/spring-webflow/docs/reference/images/callouts/15.png new file mode 100644 index 00000000..0d65765f Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/15.png differ diff --git a/spring-webflow/docs/reference/images/callouts/2.gif b/spring-webflow/docs/reference/images/callouts/2.gif new file mode 100644 index 00000000..100ff79f Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/2.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/2.png b/spring-webflow/docs/reference/images/callouts/2.png new file mode 100644 index 00000000..5d09341b Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/2.png differ diff --git a/spring-webflow/docs/reference/images/callouts/3.gif b/spring-webflow/docs/reference/images/callouts/3.gif new file mode 100644 index 00000000..5008ca7d Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/3.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/3.png b/spring-webflow/docs/reference/images/callouts/3.png new file mode 100644 index 00000000..ef7b7004 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/3.png differ diff --git a/spring-webflow/docs/reference/images/callouts/4.gif b/spring-webflow/docs/reference/images/callouts/4.gif new file mode 100644 index 00000000..0e5617d2 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/4.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/4.png b/spring-webflow/docs/reference/images/callouts/4.png new file mode 100644 index 00000000..adb8364e Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/4.png differ diff --git a/spring-webflow/docs/reference/images/callouts/5.gif b/spring-webflow/docs/reference/images/callouts/5.gif new file mode 100644 index 00000000..9bc75ada Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/5.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/5.png b/spring-webflow/docs/reference/images/callouts/5.png new file mode 100644 index 00000000..4d7eb460 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/5.png differ diff --git a/spring-webflow/docs/reference/images/callouts/6.gif b/spring-webflow/docs/reference/images/callouts/6.gif new file mode 100644 index 00000000..d3964070 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/6.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/6.png b/spring-webflow/docs/reference/images/callouts/6.png new file mode 100644 index 00000000..0ba694af Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/6.png differ diff --git a/spring-webflow/docs/reference/images/callouts/7.gif b/spring-webflow/docs/reference/images/callouts/7.gif new file mode 100644 index 00000000..c90b2f3d Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/7.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/7.png b/spring-webflow/docs/reference/images/callouts/7.png new file mode 100644 index 00000000..472e96f8 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/7.png differ diff --git a/spring-webflow/docs/reference/images/callouts/8.gif b/spring-webflow/docs/reference/images/callouts/8.gif new file mode 100644 index 00000000..6fe3287d Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/8.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/8.png b/spring-webflow/docs/reference/images/callouts/8.png new file mode 100644 index 00000000..5e60973c Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/8.png differ diff --git a/spring-webflow/docs/reference/images/callouts/9.gif b/spring-webflow/docs/reference/images/callouts/9.gif new file mode 100644 index 00000000..bc5c8125 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/9.gif differ diff --git a/spring-webflow/docs/reference/images/callouts/9.png b/spring-webflow/docs/reference/images/callouts/9.png new file mode 100644 index 00000000..a0676d26 Binary files /dev/null and b/spring-webflow/docs/reference/images/callouts/9.png differ diff --git a/spring-webflow/docs/reference/readme.txt b/spring-webflow/docs/reference/readme.txt new file mode 100644 index 00000000..f24f3b43 --- /dev/null +++ b/spring-webflow/docs/reference/readme.txt @@ -0,0 +1,20 @@ +This project uses the 'DocBook XSL distribution' for HTML and PDF +generation of project reference documentation. + +This project's build.xml file contains targets to generate the +project reference documentation. + +To generate project documentation, execute one of the following +build targets: + +* doc-all - generate documentation in all formats +* doc-pdf - generate the PDF documentation +* doc-html - generate the HTML documentation +* doc-htmlsingle - generate single page HTML documentation +* doc-clean - clean any output directories for docs + +For generation to complete successfully, you must have first extracted +the .jar libraries contained in this archive: + - http://static.springframework.org/spring/files/docbook-reference-libs.zip +... to ${basedir}/docs/reference. If you have not yet done so, download this file +and unzip the contents of the archive into ${basedir}/docs/reference. \ No newline at end of file diff --git a/spring-webflow/docs/reference/src/flow-definition.xml b/spring-webflow/docs/reference/src/flow-definition.xml new file mode 100644 index 00000000..736fb3a0 --- /dev/null +++ b/spring-webflow/docs/reference/src/flow-definition.xml @@ -0,0 +1,2461 @@ + + + Flow definition + + Introduction + + Spring Web Flow allows developers to build reusable, self-contained controller modules + called flows. A flow defines a user dialog that responds to user events to drive + the execution of application code to complete a business goal. + + + Flows are defined declaratively using a rich domain-specific language (DSL) + tailored to the problem domain of UI flow. Currently, XML and Java-based + forms of this language are provided. + + + This chapter documents Spring Web Flow's core flow definition language. You will + learn the core domain constructs of the system and how those constructs are + representable in an externalized XML form. + + + + FlowDefinition + + A flow definition is a instance of org.springframework.webflow.definition.FlowDefinition. + This is the central domain artifact representing the definition of a user dialog or task. + + + A flow definition consists of a set of one or more states, where each state defines a step in + the flow that when entered executes a behavior. What behavior is executed is + a function of the state's type and configuration. The outcome of a state's + execution, called an event, is used by the flow to drive a state transition. + + + Exactly one of a flow's states is the startState + that defines the starting point of the flow. Optionally, a flow can have one or more end states + defining the ending points of the flow. + + + An example definition of a simple flow to carry out a search process is shown graphically below: + + + + + + + + + + Search Flow + + + + The default FlowDefinition implementation in Spring Web Flow is + org.springframework.webflow.engine.Flow. Its configurable properties are + summarized below: + + + Flow properties + + + + + + + + Property name + Description + Cardinality + Default value + + + + + id + The identifier of the flow definition, typically unique to all other flows of the application. + + 1 + + + + attributes + Additional custom attributes about the flow. + + 0..* + + None + + + states + The steps of the flow. + + 1..* + + + + startState + The starting point of the flow. + + 1 + + + + variables + The set of flow variables to create each time an execution of the flow is started. + + 0..* + + Empty + + + inputMapper + + The service responsible for mapping flow input provided by a caller each time an + execution of the flow is started. + + + 0..1 + + Null + + + startActions + The list of actions to execute each time an execution of the flow is started. + + 0..* + + Empty + + + endActions + The list of actions to execute each time an execution of the flow ends. + + 0..* + + Empty + + + outputMapper + + The service responsible for mapping flow output to expose to the caller each time an + execution of the flow ends. + + + 0..1 + + Null + + + globalTransitions + The set of transitions shared by all states of the flow. + + 0..* + + Empty + + + exceptionHandlers + An ordered set of handlers to be applied when an exception is thrown within a state of the flow. + + 0..* + + Empty + + + inlineFlows + A set of inner flows that will be called as subflows; these flows are locally scoped to the outer flow. + + 0..* + + Empty + + + +
+ + Below is a high level example of how these properties can be configured in XML form + or directly in Java code. + + + XML-based Flow template + + <?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-1.0.xsd"> + + <attribute .../> + + <var .../> + + <input-mapper .../> + + <start-actions> + ... + </start-actions> + + <start-state idref="yourStartingStateId"/> + + <-- your state definitions go here --> + + <end-actions> + ... + </end-actions> + + <output-mapper .../> + + <global-transitions> + ... + </global-transitions> + + <exception-handler .../> + + <inline-flow> + ... + </inline-flow> + + </flow> + + + + Java Flow API example + + Flow flow = new Flow("id"); + flow.getAttributeMap().put("name", "value"); + flow.addState(...); + flow.setStartState("startingPoint"); + flow.addVariable(...); + flow.setInputMapper(...); + flow.getStartActionList().add(...); + flow.getEndActionList().add(...); + flow.setOutputMapper(...); + flow.getGlobalTransitionSet().add(...); + flow.getExceptionHandlerSet().add(...); + flow.addInlineFlow(...); + + + A Flow is typically built by a FlowBuilder rather than assembled + by hand. The flow building subsystem is contained within the + org.springframework.webflow.engine.builder package. + The XML Flow Builder and spring-webflow.xsd schema are located + within the org.springframework.webflow.engine.builder.xml package. + The XML-based format is the most popular way to define flows. + + +
+ + StateDefinition + + A StateDefinition defines the behavior for a step of a FlowDefinition. + The base implementation class for all Flow state types is org.springframework.webflow.engine.State. + This abstract class defines common properties applicable to all state types, which include: + + + State properties + + + + + + + + Property name + Description + Cardinality + Default value + + + + + id + The id of the state, unique to its containing flow definition. + + 1 + + + + owner + The owning flow definition. + + 1 + + + + attributes + Additional custom attributes about the state. + + 0..* + + None + + + entryActions + The list of actions to execute each time the state is entered. + + 0..* + + Empty + + + exceptionHandlers + An ordered set of handlers to be invoked when an exception is thrown within the state. + + 0..* + + Empty + + + +
+
+ + Transitionable State + + A central subclass of State is org.springframework.webflow.TransitionableState. + This abstract class defines common properties applicable to all state types that execute + transitions to other states in response to events. These properties include: + + + TransitionableState properties + + + + + + + + Property name + Description + Cardinality + Default value + + + + + transitions + The eligible paths out of this state. + + 1..* + + + + exitActions + The list of actions to execute each time this state is exited. + + 0..* + + Empty + + + +
+ + Below is a mock flow definition snippet showing how properties may be configured for + a TransitionableState in XML and in Java code: + + + XML-based state template + + <?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-1.0.xsd"> + + <start-state idref="myStateId"/> + + <xxx-state id="myStateId"> + <attribute name="..." value="..."/> + + <entry-actions> + ... + </entry-actions> + + <transition on="..." to="..."/> + <transition on-exception="..." to="..."/> + + <exit-actions> + ... + </exit-actions> + + <exception-handler .../> + </xxx-state> + + </flow> + + + + Java state API example + + Flow flow = new Flow("id"); + TransitionableState state = new XXXState(flow, "stateId"); + state.getAttributeMap().put("name", "value"); + state.getEntryActionList().add(...); + state.getTransitionSet().add(...); + state.getExitActionList().add(...); + + + A State is typically constructed by a FlowArtifactFactory, used by + a FlowBuilder during flow assembly. The flow building subsystem is contained within the + org.springframework.webflow.engine.builder package. + + +
+ + TransitionDefinition + + A transition takes a flow from one state to another, defining a path through the flow. + This is modeled using a TransitionDefinition. + + + Recall that all TransitionableStates have a set of one or more transitions, each defining a + path to another state in the flow (or a recursive path back to the same state). + When a transitionable state is entered it executes a behavior. + For example, a transitionable state called "Display Form" may display a form to the user + and wait for user input. The outcome of the state's execution, called an event, is used to drive execution of + one of the state's transitions. For example, the user may press the form submit button which + signals a submit event that matches the transition to + the "Process Submit" state. + + + This event-driven transition execution process is shown graphically below: + + + + + + + + + + Transition execution + + + + The transition definition implementation is defined by an instance of + org.springframework.webflow.engine.Transition. + Its properties are summarized below: + + + Transition properties + + + + + + + + Property name + Description + Cardinality + Default value + + + + + attributes + Additional attributes describing the transition. + + 0..* + + None + + + matchingCriteria + The strategy that determines if the transition matches on an event occurrence. + + 1 + + Always matches + + + executionCriteria + The strategy that determines if the transition, once matched, is allowed to execute. + + 1 + + Always allowed + + + targetStateResolver + + The strategy that resolves the target state of the transition. + Most transitions always resolve to the same target state. + This strategy allows for dynamic resolution. + + + 1 + + + + + +
+ + Below is a high-level example of how a Transition can be configured in XML form + or directly in Java code. + + + Transition XML template + + <transition on="event" to="targetState"> + <attribute ... /> + <action ... /> + </transition> + + + + Transition Java API example + + Transition transition = new Transition("targetState"); + transition.getAttributeMap().put("name", "value"); + transition.setMatchingCriteria(new EventIdTransitionCriteria("event")); + transition.setExecutionCriteria(...); + + + + Action transition execution criteria + + In the XML transition template above note the support for the action element within the transition element. + + + A transition may be configured with one or more actions that execute before the transition itself + executes as executionCriteria. If one or more of these + actions do not complete successfully the transition will not be allowed. + This action transition criteria makes it possible to execute arbitrary logic + after a transition is matched but before it is executed. This is useful when you want to execute + event post-processing logic. A good example is executing form data binding and validation behavior + after a form submit event. + + + + Dynamic transitions + + A transition's target state resolver can be configured to dynamically calculate the + target state. For example: + + + <transition on="back" to="${flowScope.lastStateId}" /> + + + This will transition the flow to the state resolved by evaluating the + flowScope.lastStateId expression. + + + + Global transitions + + As outlined, one or more transitions are added to all TransitionableState types, + attached at the state-level. Optionally, transitions may also be added at the + flow-level where they are shared by all states. These shared + transitions are called global transitions. + + + When an event is signaled in a transitionable state the state will first try and + match one of its own transitions. If there is no match at the state level the set of + global transitions will be tested. If there still is no match + a NoMatchingTransitionException will be thrown. + + + Global transitions are useful in situations where many states of the flow share + the same transitional criteria. For example, consider a navigation menu that displays + alongside each view of a flow. Logic to process navigation menu events is needed + by all view states. This is the problem global transitions are designed to solve. + + + Global transitions - XML example + + The following example shows transitions defined at the state level, as well as + global transitions defined at the flow level. + + + <?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-1.0.xsd"> + + <start-state idref="state1"/> + + <xxx-state id="state1"> + <transition on="localEvent1" to="state2"/> + </xxx-state> + + <xxx-state id="state2"> + <transition on="localEvent1" to="state1"/> + </xxx-state> + + <global-transitions> + <transition on="globalEvent1" to="state1"/> + <transition on="globalEvent2" to="state2"/> + </global-transitions> + + </flow> + + + In this mock example state1 defines one transition and also inherits + the two others defined within the global-transitions element. + Any other states defined within this flow would also inherit those global + transitions. + + + This example is shown graphically below: + + + + + + + + + + Global transitions + + + + + + Transition executing state exception handlers + + The <transition/> element contains an exclusive on-exception + attribute used to specify an exception-based criteria for transition execution. This allows you to + transition the flow to another state on the occurrence of an exception. + + + Exception handling - XML example + + The following example shows a transition that is applied as a state exception handler: + + + <?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-1.0.xsd"> + + <start-state idref="state1"/> + + <xxx-state id="state1"> + <transition on="event1" to="state2"/> + <transition on-exception="example.MyBusinessException" to="state3"/> + </xxx-state> + + ... + + </flow> + + + In this example state1 defines one transition and an exception handler + which executes a transition to state3 if a MyBusinessException + is thrown within the state. + + + +
+ + Concrete state types + + Spring Web Flow has five (5) built-in concrete state types, all contained within the + org.springframework.webflow.engine package. These states execute common + controller behaviors including: + + allowing the user to participate in a flow (ViewState) + executing business application code (ActionState) + making a flow routing decision (DecisionState) + spawning another flow as a subflow (SubflowState) + terminating a flow (EndState) + + + + Each of these state types, with the exception of EndState, is transitionable. + This hierarchy is illustrated below: + + + + + + + + + + FlowDefinition class diagram + + + + As you will see, with these five basic state types you can develop rich controller modules. + + + ViewState + + When entered a view state allows the user (or other external client) to participate + in a flow. This participation process goes as follows: + + + + The entered view state makes a org.springframework.webflow.execution.ViewSelection + that represents a logical response to issue to the caller. + + + + + The flow execution 'pauses' in this state, and control is returned to the calling + system. + + + + + The calling system uses the returned ViewSelection to present a + suitable interface (or other response) to the user. + + + + + After some 'think time' the user signals an input event to resume the flow execution + from the 'paused' point. + + + + + + Spring Web Flow gives you full control over the view selection process and, on resume, + how a view state responds to a user input event. It's important to understand that Spring Web Flow is not + responsible for response rendering--as a controller, a flow makes a logical view selection when user input is required, + where a view selection serves as a response instruction. It is up to the calling system to interpret that instruction to issue a + response suitable for the environment in which the flow is executing. + + + The properties of a org.springframework.webflow.engine.ViewState are summarized below: + + + ViewState properties + + + + + + + + Property name + Description + Cardinality + Default value + + + + + viewSelector + The strategy that makes the view selection when this state is entered. + + 0..1 + + Null + + + renderActions + + The list of actions to execute each time a renderable view selection is made. + Allows for execution of pre-render logic. + + + 0..* + + Empty + + + +
+ + The org.springframework.webflow.execution.ViewSelection base class is abstract, + acting as a marker indicating a response should be issued to the client interacting + with the flow. Concrete subtypes exist for each of the supported response types. + These response types are summarized below: + + + Concrete ViewSelection types + + + + + + Type + Description + + + + + ApplicationView + Requests the rendering of a local, internal application view resource such as a JSP, Velocity, or Freemarker template. + + + FlowExecutionRedirect + + Requests a redirect back to the ViewState at a unique flow execution URL. + When this URL is accessed on subsequent requests an ApplicationView will be reconstituted and rendered. + The URL is refreshable while the flow execution remains active. + + + Multiple flow execution URLs may be generated for a single logical user conversation. + In that case each flow execution URL provides access to the conversation + from a previous point (ViewState). Accessing the URL refreshes the execution + from that point. + + + + + + FlowDefinitionRedirect + + Requests a redirect that launches an entirely new flow execution. Used to support + redirect to flow (flow chaining) and restart flow use cases. + + + + ExternalRedirect + + Requests a redirect to an arbitrary external URL, typically used to inteface + with an external system. + + + + NullView + + Requests that no response be issued; for use in corner cases where the flow itself has already + issued the response. + + + + +
+ + ViewSelector + + The creational strategy responsible for making a ViewSelection when an ViewState is entered + is org.springframework.webflow.engine.ViewSelector. This provides a plugin-point for customizing how + a response instruction is constructed. + + + Four ViewSelector implementations are provided with Spring Web Flow: + + + ViewSelector implementations + + + + + + Implementation + Description + + + + + ApplicationViewSelector + + Returns an ApplicationView referencing a logical viewName to render and containing a + modelMap with the application data needed by the rendering process + (by default, this map contains the union of the data scopes such flow, flash, and request scope). + Supports setting a redirect flag that triggers + a browser redirect to the selected view using a FlowExecutionRedirect. + The default implementation. + + + + FlowDefinitionRedirectSelector + + Returns a FlowDefinitionRedirect with a flowId and executionInput map requesting + the launch of an entirely new flow execution (an instance of the FlowDefinition identified by the flowId). + Useful for redirect after flow completion, where one flow ending should trigger + the start of another flow independently. + + + + ExternalRedirectSelector + + Returns an ExternalRedirect that triggers a browser redirect to an abitrary external URL. + Mainly used by end states to redirect to external systems after flow completion, + but can also be used by view states to interface with an external system that may + call back into the flow execution at a later point. + + + + NullViewSelector + + Returns an NullView indicating that no response should be issued. + + + + +
+
+ + ViewState class diagram + + The class diagram below shows the ViewState and the associated types used to carry + out the view selection process: + + + + + + + + + + ViewState class diagram + + + + + ViewState XML - application view selection + + The following example shows a view-state definition in XML that makes an application view + selection when entered, selecting the searchForm view for display and, on resume, responding to + two possible user input events (submit and cancel) in different ways: + + + <?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-1.0.xsd"> + + <start-state idref="displaySearchForm"/> + + <view-state id="displaySearchForm" view="searchForm"> + <transition on="submit" to="processFormSubmission"/> + <transition on="cancel" to="processCancellation"/> + </view-state> + + ... + + </flow> + + + View name expressions may also be specified for the view attribute to + achieve runtime view name calculation. + For example, view="${requestScope.calculatedViewName}". + + + + ViewState API - application view selection + + The following example shows the equivalent view state definition using + the FlowBuilder API: + + + public class SearchFlowBuilder extends AbstractFlowBuilder { + public void buildStates() { + addViewState("displaySearchForm", "searchForm", + new Transition[] { + transition(on("submit"), to("processFormSubmission")), + transition(on("cancel"), to("processFormCancellation")) + } + ); + ... + } + } + + + + ViewState XML - flow execution redirect + + The following example illustrates a view-state definition in XML that makes an + flow execution redirect selection when entered, redirecting to the + yourList view for display. + + + <?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-1.0.xsd"> + + <start-state idref="displayList"/> + + <view-state id="displayList" view="redirect:yourList"> + <transition on="add" to="addListItem"/> + </view-state> + + ... + + </flow> + + + This example is called a flow execution redirect because the application view selected + is rendered only after a redirect to the flow execution. The redirect request is sent to a + URL that refreshes the flow execution paused in the displayList + view state. Refresh then triggers the rendering of the yourList application view + on the next request into the server. + + + POST+REDIRECT+GET in Spring Web Flow + + The above example is one way to achieve the POST+REDIRECT+GET pattern in Spring Web Flow. + When the redirect is performed, the GET request issued hits a stable flow execution URL + which remains active for the duration of the conversation. This URL may be freely refreshed. + Browser navigational buttons may be used freely without browser warnings. + + + Later in this document the execution attribute alwaysRedirectOnPause is discussed, + which enforces this pattern by default. In that case each time a view state is entered + a redirect is always performed--automatically. + + + + + ViewState API - flow execution redirect + + The following example shows the equivalent view state definition using + the FlowBuilder API: + + + public class SearchFlowBuilder extends AbstractFlowBuilder { + public void buildStates() { + addViewState("displayList", viewSelector("redirect:yourView"), + transition(on("add"), to("addListItem")) + ); + ... + } + } + + + + ViewState XML - null view + + The following example illustrates a view-state definition in XML that makes a + null view selection when entered, which causes no additional response to be issued. + + + <?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-1.0.xsd"> + + <start-state idref="displayPdf"/> + + <view-state id="displayPdf"> + <render-actions> + <action bean="pdfWriter" method="write"/> + </render-actions> + </view-state> + + ... + + </flow> + + + + FlowDefinitionRedirect and ExternalRedirect + + The FlowDefinitionRedirect and ExternalRedirect are not + normally used with a view state. Instead they're used in an end state to continue with another, + independent flow or to redirect to an external URL. Examples are provided in the discussion of the + end state. + + + + ViewState XML - form state behavior + + The following example illustrates a view-state definition in XML that encapsulates + typical "form state" behavior. + + + Consider the requirements of typical input forms. Most forms require pre-render or + setup logic to execute before the form is displayed. For example, such logic might + load the backing form object from the database, install formatters for formatting + form field values, and pull in supporting form data needed to populate drop-down menus. + + + In addition, most forms require post-back or submission logic + to execute when the form is submitted. This logic typically involves binding form input to the + backing form object and performing type conversion and data validation. + + + This "form state" behavior of form setup, display, and post-back is handled elegantly in Spring Web Flow + by the capabilities of the view-state construct. See below: + + + <?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-1.0.xsd"> + + <start-state idref="displayForm"/> + + <view-state id="displayForm" view="form"> + <render-actions> + <action bean="formAction" method="setupForm"/> + <action bean="formAction" method="loadFormReferenceData"/> + </render-actions> + <transition on="submit" to="saveForm"> + <action bean="formAction" method="bindAndValidate"/> + </transition> + </view-state> + + ... + + </flow> + + + This reads "when this flow starts enter the displayForm state + to execute the setupForm and loadFormReferenceData methods + before rendering the form view. On submit, + transition to the saveForm state if the bindAndValidate method executes successfully." + + +
+ + ActionState + + When entered, an action state executes business application code, then responds to the result of that + execution by deciding what state in the flow to enter next. Specifically: + + + + The entered action state executes an ordered list of one or more + org.springframework.webflow.execution.Action + instances. This Action interface is the central abstraction that + encapsulates the execution of a logical unit of application code. + + + + + The state determines if the outcome of the first action's execution matches a + transition. If there is a match, the transition is executed. If there is no match, + the next action in the list is executed. This process continues until a transition is + matched or the list of actions is exhausted. + + + + + + Spring Web Flow gives you full control over implementing your own actions and configuring when they should be invoked + within the lifecycle of a flow. The system can also automatically adapt methods on + your existing application objects (POJOs) to the Action interface in a non-invasive manner. + This means in many cases you can implement your flows without needing to develop custom glue code to bind SWF + to your service layer operations. + + + The properties of a org.springframework.webflow.engine.ActionState are summarized below: + + + ActionState properties + + + + + + + + Property name + Description + Cardinality + Default value + + + + + actions + The ordered list of actions to execute when the state is entered. + + 1..* + + + + + +
+ + Action execution points + + As outlined, the ActionState is the dedicated state type for invoking one + or more actions and responding to their result to drive a state transition. There are + also other points within the lifecycle of a flow where a chain of actions can be executed. + At all of these points the only requirement is that these actions implement the central + org.springframework.webflow.execution.Action interface. + + + + Other points in a Flow where an Action can be executed and how those points + can be defined in a XML-based Flow definition. + + + + + + + + Point + Description + XML Configuration Element + + + + + on flow start + + Each time a new flow session starts. + + + A flow's <start-actions/> + + + + on state entry + + Each time a state enters. + + + A state's <entry-actions/> + + + + on transition + + Each time a state transition is matched but before it is executed. + + + A transition <action/> + + + + on state exit + + Each time a transitionable state exits. + + + A transitionable state's <exit-actions/> + + + + before view rendering + + Each time a renderable view selection is made. + + + A view state's <render-actions/> + + + + on flow end + + Each time a flow session terminates. + + + A flow's <end-actions/> + + + + +
+ + + The above other points in a flow where actions may be executed do not + allow you to execute a state transition in response to the action result event. + If you need such flow control you must execute the action from within an action state. + + +
+ + Action attributes + + An Action may be annotated with attributes by wrapping the Action + in a decorator, an instance of org.springframework.webflow.engine.AnnotatedAction. + These attributes may provide descriptive characteristics, or may be used to affect + the action's execution in a specific usage context. + + + Support for setting several common attributes is provided for convenience. These + include: + + + Common Action attributes + + + + + + Attribute name + Description + + + + + caption + + A short description about the action, suitable for display as a tooltip. + + + + description + + A long description about the action, suitable for display in a text box. + + + + name + + The name of the action, used to qualify the action's result event. + For example, an Action named placeOrder that returns success + would be assigned a result event identified by placeOrder.success. + This allows you to distinguish logical execution outcomes by action, useful when + invoking multiple actions as part of a chain. + + + + method + + The name of the target method on the Action instance to invoke to carry out execution. + This facilitates multiple action methods per Action instance, + supported by the org.springframework.webflow.action.MultiAction. + + + + +
+
+ + ActionState class diagram + + The class diagram below shows the ActionState and the associated types used to carry + out the action execution process: + + + + + + + + + + ActionState class diagram + + + + + ActionState XML - simple action execution + + The following example constructs an ActionState definition from + XML that executes a single action when entered and then responds to its result: + + <?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-1.0.xsd"> + + <start-state idref="executeSearch"/> + + <action-state id="executeSearch"> + <action bean="searchAction"/> + <transition on="success" to="displayResults"/> + </action-state> + + ... + + </flow> + + This state definition reads "when the executeSearch + state is entered, execute the searchAction. On successful execution, + transition to the displayResults state." + + + The binding between the searchAction id and an + Action implementation is made at Flow build time + by querying a service locator, typically a Spring BeanFactory. For example: + + <beans> + <bean id="searchAction" class="example.webflow.SearchAction"/> + </beans> + + ... binds the searchAction action identifier to a singleton instance of the + example.webflow.SearchAction class. + + + A simple SearchAction implementation might look like this: + + public class SearchAction implements Action { + private SearchService searchService; + + public SearchAction(SearchService searchService) { + this.searchService = searchService; + } + + public Event execute(RequestContext context) { + // lookup the search criteria in "flow scope" + SearchCriteria criteria = + (SearchCriteria)context.getFlowScope().get("criteria"); + + // execute the search + Collection results = searchService.executeSearch(criteria); + + // set the results in "request scope" + context.getRequestScope().put("results", results); + + // return "success" + return new Event(this, "success"); + } + } + + + + + ActionState API - standard action + + The following example constructs the equivalent action state definition using + the FlowBuilder API: + + + public class SearchFlowBuilder extends AbstractFlowBuilder { + public void buildStates() { + ... + addActionState("executeSearch", action("searchAction"), + transition(on("success"), to("displayResults"))); + ... + } + } + + + + ActionState XML - multi action + + The next example constructs an ActionState definition from XML that + executes a single action method on a org.springframework.webflow.action.MultiAction + and then responds to its result: + + <?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-1.0.xsd"> + + <start-state idref="executeSearch"/> + + <action-state id="executeSearch"> + <action bean="searchAction" method="executeSearch"/> + <transition on="success" to="displayResults"/> + </action-state> + + ... + + </flow> + + This state definition reads "when the executeSearch + state is entered, call the executeSearch method on the + searchFlowAction. On successful execution, + transition to the displayResults state." + + + A SearchAction implementation containing multiple action methods + might look like this: + + public class SearchAction extends MultiAction { + private SearchService searchService; + + public SearchAction(SearchService searchService) { + this.searchService = searchService; + } + + public Event executeSearch(RequestContext context) { + // lookup the search criteria in "flow scope" + SearchCriteria criteria = + (SearchCriteria)context.getFlowScope().get("criteria"); + + // execute the search + Collection results = searchService.executeSearch(criteria); + + // set the results in "request scope" + context.getRequestScope().put("results", results); + + // return "success" + return success(); + } + + public Event someOtherRelatedActionMethod(RequestContext context) { + ... + return success(); + } + + public Event yetAnotherRelatedActionMethod(RequestContext context) { + ... + return success(); + } + } + + As you can see, this allows you to define one to many action methods per Action class. + With this approach, there are two requirements: + + + + Your Action class must extend from org.springframework.webflow.MultiAction, or + another class that extends from MultiAction. The multi action cares + for the action method dispatch that is based on the value of the method + property. + + + + + Each action method must conform to the signature illustrated above: public Event ${method}(RequestContext) { ... } + + + + + + MultiActions are useful for centralizing command logic on a per-flow definition basis, as + a flow definition typically carries out execution of a single application use case. + + + + ActionState API - multi action + + The following example constructs the equivalent action state definition using + the FlowBuilder API: + + + public class SearchFlowBuilder extends AbstractFlowBuilder { + public void buildStates() { + ... + addActionState("executeSearch", invoke("executeSearch", action("searchAction")), + transition(on("success"), to("displayResults"))); + ... + } + } + + + + ActionState XML - bean action + + The next example constructs an ActionState definition from XML that + executes a single method on a Plain Old Java Object (POJO) and then responds to the result: + + <?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-1.0.xsd"> + + <start-state idref="executeSearch"/> + + <action-state id="executeSearch"> + <bean-action bean="searchService" method="executeSearch"> + <method-arguments> + <argument expression="${flowScope.criteria}"/> + </method-arguments> + <method-result name="results"/> + </bean-action> + <transition on="success" to="displayResults"/> + </action-state> + + ... + + </flow> + + This state definition reads "when the executeSearch + state is entered, call the executeSearch method on the + searchService passing it the object indexed by name criteria + in flowScope. On successful execution, expose the method + return value in the default scope (request) under the name results + and transition to the displayResults state." + + + In this example the referenced bean searchService would be + your application object, typically a transactional + business service. Such a service implementation must have defined the + the Collection executeSearch(SearchCriteria) method, + typically by implementing a service interface: + + + public interface SearchService { + public Collection executeSearch(SearchCriteria criteria); + } + + + With this approach there are no requirements on the signature of the methods that carry out + action execution, nor is there any requirement to extend from a Web Flow specific base class. + Basically, you are not required to write a custom Action implementation at all--you + simply instruct Spring Web Flow to call your business methods directly. The need + for custom "glue code" to bind your web-tier to your middle-tier is eliminated. + + + Spring Web Flow achieves this by automatically adapting the method on your existing + application object to the Action interface and caring for + exposing any return value in the correct scope. + + + This adaption process is shown graphically below: + + + + + + + + + + Bean->Action adapter + + + + + ActionState XML - decision bean action + + The following example constructs an ActionState from + XML that executes an action whose execution result forms the basis for the transition decision: + + + <?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-1.0.xsd"> + + ... + + <action-state id="shippingRequired"> + <bean-action bean="shippingService" method="isShippingRequired"> + <method-arguments> + <argument expression="${flowScope.purchase}"/> + </method-arguments> + </bean-action> + <transition on="yes" to="enterShippingDetails"/> + <transition on="no" to="placeOrder"/> + </action-state> + + ... + + </flow> + + + This state definition reads "if the isShippingRequired method on the + shippingService returns true, transition to the enterShippingDetails + state, otherwise transition to the placeOrder state." + + + + Note how the boolean return value of the isShippingRequired method is + converted to the event identifiers yes or no. + + + + This conversion process is handled by the action adapter responsible for adapting the method on your + application object to the org.springframework.webflow.execution.Action interface. + By default, this adapter applies a number of rules for creating a result event from a method return value. + + + These conversion rules are: + + + Default method return value to Event conversion rules + + + + + + Return type + Event identifier + + + + + boolean + yes or no + + + java.lang.Enum + this.name() + + + org.springframework.core.enum.LabeledEnum + this.getLabel() + + + org.springframework.webflow.execution.Event + this.getId() + + + java.lang.String + the string + + + any other type + success + + + +
+ + You may customize these default conversion policies by setting a custom ResultEventFactory + instance on the bean invoking action performing the adaption. Consult the JavaDoc documentation for + more details on how to do this. + +
+ + ActionState XML - decision bean action with enum return value + + The following example constructs an ActionState from + XML that executes a action that invokes a method on an application object that + returns a java.lang.Enum: + + + <?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-1.0.xsd"> + + ... + + <action-state id="shippingRequired"> + <bean-action bean="shippingService" method="calculateShippingMethod"/> + <method-arguments> + <argument expression="${flowScope.order}"/> + </method-arguments> + </bean-action> + <transition on="BASIC" to="enterBasicShippingDetails"/> + <transition on="EXPRESS" to="enterExpressShippingDetails"/> + <transition on="NONE" to="placeOrder"/> + </action-state> + + ... + + </flow> + + + This state definition reads "if the calculateShippingMethod method on the + shippingService returns BASIC for the current order, transition to the enterBasicShippingDetails + state. If the return value is EXPRESS transition to the enterExpressShippingDetails state. + If the return value is NONE transition to the placeOrder state." + + + + ActionState XML - evaluate action + + The following example constructs an ActionState from + XML that executes a action that evaluates an expression against the + flow request context and exposes the evaluation result: + + + <?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-1.0.xsd"> + + + <action-state id="getNextInterviewQuestion"> + <evaluate-action expression="flowScope.interview.nextQuestion()"/> + <evaluation-result name="question"/> + </evaluate-action> + <transition on="success" to="displayQuestion"/> + </action-state> + + </flow> + + + This state definition reads "evaluate the flowScope.interview.nextQuestion() expression + and expose the result under name question in the default scope." + + + The expression can evaluate any object traversable from the flow's + org.springframework.webflow.execution.RequestContext. This example expression evaluates the + nextQuestion method on the interview + business object in flow scope. + + + + ActionState XML - set action + + The next example constructs an ActionState from + XML that executes an action on a success transition that sets an attribute in "flash scope": + + + <?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-1.0.xsd"> + + <view-state id="selectFile" view="fileUploadForm"> + <transition on="submit" to="uploadFile"/> + </view-state> + + <action-state id="uploadFile"> + <action bean="uploadAction" method="uploadFile"/> + <transition on="success" to="selectFile"> + <set attribute="fileUploaded" scope="flash" value="true"/> + </transition> + </action-state> + + </flow> + + + This flow definition reads "display the fileUploadForm. + On form submit invoke the uploadFile method + on the uploadAction. On success allow the user + to select another file to upload. Report that the last file was uploaded successfully by + setting the fileUploaded attribute in flash scope to + true. + + + + Flash scoped attributes are preserved until the next user event is signaled into + the flow execution. In this example this means the fileUploaded + attribute is preserved across a redirect to the selectFile + view state and any subsequent browser refreshes. Only when the submit + event is signaled will the flash scope be cleared. + + + + + When to use which kind of action? + + Simple action, Multi action, bean action, evaluate action, set? When to use one or the other? + + + + Action implementation usage guidelines + + + + + + + Action type + Usage scenario + + + + + Simple (extends AbstractAction) + + You have a specialized behavior that stands on its own; + for creating lightweight stubs or mocks for testing purposes. + + + + MultiAction + + To group related command logic together. Particularly + useful for when there are multiple related behaviors + called by a flow. + + + + Bean action + + When the logical behavior maps well to a method call on a service + layer bean. When there is no "special" or exotic glue code + required. + + + + + EvaluateAction + + + When you need to invoke a bean in flow scope or evaluate + any other flow expression. + + + + + SetAction + + + When you need to set an attribute in flow or other scope + during the course of flow execution. + + + + +
+
+
+ + DecisionState + + When entered, a decision state makes a flow routing decision. This process consists of: + + + + Evaluating one or more boolean expressions against the executing flow to decide + what state to transition to next. + + + + + + The properties of a org.springframework.webflow.engine.DecisionState are summarized below: + + + DecisionState properties + + + + + + + + Property name + Description + Cardinality + Default value + + + + + transitions (inherited from TransitionableState) + + The transitions that are evaluated on an event occurrence that + forms the basis for the decision. + + + 1..* + + + + +
+ + DecisionState XML - expression evaluation + + The following example constructs a DecisionState from + XML that evalutes a boolean expression to determine what transition + to execute: + + + <?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-1.0.xsd"> + ... + + <decision-state id="shippingRequired"> + <if test="${flowScope.order.needsShipping}" then="enterShippingDetails" else="placeOrder"/> + </decision-state> + + ... + + </flow> + + + This state definition reads "if the needsShipping property on the + order object in flow scope is true, transition to the enterShippingDetails + state, otherwise transition to the placeOrder state." + + + + Caution: flow definitions should not be vehicles for + business logic. In this case the decision made was controller logic, reasoning on a + pre-calculated value to decide what step of the flow to transition to next. That is the kind of logic that + should be in a flow definition. In contrast, having the state itself embed + the business rule defining how shipping status is calculated is a misuse. + Instead, push such a calculation into application code where it belongs and instruct + the flow to invoke that code using an action. + + + +
+ + SubflowState + + When entered, a subflow state spawns another flow as a subflow. + + + Recall that a flow is a reusable, self-contained controller module. The ability for one flow to call another flow + gives you the ability to compose independent modules together to create complex controller workflows. Any flow can be used as subflow + by any other flow, and there is a well-defined contract in play. Specifically: + + + + + + A Flow is an instance of org.springframework.webflow.engine.Flow. + + + + + A newly launched flow can be passed input attributes which it may choose + to map into its own local scope. + + + + + An ending flow can return output attributes. If the ended flow was launched as a subflow, + the resuming parent flow may choose to map these output attributes into its own scope. + + + + + + It is helpful to think of the process of calling a flow like calling a Java method. Flows can + be passed input arguments and can produce return values just like methods can. Flows are more powerful because + they are potentially long-running, as they can span more than one request into the server. + + + The properties of a org.springframework.webflow.engine.SubflowState are summarized below: + + + SubflowState properties + + + + + + + + Property name + Description + Cardinality + Default value + + + + + subflow + + The definition of the flow to be spawned as a subflow. + + + 1 + + + + + attributeMapper + + The strategy responsible for mapping input attributes to the subflow and + mapping output attributes from the subflow. + + + 0..* + + Null + + + +
+ + When a SubflowState is entered, the following behavior occurs: + + + + The state first messages its attributeMapper, an instance of + org.springframework.webflow.engine.FlowAttributeMapper, + to prepare a Map of input attributes to pass to the subflow. + + + + + The subflow is spawned, passing the input attributes. When this happens, + the parent flow suspends itself in the subflow state until + the subflow ends. + + + + + When the subflow ends, a result event is returned describing the flow outcome + that occurred. The parent flow resumes back in the subflow state. + + + + + The resumed subflow state messages its attributeMapper to + map any output attributes returned by the subflow into flow scope, if necessary. + + + + + Finally, the resumed subflow state responds to the result event returned by the ended subflow + by matching and executing a state transition. + + + + + + The constructs used in spawning a flow as a subflow are shown graphically below: + + + + + + + + + + SubflowState class diagram + + + + SubflowState XML - with input attribute + + The following example constructs an SubflowState from + XML that spawns a shipping subflow: + + + <?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-1.0.xsd"> + + ... + + <subflow-state id="enterShippingDetails" flow="shipping"> + <attribute-mapper> + <input-mapper> + <mapping source="flowScope.order.shipping" target="shipping"/> + </input-mapper> + </attribute-mapper> + <transition on="finish" to="placeOrder"/> + </subflow-state> + + ... + + </flow> + + + This subflow state definition reads "spawn the shipping flow + and pass it the value of the shipping property on the order + object in flow scope as an input attribute with the name shipping. + When the shipping flow ends, respond to the finish + result event by transitioning to the placeOrder state." + + + + The inner structure and behavior of the shipping flow is fully encapsulated within + its own flow definition. A flow calling another flow as a subflow can pass that flow input + and capture its output, but it cannot see inside it. Flows are black boxes. + Because any flow can be used as a subflow, it can be reused in other contexts without change. + + + + + SubflowState API - input attributes + + The following illustrates the equivalent example using the FlowBuilder API: + + + public class OrderFlowBuilder extends AbstractFlowBuilder { + public void buildStates() { + ... + addSubflowState("enterShippingDetails", flow("shipping"), shippingMapper(), + transition(on("finish"), to("placeOrder"))); + ... + } + + protected FlowAttributeMapper shippingMapper() { + DefaultFlowAttributeMapper mapper = new DefaultFlowAttributeMapper(); + mapper.addInputMapping(mapping().source("flowScope.order.shipping").target("shipping").value()); + return mapper; + } + } + + + + Flow input mapping - input contract + + Internally within the definition of the shipping flow referenced above, the flow + may choose to map the shipping input attribute into its own scope using + its input mapper when it starts. Any input attributes must be explictly mapped, defining the input + contract for the flow: + + + <?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-1.0.xsd"> + + <input-mapper> + <input-attribute name="shipping"/> + </input-mapper> + + ... + + </flow> + + + This short-form input mapper declaration reads "when a new execution of this flow starts + map the shipping input attribute into flowScope under + the name shipping." + + + + Had this input mapping not been defined the shipping attribute made available as input + to this flow by a calling parent flow or external client would have been ignored. + + + +
+ + EndState + + When entered, an end state terminates a flow. A EndState represents exactly one logical + flow outcome; for example, "finish", or "cancel". + + + If the ended flow was acting as a top-level or root flow the + entire flow execution ends and cannot be resumed. In this case the end state is responsible + for making a ViewSelection that is the basis for the ending response (for example, + a confirmation page, or a redirect request to another flow or an external URL). + + + If the ended flow was acting as a subflow, the spawned subflow session ends and + the calling parent flow resumes by responding to the end + result returned. In this case the responsibility for any ViewSelection + falls on the parent flow. + + + Once a flow ends any attributes in flow scope go out of scope immediately + and become eligible for garbage collection. + + + As outlined, an end state entered as part of a root flow messages its ViewSelector + to make a ending view selection. Typically this is a redirect-based ViewSelector, + allowing for redirect after flow completion. An end state entered as part of + a subflow is not responsible for a view selection; this responsibility falls on the calling flow. + + + EndState result events + + When a EndState is entered it terminates a flow and, if used as subflow, + returns a result event the parent flow uses to drive a state transition from the calling subflow + state. It is the end state's responsibility to create this result event which + is the basis for communicating the logical flow outcome to + callers. + + + By default, an EndState creates a result event with an identifier that matches the + identifier of the end-state itself. For example, an end state with id finish + returns a result event with id finish. Also, any attributes in + flow scope that have been explicitly mapped as output attributes + are returned as result event parameters. This allows you to return data along + with the logical flow outcome. + + + Spring Web Flow gives you full control over the ending view selection strategy, as + well as what flow attributes should be exposed as output on a per EndState basis. + These configurable properties are summarized below: + + + + EndState Properties + + EndState properties + + + + + + + + Property name + Description + Cardinality + Default value + + + + + viewSelector + The strategy that makes the ending view selection when this state is entered and the flow is a root flow. + + 0..1 + + Null + + + outputMapper + + The service responsible for exposing flow output attributes, making those attributes eligible for output mapping by a calling flow. + + + 0..1 + + None + + + +
+
+ + EndState XML - redirect to flow after completion + + The following example constructs an EndState from + XML that terminates a shipping subflow and requests a + redirect response to another flow: + + + <?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-1.0.xsd"> + + ... + + <end-state id="finish" view="flowRedirect:searchFlow"/> + + </flow> + + + This end state definition reads "terminate the order flow + and redirect to a new execution of the searchFlow". + + + + EndState XML - redirect after flow completion + + The following example constructs an EndState from + XML that terminates a shipping subflow and requests a + redirect response to an external URL: + + + <?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-1.0.xsd"> + + ... + + <end-state id="finish" view="externalRedirect:/orders/${flowScope.order.id}"/> + + </flow> + + + This end state definition reads "terminate the order flow + and redirect to the URL returned by evaluating the /orders/${flowScope.order.id} + expression." + + + This is an example of the familiar redirect after post pattern where + after transaction completion a redirect is issued allowing the result of the transaction + to be viewed (in this case using REST-style URLs). + + + + EndState XML - flow output attribute + + The following example constructs an EndState from + XML that terminates a shipping subflow: + + + <?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-1.0.xsd"> + + ... + + <end-state id="finish"> + <output-mapper> + <output-attribute name="shipping"/> + </output-mapper> + </end-state> + + </flow> + + + This end state definition reads "terminate the shipping flow + and expose the shipping property in flow scope as an output attribute + with name shipping." + + + + EndState API - flow output attribute + + The following illustrates the equivalent example using the FlowBuilder API: + + + public class ShippingFlowBuilder extends AbstractFlowBuilder { + public void buildStates() { + ... + addEndState("finish", + new DefaultAttributeMapper().add( + mapping().source("flowScope.shipping").target("shipping").value() + ); + } + } + + + Since this end-state does not make a view selection it is expected this flow will be always used + as a subflow. When this flow ends, the calling parent flow is expected to respond to the + finish result, and may choose to map the shipping output + attribute into its own scope. + + + + SubflowState XML - mapping an output attribute + + The next example shows how a subflow-state can respond to the ending + result of a subflow and map output attributes into its own scope: + + + <?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-1.0.xsd"> + + ... + + <subflow-state id="enterShippingDetails" flow="shipping"> + <attribute-mapper> + <output-mapper> + <output-attribute name="shipping"/> + </output-mapper> + </attribute-mapper> + <transition on="finish" to="placeOrder"/> + </subflow-state> + + ... + + </flow> + + + This subflow state definition reads "spawn the shipping flow + as a subflow. When the shipping flow ends map the shipping output + attribute into flow scope under the name shipping, then respond to + the finish result event by transitioning to + the placeOrder state." + + + + Had this output mapping not been defined the shipping attribute made available as output + to this flow by the ending subflow would have been ignored. + + + +
+
+
\ No newline at end of file diff --git a/spring-webflow/docs/reference/src/flow-execution-repository.xml b/spring-webflow/docs/reference/src/flow-execution-repository.xml new file mode 100644 index 00000000..a3408bcc --- /dev/null +++ b/spring-webflow/docs/reference/src/flow-execution-repository.xml @@ -0,0 +1,247 @@ + + + Flow execution repositories + + Introduction + + A flow execution represents an executing flow at a point in time. + At runtime there can be any number of flow executions active in parallel. A single user + can even have multiple executions active at the same time (for example, when a user is + operating multiple windows or tabs within their browser). + + + Many of these flow executions span multiple requests into the server and therefore + must be saved so they can be resumed on subsequent requests. This presents technical + challenges, as there must exist a stable mechanism for a new request to be associated + with an existing execution in the view state that matches what the user expects. This problem + is more difficult when you consider that many applications require use of browser + navigational buttons and their use involves updating local history without + notifying the server. + + + The problem of flow execution persistence is addressed by Spring Web Flow's flow + execution repository subsystem. In this chapter you will learn how to use the system + to manage the storage of active web conversations in a stable manner. + + + + Repository architecture overview + + Recall the following bullet points noting what happens when a flow execution enters a ViewState: + + + + + + When a flow execution reaches a ViewState it is said to have paused, + where it waits in that state for user input to be provided so it can continue. After pausing the + ViewSelection returned is used to issue a response to the user + that provides a vehicle for collecting the required input. + + + + + User input is provided by signaling an event that + resumes the flow execution in the paused view state. + The input event communicates what user action was taken. + + + + + + Each time an active flow execution is paused it is saved out to a + repository. When the next request comes in for that flow execution, it is restored + from the repository, resumed, and continued. This process continues + until the flow execution reaches an end state, at which time it is removed from the repository. + + + This process is demonstrated over the next two graphics: + + + + + + + + + + Request one (1) - Paused flow execution persistence + + + + + + + + + + + Request two (2) - Paused flow execution restoration, removal on end + + + + + Flow execution identity + + When a flow execution is created it marks the start of a new conversation between a browser + and the server. As outlined, a new flow execution that is still active after startup + processing indicates the start of a conversation that will span more than one request + and needs to be persisted. When this is the case, that flow execution is assigned + an persistent identifer by the repository. By default the + structure of this identifier consists of a two-part composite key. This key is used + by clients to restore the flow execution on subsequent requests. + + + Conversation identifier + + The first part of a flow execution's persistent identity is a + unique conversation identifier. This serves as an index into + the logical conversation between the browser and the server that + has just started. + + + + Continuation identifier + + The second part of a flow execution's persistent identity is a continuation identifier. + This identifier serves as an index into a flow execution representing the state of the conversation + at this point in time. + + + + Flow execution key + + Together the conversation id plus the continuation id make up the unique two-part + flow execution key that identifies a state + of a conversation at a point in time. By submitting this key + in a subsequent request a browser can restore the conversation at that point + and continue from there. + + + So on a subsequent request the conversation is resumed by restoring a flow execution + from the repository using the two-part key. After event processing if the + flow execution is still active it is saved back out to the repository. + At this time a new flow execution key is generated. By default that key + retains the same conversation identifier, as the same logical + conversation is in progress; however the continuation identifier + changes to provide an index into the state of the flow execution + at this new point in time. + + + By submitting this new key in a subsequent request a browser can + restore the conversation at that point and continue from there. + This process continues until a flow execution reaches an end state during event processing + signaling the end of the conversation. + + + + + Conversation ending + + When a flow execution reaches an end state it terminates. If the flow execution was associated + with a logical conversation that spanned more than on request, it is removed from the + repository. More specifically, the entire conversation is ended, + resulting in any flow execution continuations associated with the conversation being purged. + + + Once a conversation has been ended the conversation identifier is no longer valid + and cannot ever be used again. + + + + Flow execution repository implementations + + The next section looks at the repository implementations that are available for use + with Spring Web Flow out-of-the-box. + + + Simple flow execution repository + + The simplest possible repository (SimpleFlowExecutionRepository). + This repository stores exactly one flow execution instance per conversation + in the user's session, invalidating it when its end state is reached. + This repository implementation has been designed with minimal storage overhead in mind. + + + + It is important to understand that use of this repository consistently prevents + duplicate submission when using the back button. If you attempt to go back + and resubmit, the continuation id stored in your browser history will not + match the current continuation id needed to access the flow execution and + access will be disallowed. + + + + + This repository implementation should generally be used when you do not have to + support browser navigational button use; for example, when you lock down the browser and + require that all navigation events to be routed through Spring Web Flow. + + + + + Continuation flow execution repository + + This repository (ContinuationFlowExecutionRepository) stores one to many flow + execution instances per conversation in the user's session, where each flow execution represents a + paused and restorable state of the conversation at a point in time. This repository implementation is + considerably more flexible than the simple one, but incurs more storage overhead. + + + + It is important to understand that use of this repository allows resubmission when + using the back button. If you attempt to go back and resubmit while the conversation + is active, the continuation id stored in your browser history will match the + continuation id of a previous flow execution in the repository. Access to + that flow execution representing the state of the conversation at that point in + time will be granted. + + + + Like the simple implementation, this repository implementation provides support for conversation + invalidation after completion where once a logical + conversation completes (by one of its FlowExecutions reaching an end state), + the entire conversation is invalidated. This prevents the possibility of + resubmission after completion. + + + This repository is more elaborate than the default repository, offering + more power (by enabling multiple continuations to exist per conversation), + but incurring more storage overhead. This repository implementation should be + considered when you do have to support browser navigational button use. + This implementation is the default. + + + + Client continuation flow execution repository + + This repository is entirely stateless and its use entails no server-side state + (ClientContinuationFlowExecutionRepository). + + + This is achieved by encoding a serialized flow execution directly into the + flow execution continuation key that is sent in the response. + + + When asked to load a flow execution by its key on a subsequent request, this + repository decodes and deserializes the flow execution, restoring it to + the state it was in when it was serialized. + + + + This repository implementation does not currently support + conversation invalidation after completion, as + this capability requires tracking active conversations using some + form of centralized storage, like a database table. + + + + + Storing state (a flow execution continuation) on the client + entails a certain security risk that should be evaluated. Furthermore, it + puts practical constraints on the size of the flow execution. + + + + + \ No newline at end of file diff --git a/spring-webflow/docs/reference/src/flow-execution.xml b/spring-webflow/docs/reference/src/flow-execution.xml new file mode 100644 index 00000000..874a33b6 --- /dev/null +++ b/spring-webflow/docs/reference/src/flow-execution.xml @@ -0,0 +1,809 @@ + + + Flow execution + + Introduction + + Once a flow has been defined any number of executions of it can be launched in parallel + at runtime. Execution of a flow is carried out by a dedicated system that + is based internally on a state machine that runs atop the Java VM. As the life of a + flow execution can span more than one request into the server, this system + is also responsible for persisting execution state across requests. + + + This chapter documents Spring Web Flow's flow execution system. You'll + learn the core constructs of the system and how to execute flows out-of-container + within a JUnit test environment. + + + + FlowExecution + + A org.springframework.webflow.execution.FlowExecution is a runtime instantiation of a flow definition. + Given a single FlowDefinition any + number of independent flow executions may be created, typically by a + FlowExecutionFactory. + + + A flow execution carries out the execution of program instructions defined within + its definition in response to user events. + + + + It may be helpful to think of a flow definition as analagous to a Java Class and a + flow execution as analagous to an object instance of that Class. Signaling + an execution event can be considered analagous to sending an object a message. + + + + Flow execution creation + + FlowDefinition definition = ... + FlowExecutionFactory factory = ... + FlowExecution execution = factory.createFlowExecution(definition); + + + Once created, a new flow execution is initially inactive, waiting to be started. Once + started a flow execution becomes active by entering its startState. + From there it continues executing until it enters a state where user input is required + to continue or it terminates. + + + + Flow execution startup + + MutableAttributeMap input = ... + ExternalContext context = ... + ViewSelection startingView = execution.start(input, context); + + + When a flow execution reaches a state where input is required to continue it is said to have paused, + where it waits in that state for the input to be provided. After pausing the + ViewSelection returned is typically used to issue a response to the user + that provides a vehicle for collecting the required input. + + + User input is provided by signaling an event that + resumes the flow execution by communicating what user action was taken. + Attributes of the signal event request form the basis for user input. The flow execution + resumes by consuming the event. + + + Once a flow execution has resumed it continues executing until it again enters a + state where more input is needed or it terminates. Once a flow execution has terminated + it becomes inactive and cannot be resumed. + + + + Flow execution resume + + ExternalContext context = ... + ViewSelection nextView = execution.signalEvent("submit", context); + if (execution.isActive()) { + // still active but paused + } else { + // has ended + } + + + + Flow execution lifecycle + + As outlined, a flow execution can go through a number of phases throughout its lifecycle; + for example, created, active, paused, + ended. + + + Spring Web Flow gives you the ability to observe the lifecycle of an + executing flow by implementing a FlowExecutionListener. + + + The different phases of a flow execution are shown graphically below: + + + + + + + + + + Flow execution lifecycle + + + + + Flow execution properties + + The Spring Web Flow flow execution implementation is org.springframework.webflow.engine.impl.FlowExecutionImpl, + typically created by a FlowExecutionImplFactory (a FlowExecutionFactory implementation). + The configurable properties of this flow execution implementation are summarized below: + + + Flow Execution properties + + + + + + + + Property name + Description + Cardinality + Default value + + + + + definition + The flow definition to be executed. + + 1 + + + + listeners + The set of observers observing the lifecycle of this flow execution. + + 0..* + + Empty + + + attributes + Global system attributes that can be used to affect flow execution behavior + + 0..* + + Empty + + + +
+ + The configurable constructs related to flow execution are shown graphically below: + + + + + + + + + + Flow execution + + +
+ + Flow execution impl creation + + FlowExecutionFactory factory = new FlowExecutionImplFactory(); + factory.setExecutionListeners(...); + factory.setExecutionAttributes(...); + FlowExecution execution = factory.createFlowExecution(definition); + + +
+ + Flow execution context + + Once created, a flow execution, representing the state of a flow at a point in time, + maintains contextual state about itself that can be reasoned upon by clients. In addition, + a flow execution exposes several data structures, called scopes, that allow clients to set + arbitrary attributes that are managed by the execution. + + + The contextual properties associated with a flow execution are summarized below: + + + Flow Execution Context properties + + + + + + + + Property name + Description + Cardinality + Default value + + + + + active + + A flag indicating if the flow execution is active. + An inactive flow execution has either ended or has never been started. + + + 1 + + + + definition + + The definition of the flow execution. The flow definition serves as + the blueprint for the program. It may be helpful to think of a flow + definition as like a Class and a + flow execution as like an instance of that Class. + This method may always be safely called. + + + 1 + + + + activeSession + + The active flow session, tracking the flow that is currently executing + and what state it is in. The active session can change over the life of the + flow execution because a flow can spawn another flow as a subflow. + This property can only be queried while the flow execution is active. + + + 1 + + + + conversationScope + + A data map that forms the basis for "conversation scope". Arbitrary attributes placed in this map will be retained + for the life of the flow execution and correspond to the length of the logical conversation. + This map is shared by all flow sessions. + + + 1 + + + + +
+ + As a flow execution is manipulated by clients its contextual state changes. Consider how + contextual state is effected when the following events occur: + + + An ordered set of events and their effects on flow execution context + + + + + + + Flow Execution Event + Active? + Value of the activeSession property + + + + + created + false + Throws an IllegalStateException + + + started + true + + A FlowSession whose definition + is the top-level flow definition and whose state is the definition's start state. + + + + state entered + true + + A FlowSession whose definition + is the top-level flow definition and whose state is the newly entered state. + + + + subflow spawned + true + + A FlowSession whose definition + is the subflow definition and whose state is the subflow's start state. + + + + subflow ended + true + + A FlowSession whose definition is back to the + top-level flow definition and whose state is the resuming state. + + + + ended + false + Throws an IllegalStateException + + + +
+ + As you can see, the activeSession of a flow execution changes when a subflow + is spawned. Each flow execution maintains a stack of flow sessions, where each flow session + represents a spawned instance of a flow definition. When a flow execution starts, the session stack initially + consists of one (1) entry, an instance dubbed the root session. + When a subflow is spawned, the stack increases to two (2) entries. When the subflow ends, + the stack decreases back to one (1) entry. The active session is always + the session at the top of the stack. + + + The contextual properties associated with a FlowSession + are summarized below: + + + Flow Session properties + + + + + + + + Property name + Description + Cardinality + Default value + + + + + definition + + The definition of the flow the session is an instance of. + + + 1 + + + + state + + The current state of the session. + + + 1 + + + + status + + A status indicator describing what the session is currently doing. + + + 1 + + + + scope + + A data map that forms the basis for flow scope. + Arbitrary attributes placed in this map will be retained for the scope + of the flow session. This map is local to the session. + + + 1 + + + + flashMap + + A data map that forms the basis for flash scope. + Attributes placed in this map will be retained until the next + external user event is signaled in the session. + + + 1 + + + + +
+ + The following graphic illustrates an example flow execution context and flow + session stack: + + + + + + + + + + Flow execution context + + + + In this illustration a flow execution has been created for the Book Flight flow. + The execution is currently active and the activeSession indicates it + is in the Display Seating Chart state of the Assign Seats flow, + which was spawned as a subflow from the Enter Seat Assignments state. + + + + Note how the active session status is paused, indicating the flow execution + is currently waiting for user input to be provided to continue. In this case, it is + expected the user will choose a seat for their flight. + + +
+ + Flow execution scopes + + As alluded to, a flow execution manages several containers called scopes, + which allow arbitrary attributes to be stored for a period of time. There are four scope + types, each with different storage management semantics: + + + Flow execution scope types + + + + + + Scope type name + Management Semantics + + + + + request + + Eligible for garbage collection when a single call into the flow execution completes. + + + + flash + + Cleared when the next user event is signaled into the flow session; eligible for garbage collection when the flow session ends. + + + + flow + + Eligible for garbage collection when the flow session ends. + + + + conversation + + Eligible for garbage collection when the root session of the governing flow execution (logical conversation) ends. + + + + +
+
+ + Flow execution testing + + Spring Web Flow provides support within the org.springframework.webflow.test + package for testing flow executions with JUnit. This support is provided as convenience but is + entirely optional, as a flow execution is instantiable in any environment with the standard + new operator. + + + The general strategy for testing flows follows: + + + + + + Your own implementations of definitional artifacts used by a flow such as actions, + attribute mappers, and exception handlers should be unit tested in isolation. + Spring Web Flow ships convenient stubs to assist with this, for instance + MockRequestContext. + + + + + The execution of a flow should be tested as part of a system integration test. + Such a test should exercise all possible paths of the flow, asserting that + the flow responds to events as expected. + + + + + + + A flow execution integration test typically selects mock or stub implementations of application + services called by the flow, though it may also exercise production implementations. + Both are useful, supported system test configurations. + + + + Flow execution test example + + To help illustrate testing a flow execution, first consider the following flow definition + to search a phonebook for contacts: + + + + + + + + + + Phonebook Search Flow - State Diagram + + + + The corresponding XML-based flow definition implementation: + + + <?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-1.0.xsd"> + + <start-state idref="enterCriteria"/> + + <view-state id="enterCriteria" view="searchCriteria"> + <render-actions> + <action bean="formAction" method="setupForm"/> + </render-actions> + <transition on="search" to="displayResults"> + <action bean="formAction" method="bindAndValidate"/> + </transition> + </view-state> + + <view-state id="displayResults" view="searchResults"> + <render-actions> + <bean-action bean="phonebook" method="search"> + <method-arguments> + <argument expression="flowScope.searchCriteria"/> + </method-arguments> + <method-result name="results"/> + </bean-action> + </render-actions> + <transition on="newSearch" to="enterCriteria"/> + <transition on="select" to="browseDetails"/> + </view-state> + + <subflow-state id="browseDetails" flow="detail-flow"> + <attribute-mapper> + <input-mapper> + <mapping source="requestParameters.id" target="id" from="string" to="long"/> + </input-mapper> + </attribute-mapper> + <transition on="finish" to="displayResults"/> + </subflow-state> + + </flow> + + + Above you see a flow with three (3) states that execute these behaviors, respectively: + + + + + + The first state enterCriteria displays a search criteria form so the user can enter who + he or she wishes to search for. + + + + + On form submit and successful data binding and validation the search is executed. + After search execution a results view is displayed. + + + + + From the results view the user may select a result they wish to browse additional details on + or they may request a new search. On select, the "detail" flow is spawned and + when it finishes the search is re-executed and it's results redisplayed. + + + + + + From this behavior narrative the following assertable test scenarios can be extracted: + + + + + That when a flow execution starts, it enters the enterCriteria state and + makes a searchCriteria view selection containing a form object + to be used as the basis for form field population. + + + + + That on submit with valid input, the search is executed and a searchResults view selection is made. + + + + + That on submit with invalid input, the searchCriteria view is reselected. + + + + + That on newSearch, the searchCriteria view is selected. + + + + + That on select, the detail flow is spawned and passed the id of the selected result as expected. + + + + + To assist with writing these assertions Spring Web Flow ships with JUnit-based flow execution + test support within the org.springframwork.webflow.test package. + These base test classes are indicated below: + + + Flow execution test support hierarchy + + + + + + Class name + Description + + + + + AbstractFlowExecutionTests + The most generic base class for flow execution tests. + + + AbstractExternalizedFlowExecutionTests + The base class for flow execution tests whose flow is defined within an externalized resource, such as a file. + + + AbstractXmlFlowExecutionTests + The base class for flow execution tests whose flow is defined within an externalized XML resource. + + + +
+ + The completed test for this example extending AbstractXmlFlowExecutionTests is shown below: + + + public class SearchFlowExecutionTests extends AbstractXmlFlowExecutionTests { + + public void testStartFlow() { + ApplicationView view = applicationView(startFlow()); + assertCurrentStateEquals("enterCriteria"); + assertViewNameEquals("searchCriteria", view); + assertModelAttributeNotNull("searchCriteria", view); + } + + public void testCriteriaSubmitSuccess() { + startFlow(); + MockParameterMap parameters = new MockParameterMap(); + parameters.put("firstName", "Keith"); + parameters.put("lastName", "Donald"); + ApplicationView view = applicationView(signalEvent("search", parameters)); + assertCurrentStateEquals("displayResults"); + assertViewNameEquals("searchResults", view); + assertModelAttributeCollectionSize(1, "results", view); + } + + public void testCriteriaSubmitError() { + startFlow(); + signalEvent("search"); + assertCurrentStateEquals("enterCriteria"); + } + + public void testNewSearch() { + testCriteriaSubmitSuccess(); + ApplicationView view = applicationView(signalEvent("newSearch")); + assertCurrentStateEquals("enterCriteria"); + assertViewNameEquals("searchCriteria", view); + } + + public void testSelectValidResult() { + testCriteriaSubmitSuccess(); + MockParameterMap parameters = new MockParameterMap(); + parameters.put("id", "1"); + ApplicationView view = applicationView(signalEvent("select", parameters)); + assertCurrentStateEquals("displayResults"); + assertViewNameEquals("searchResults", view); + assertModelAttributeCollectionSize(1, "results", view); + } + + @Override + protected FlowDefinitionResource getFlowDefinitionResource() { + return createFlowDefinitionResource("src/main/webapp/WEB-INF/flows/search-flow.xml"); + } + + @Override + protected void registerMockServices(MockFlowServiceLocator serviceRegistry) { + Flow mockDetailFlow = new Flow("detail-flow"); + mockDetailFlow.setInputMapper(new AttributeMapper() { + public void map(Object source, Object target, Map context) { + assertEquals("id of value 1 not provided as input by calling search flow", new Long(1), ((AttributeMap)source).get("id")); + } + }); + // test responding to finish result + new EndState(mockDetailFlow, "finish"); + + serviceRegistry.registerSubflow(mockDetailFlow); + serviceRegistry.registerBean("phonebook", new ArrayListPhoneBook()); + } + } + + + With a well-written flow execution test passing that covers the controller behavior scenarios + possible for your flow you have concrete evidence the flow will execute as expected when + deployed in a container. + + + + + + + + + + Go for Green + + +
+ + Execution unit testing vs. full-blown system testing + + The previous example shows how to test a flow execution in relative isolation with a mock service + layer and mock subflows. Flow execution testing against a real service-layer + and real subflows is also supported. + + + The next example shows how the createFlowServiceLocator method can + be overridden to create the service-layer using a Spring application context: + + + public class SearchFlowExecutionTests extends AbstractXmlFlowExecutionTests { + + ... + + @Override + protected FlowDefinitionResource getFlowDefinitionResource() { + return createFlowDefinitionResource("src/main/webapp/WEB-INF/flows/search-flow.xml"); + } + + @Override + protected FlowServiceLocator createFlowServiceLocator() { + + // create a context to host our middle tier services + ApplicationContext context = + new ClassPathXmlApplicationContext(new String[] { + "classpath:service-layer-config.xml", + "classpath:data-access-layer-config.xml" + }); + + // create a registry for our flow definitions being tested + FlowDefinitionRegistry registry = new FlowDefinitionRegistryImpl(); + + // initialize the service locator + DefaultFlowServiceLocator locator = new DefaultFlowServiceLocator(registry, context); + + // perform subflow definition registration with the help of a registrar + XmlFlowRegistrar registrar = new XmlFlowRegistrar(locator); + registrar.addResource(createFlowDefinitionResource("/WEB-INF/flows/search-flow.xml")); + registrar.addResource(createFlowDefinitionResource("/WEB-INF/flows/detail-flow.xml")); + registrar.registerFlowDefinitions(registry); + + return locator; + } + } + + +
+
\ No newline at end of file diff --git a/spring-webflow/docs/reference/src/flow-executor.xml b/spring-webflow/docs/reference/src/flow-executor.xml new file mode 100644 index 00000000..b0338035 --- /dev/null +++ b/spring-webflow/docs/reference/src/flow-executor.xml @@ -0,0 +1,555 @@ + + + Flow executors + + Introduction + + Flow executors are the highest-level entry points into + the Spring Web Flow system, responsible for driving the execution of flows + across a variety of environments. + + + In this chapter you'll learn how to execute flows within Spring MVC, Struts, + and Java Server Faces (JSF) based applications. + + + + FlowExecutor + + org.springframework.webflow.executor.FlowExecutor is the + central facade interface external systems use to drive the execution of flows. + This facade acts as a simple, convenient service entry-point into + the Spring Web Flow system that is reusable across environments. + + + The FlowExecutor interface is shown below: + + + public interface FlowExecutor { + ResponseInstruction launch(String flowDefinitionId, ExternalContext context); + ResponseInstruction resume(String flowExecutionKey, String eventId, ExternalContext context); + ResponseInstruction refresh(String flowExecutionKey, ExternalContext context); + } + + + As you can see there are three central use-cases fulfilled by this interface: + + + + Launch (start) a new execution of a flow definition. + + + + + Resume a paused flow execution by signaling an event against its current state. + + + + + Request that the last response issued by a flow execution be re-issued. + Unlike start and signalEvent, the refresh operation is an idempotent operation + that does not affect the state of a flow execution. + + + + + + Each operation accepts an ExternalContext that provides normalized + access to properties of an external system that has called into Spring Web Flow, allowing + access to environment-specific request parameters as well as request, session, and + application-level attributes. + + + Each operation returns a ResponseInstruction which the calling system is + expected to use to issue a suitable response. + + + These relationships are shown graphically below: + + + + + + + + + + Flow executor + + + + As you can see, an ExternalContext implementation exists for each of + the environments Spring Web Flow supports. If a flow artifact such as an Action needs + to access native constructs of the calling environment it can downcast a context to its + specific implementation. The need for such downcasting is considered a special case. + + + FlowExecutorImpl + + The default executor implementation is org.springframework.webflow.executor.FlowExecutorImpl. + It allows for configuration of a FlowDefinitionLocator responsible for loading the flow definitions to execute, as well as + the FlowExecutionRepository strategy responsible for persisting flow executions that remain + active beyond a single request into the server. + + + The configurable FlowExecutorImpl properties are shown below: + + + FlowExecutorImpl properties + + + + + + + Property name + Description + Cardinality + + + + + definitionLocator + The service for loading flow definitions to be executed, typically a FlowDefinitionRegistry + + 1 + + + + executionFactory + The factory for creating new flow executions. + + 1 + + + + executionRepository + The repository for saving and loading persistent (paused) flow executions + + 1 + + + + +
+
+ + A typical flow executor configuration with Spring 2.0 + + <?xml version="1.0" encoding="UTF-8"?> + <beans xmlns="http://www.springframework.org/schema/beans" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:flow="http://www.springframework.org/schema/webflow-config" + xsi:schemaLocation=" + http://www.springframework.org/schema/beans + http://www.springframework.org/schema/beans/spring-beans-2.0.xsd + http://www.springframework.org/schema/webflow-config + http://www.springframework.org/schema/webflow-config/spring-webflow-config-1.0.xsd"> + + <!-- Launches new flow executions and resumes existing executions. --> + <flow:executor id="flowExecutor" registry-ref="flowRegistry"/> + + <!-- Creates the registry of flow definitions for this application --> + <flow:registry id="flowRegistry"> + <flow:location path="/WEB-INF/flows/**/*-flow.xml"/> + </flow:registry> + + </beans> + + + This instructs Spring to create a flow executor that can execute all XML-based flow definitions + contained within the /WEB-INF/flows directory. The default flow execution + repository, continuation, is used. + + + + A flow executor using a simple execution repository + + <flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="simple"/> + + + This executor is configured with a simple repository that manages + execution state in the user session. + + + + A flow executor using a client-side continuation-based execution repository + + <flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="client"/> + + + This executor is configured with a continuation-based repository that serializes + continuation state to the client using no server-side state. + + + + A flow executor using a single key execution repository + + <flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="singleKey"/> + + + This executor is configured with a simple repository that assigns a single + flow execution key per conversation. The key, once assigned, never changes + for the duration of the conversation. + + + + A flow executor setting system execution attributes + + <flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="continuation"> + <flow:execution-attributes> + <flow:alwaysRedirectOnPause value="false"/> + <flow:attribute name="foo" value="bar"/> + </flow:execution-attributes> + </flow-executor> + + + This executor is configured to set two flow execution system attributes + alwaysRedirectOnPause=false and foo=bar. + + + + The alwaysRedirectOnPause attribute determines if + a flow execution redirect occurs automatically each time an execution pauses + (automated POST+REDIRECT+GET behavior). + Setting this attribute to false will disable the default 'true' behavior + where a flow execution redirect always occurs on pause. + + + + + A flow executor setting custom execution listeners + + <flow:executor id="flowExecutor" registry-ref="flowRegistry" repository-type="continuation"> + <flow:execution-listeners> + <flow:listener ref="listener" criteria="order-flow"/> + </flow:execution-listeners> + </flow-executor> + + <!-- A FlowExecutionListener to observe the lifecycle of order-flow executions --> + <bean id="listener" class="org.springframework.webflow.samples.sellitem.SellItemFlowExecutionListener"/> + + + This executor is configured to apply the execution listener to the "order-flow". + + + + A Spring 1.2 compatible flow executor configuration + + <?xml version="1.0" encoding="UTF-8"?> + <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" + "http://www.springframework.org/dtd/spring-beans.dtd"> + <beans> + + <!-- Launches new flow executions and resumes existing executions: Spring 1.2 config version --> + <bean id="flowExecutor" class="org.springframework.webflow.config.FlowExecutorFactoryBean"> + <property name="definitionLocator" ref="flowRegistry"/> + <property name="executionAttributes"> + <map> + <entry key="alwaysRedirectOnPause"> + <value type="java.lang.Boolean">false</value> + </entry> + </map> + </property> + <property name="repositoryType" value="CONTINUATION"/> + </bean> + + <!-- Creates the registry of flow definitions for this application: Spring 1.2 config version --> + <bean id="flowRegistry" + class="org.springframework.webflow.engine.builder.xml.XmlFlowRegistryFactoryBean"> + <property name="flowLocations"> + <list> + <value>/WEB-INF/flows/**/*-flow.xml</value> + </list> + </property> + </bean> + + </beans> + + + This achieves similar semantics as the Spring 2.0 version above. + The 2.0 version is more concise, provides stronger validation, and encapsulates + internal details such as FactoryBean class names. The 1.2 version is Spring 1.2 or > + compatible and digestable by Spring IDE 1.3. + + +
+ + Spring MVC integration + + Spring Web Flow integrates with both Servlet and Portlet MVC which ship with the + core Spring Framework. Use of Portlet MVC requires Spring 2.0. + + + For both Servlet and Portlet MVC a FlowController acts as an adapter + between Spring MVC and Spring Web Flow. As an adapter, this controller has knowledge + of both systems and delegates to a flow executor for driving the execution of flows. + One controller typically executes all flows of an application, relying on + parameterization to determine what flow to launch or what flow execution to resume. + + + A single flow controller executing all flows in a Servlet MVC environment + + <bean name="/flowController.htm" class="org.springframework.webflow.executor.mvc.FlowController"> + <property name="flowExecutor" ref="flowExecutor"/> + </bean> + + + This controller, exported at the context-relative /flowController.htm URL, + delegates to the configured flow executor for driving flow executions in a Spring Servlet + MVC environment. + + + + A single portlet flow controller executing a flow within a Portlet + + <bean id="portletModeControllerMapping" + class="org.springframework.web.portlet.handler.PortletModeHandlerMapping"> + <property name="portletModeMap"> + <map> + <entry key="view" value-ref="flowController"/> + </map> + </property> + </bean> + + <bean id="flowController" class="org.springframework.webflow.executor.mvc.PortletFlowController"> + <property name="flowExecutor" ref="flowExecutor"/> + <property name="defaultFlowId" ref="search-flow"/> + </bean> + + + This controller, exported for access with the configured portlet mode, + delegates to the configured flow executor for driving flow executions in a Spring Portlet + MVC environment (by default, an execution of the search-flow + will be launched). + + + + + Flow executor parameterization + + Spring Web Flow allows for full control over how flow executor method arguments such as the + flowDefinitionId, flowExecutionKey, and eventId + are extracted from an incoming controller request with the + org.springframework.webflow.executor.support.FlowExecutorArgumentExtractor + strategy. + + The various flow controllers typically do not use this strategy directly but instead use a + convenient FlowExecutorArgumentHandler implementation that takes care + of argument extraction as well as exposing responsibilities (in callback URLs). + + + + The next several examples illustrate strategies for parameterizing flow controllers + from the browser to launch and resume flow executions: + + + Request parameter-based flow executor argument extraction + + The default executor argument extractor strategy is request-parameter based. + The default request parameters are: + + + Extractor request parameter names + + + + + + Parameter name + Description + + + + + _flowId + The flow definition id, needed to launch a new flow execution. + + + _flowExecutionKey + The flow execution key, needed to resume and refresh an existing flow execution. + + + _eventId + The id of an event that occured, needed to resume an existing flow execution. + + + +
+ + Launching a flow execution - parameter-style anchor + + <a href="flowController.htm?_flowId=myflow">Launch My Flow</a> + + + + Launching a flow execution - form + + <form action="flowController.htm" method="post"> + <input type="submit" value="Launch My Flow"/> + <input type="hidden" name="_flowId" value="myflow"> + </form> + + + + Resuming a flow execution - anchor + + <a href="flowController.htm?_flowExecutionKey=${flowExecutionKey}&_eventId=submit"> + Submit + </a> + + + + Resuming a flow execution - form + + <form action="flowController.htm" method="post"> + ... + <input type="hidden" name="_flowExecutionKey" value="${flowExecutionKey}"> + <input type="hidden" name="_eventId" value="submit"/> + <input type="submit" class="button" value="Submit"> + </form> + + + + Resuming a flow execution - multiple form buttons + + <form action="flowController.htm" method="post"> + ... + <input type="hidden" name="_flowExecutionKey" value="${flowExecutionKey}"> + <input type="submit" class="button" name="_eventId_submit" value="Submit"> + <input type="submit" class="button" name="_eventId_cancel" value="Cancel"> + </form> + + + + In this case the eventId is determined by parsing the name of the + button that was pressed. + + + + + Refreshing a flow execution + + <a href="flowController.htm?_flowExecutionKey=${flowExecutionKey}">Refresh</a> + + +
+ + Request path based flow executor argument extraction + + The request-path based argument extractor strategy relies on executor arguments + being path elements as much as possible. This results in friendlier REST-style URLs such + as http://host/app/myflow instead of + http://host/app?_flowId=myflow. + + + A flow controller with a request-path based argument extractor + + <bean name="/flowController.htm" class="org.springframework.webflow.executor.mvc.FlowController"> + <property name="flowExecutor" ref="flowExecutor"/> + <property name="argumentHandler"> + <bean class="org.springframework.webflow.executor.support.RequestPathFlowExecutorArgumentHandler"/> + </property> + </bean> + + + + Launching a flow execution - REST-style anchor + + <a href="flowController/myflow"/>Launch My Flow</a> + + + + Resuming a flow execution - multiple form buttons + + <form action="${flowExecutionKey}" method="post"> + ... + <input type="submit" class="button" name="_eventId_submit" value="Submit"> + <input type="submit" class="button" name="_eventId_cancel" value="Cancel"> + </form> + + + + Refreshing a flow execution + + <a href="flowController/k/${flowExecutionKey}">Refresh</a> + + + +
+ + Struts integration + + Spring Web Flow integrates with Struts 1.x or >. The integration is very similiar to + Spring MVC where a single front controller (FlowAction) drives the execution of all flows + for the application by delegating to a configured flow executor. + + + A single flow action executing all flows + + <form-beans> + <form-bean name="actionForm" type="org.springframework.web.struts.SpringBindingActionForm"/> + </form-beans> + + <action-mappings> + <action path="/flowAction" name="actionForm" scope="request" + type="org.springframework.webflow.executor.struts.FlowAction"/> + </action-mappings> + + + + + Java Server Faces (JSF) integration + + Spring Web Flow integrates with JSF. The JSF integration relies on custom implementations of + core JSF artifacts such as navigation handler and phase listener to drive the + execution of flows. + + + A typical faces-config.xml file + +<faces-config> + <application> + <navigation-handler> + org.springframework.webflow.executor.jsf.FlowNavigationHandler + </navigation-handler> + <property-resolver> + org.springframework.webflow.executor.jsf.FlowPropertyResolver + </property-resolver> + <variable-resolver> + org.springframework.webflow.executor.jsf.FlowVariableResolver + </variable-resolver> + <variable-resolver> + org.springframework.web.jsf.DelegatingVariableResolver + </variable-resolver> + <variable-resolver> + org.springframework.web.jsf.WebApplicationContextVariableResolver + </variable-resolver> + </application> + + <lifecycle> + <phase-listener>org.springframework.webflow.executor.jsf.FlowPhaseListener</phase-listener> + </lifecycle> +</faces-config> + + + + Launching a flow execution - command link + + <h:commandLink value="Go" action="flowId:myflow"/> + + + + Resuming a flow execution - form + + <h:form id="form"> + ... + <h:inputText id="propertyName" value="#{flowScope.managedBeanName.propertyName}"/> + ... + <input type="hidden" name="_flowExecutionKey" value="${flowExecutionKey}"> + <h:commandButton type="submit" value="Next" action="submit"/> + </h:form> + + + +
\ No newline at end of file diff --git a/spring-webflow/docs/reference/src/images/actionadapter-classdiagram.png b/spring-webflow/docs/reference/src/images/actionadapter-classdiagram.png new file mode 100644 index 00000000..f09de8c8 Binary files /dev/null and b/spring-webflow/docs/reference/src/images/actionadapter-classdiagram.png differ diff --git a/spring-webflow/docs/reference/src/images/actionstate-classdiagram.jpg b/spring-webflow/docs/reference/src/images/actionstate-classdiagram.jpg new file mode 100644 index 00000000..f6a6a7c2 Binary files /dev/null and b/spring-webflow/docs/reference/src/images/actionstate-classdiagram.jpg differ diff --git a/spring-webflow/docs/reference/src/images/architecture-layer-diagram.png b/spring-webflow/docs/reference/src/images/architecture-layer-diagram.png new file mode 100644 index 00000000..c8d707ef Binary files /dev/null and b/spring-webflow/docs/reference/src/images/architecture-layer-diagram.png differ diff --git a/spring-webflow/docs/reference/src/images/flow-search.png b/spring-webflow/docs/reference/src/images/flow-search.png new file mode 100644 index 00000000..2faaa307 Binary files /dev/null and b/spring-webflow/docs/reference/src/images/flow-search.png differ diff --git a/spring-webflow/docs/reference/src/images/flowdefinition-classdiagram.png b/spring-webflow/docs/reference/src/images/flowdefinition-classdiagram.png new file mode 100644 index 00000000..911629fd Binary files /dev/null and b/spring-webflow/docs/reference/src/images/flowdefinition-classdiagram.png differ diff --git a/spring-webflow/docs/reference/src/images/flowexecution-classdiagram.png b/spring-webflow/docs/reference/src/images/flowexecution-classdiagram.png new file mode 100644 index 00000000..af3fd181 Binary files /dev/null and b/spring-webflow/docs/reference/src/images/flowexecution-classdiagram.png differ diff --git a/spring-webflow/docs/reference/src/images/flowexecution-persistence.png b/spring-webflow/docs/reference/src/images/flowexecution-persistence.png new file mode 100644 index 00000000..69addc6f Binary files /dev/null and b/spring-webflow/docs/reference/src/images/flowexecution-persistence.png differ diff --git a/spring-webflow/docs/reference/src/images/flowexecution-restoration.png b/spring-webflow/docs/reference/src/images/flowexecution-restoration.png new file mode 100644 index 00000000..0b8ad8a5 Binary files /dev/null and b/spring-webflow/docs/reference/src/images/flowexecution-restoration.png differ diff --git a/spring-webflow/docs/reference/src/images/flowexecution-sessionstack.png b/spring-webflow/docs/reference/src/images/flowexecution-sessionstack.png new file mode 100644 index 00000000..3b258ef8 Binary files /dev/null and b/spring-webflow/docs/reference/src/images/flowexecution-sessionstack.png differ diff --git a/spring-webflow/docs/reference/src/images/flowexecution-statediagram.png b/spring-webflow/docs/reference/src/images/flowexecution-statediagram.png new file mode 100644 index 00000000..766cab0e Binary files /dev/null and b/spring-webflow/docs/reference/src/images/flowexecution-statediagram.png differ diff --git a/spring-webflow/docs/reference/src/images/flowexecutorfacade-classdiagram.png b/spring-webflow/docs/reference/src/images/flowexecutorfacade-classdiagram.png new file mode 100644 index 00000000..961d9ece Binary files /dev/null and b/spring-webflow/docs/reference/src/images/flowexecutorfacade-classdiagram.png differ diff --git a/spring-webflow/docs/reference/src/images/globaltransitions-statediagram.png b/spring-webflow/docs/reference/src/images/globaltransitions-statediagram.png new file mode 100644 index 00000000..60820668 Binary files /dev/null and b/spring-webflow/docs/reference/src/images/globaltransitions-statediagram.png differ diff --git a/spring-webflow/docs/reference/src/images/junit-greenbar.png b/spring-webflow/docs/reference/src/images/junit-greenbar.png new file mode 100644 index 00000000..04d2f6b1 Binary files /dev/null and b/spring-webflow/docs/reference/src/images/junit-greenbar.png differ diff --git a/spring-webflow/docs/reference/src/images/logo-ervacon.png b/spring-webflow/docs/reference/src/images/logo-ervacon.png new file mode 100644 index 00000000..62b3b1e8 Binary files /dev/null and b/spring-webflow/docs/reference/src/images/logo-ervacon.png differ diff --git a/spring-webflow/docs/reference/src/images/logo-interface21.png b/spring-webflow/docs/reference/src/images/logo-interface21.png new file mode 100644 index 00000000..81889731 Binary files /dev/null and b/spring-webflow/docs/reference/src/images/logo-interface21.png differ diff --git a/spring-webflow/docs/reference/src/images/logo.jpg b/spring-webflow/docs/reference/src/images/logo.jpg new file mode 100644 index 00000000..96076ae9 Binary files /dev/null and b/spring-webflow/docs/reference/src/images/logo.jpg differ diff --git a/spring-webflow/docs/reference/src/images/subflowstate-classdiagram.png b/spring-webflow/docs/reference/src/images/subflowstate-classdiagram.png new file mode 100644 index 00000000..59e3c466 Binary files /dev/null and b/spring-webflow/docs/reference/src/images/subflowstate-classdiagram.png differ diff --git a/spring-webflow/docs/reference/src/images/swf-highlevelarchitecture.png b/spring-webflow/docs/reference/src/images/swf-highlevelarchitecture.png new file mode 100644 index 00000000..8f47196e Binary files /dev/null and b/spring-webflow/docs/reference/src/images/swf-highlevelarchitecture.png differ diff --git a/spring-webflow/docs/reference/src/images/transition-statediagram.jpg b/spring-webflow/docs/reference/src/images/transition-statediagram.jpg new file mode 100644 index 00000000..38c9278f Binary files /dev/null and b/spring-webflow/docs/reference/src/images/transition-statediagram.jpg differ diff --git a/spring-webflow/docs/reference/src/images/viewstate-classdiagram.png b/spring-webflow/docs/reference/src/images/viewstate-classdiagram.png new file mode 100644 index 00000000..af7fe301 Binary files /dev/null and b/spring-webflow/docs/reference/src/images/viewstate-classdiagram.png differ diff --git a/spring-webflow/docs/reference/src/images/xdev-spring_logo.jpg b/spring-webflow/docs/reference/src/images/xdev-spring_logo.jpg new file mode 100644 index 00000000..622962ee Binary files /dev/null and b/spring-webflow/docs/reference/src/images/xdev-spring_logo.jpg differ diff --git a/spring-webflow/docs/reference/src/index.xml b/spring-webflow/docs/reference/src/index.xml new file mode 100644 index 00000000..1b8bf873 --- /dev/null +++ b/spring-webflow/docs/reference/src/index.xml @@ -0,0 +1,79 @@ + + + + + + + +]> + + + + + Spring Web Flow + Reference Documentation + Version 1.0 + October 2006 + + + Keith + Donald + + + Erwin + Vervaet + + + + + Copies of this document may be made for your own use and for + distribution to others, provided that you do not charge any + fee for such copies and further provided that each copy + contains this Copyright Notice, whether distributed in print + or electronically. + + + + + + Sponsors + + + Spring Web Flow would not be possible without the investment of its sponsors: + Interface21 and + Ervacon. + + + + + + + + + + + + + + + + + + + + + + + + + &overview; + &flow-definition; + &flow-execution; + &flow-execution-repository; + &flow-executor; + &practical; + + \ No newline at end of file diff --git a/spring-webflow/docs/reference/src/overview.xml b/spring-webflow/docs/reference/src/overview.xml new file mode 100644 index 00000000..e3757fda --- /dev/null +++ b/spring-webflow/docs/reference/src/overview.xml @@ -0,0 +1,646 @@ + + + Preface + + Many web applications consist of a mix of free browsing, + where the user is allowed to navigate a web site as they please, + and controlled navigations where the user is guided through + a series of steps towards completion of a business goal. + + + Consider the typical shopping cart application. While a user is + shopping, she is freely browsing available products, adding her + favorites to her cart while skipping over others. This is a good + "free browsing" use case. However, when the user decides to + checkout, a controlled workflow begins--the checkout process. + Such a process represents a single user conversation that takes + place over a series of steps, and navigation from step-to-step + is controlled. The entire process represents an discrete + application transaction that must complete exactly once + or not at all. + + + Consider some other good examples of "controlled navigations": + applying for a loan, paying your taxes on-line, + booking a trip reservation, registering an account, or + updating a warehouse inventory. + + + Traditional approaches to modeling and enforcing such controlled + navigations or "flows" fall flat, and fail to express the Flow as a + first class concept. Spring Web Flow (SWF) is a component of the + Spring Framework's web stack focused on solving this problem + in a productive and powerful manner. + + + + + Introduction + + Overview + + Spring Web Flow (SWF) is a component of the + Spring Framework's web stack focused on the + definition and execution of UI flow within + a web application. + + + The system allows you to capture a logical flow + of your web application as a self-contained module + that can be reused in different situations. Such + a flow guides a single user through the implementation + of a business task, and represents a single user + conversation. + Flows often execute across HTTP requests, + have state, exhibit transactional characteristics, + and may be dynamic and/or long-running in nature. + + + Spring Web Flow exists at a higher level of abstraction, integrating + as a self-contained flow engine within + base frameworks such as Struts, Spring MVC, Portlet MVC, and JSF. + SWF provides you the capability to capture your + application's UI flow explicitly in a declarative, + portable, and manageable fashion. SWF is + a powerful controller framework based on a finite-state machine, + fully addressing the "C" in MVC. + + + + Architecture overview + + Spring Web Flow has been architected as a self-contained flow engine + with few required dependencies on third-party APIs. All dependencies are + carefully managed. + + + At a minimum, to use Spring Web Flow you need: + + + + spring-webflow (the framework) + + + spring-core (miscellaneous utility classes used internally by the framework) + + + spring-binding (the Spring data binding framework, used internally) + + + commons-logging (a simple logging facade, used internally) + + + OGNL (the default expression language) + + + + Most users will embed SWF as a component within a larger web application development + framework, as SWF is a focused controller technology that expects a + calling system to care for request mapping and response rendering. In this case, those users + will depend on a thin integration piece for their environment. For example, those executing + flows within a Servlet environment might use the Spring MVC integration to care for dispatching + requests to SWF and rendering responses for SWF view selections. Spring Web Flow ships + convenient Spring MVC, Struts Classic, and JSF integration out of the box. + + + + Spring Web Flow, like Spring, is a layered framework, + packaged in a manner that allows teams to use the parts they need and nothing else. + For example, one team might use Spring Web Flow in a Servlet environment with Spring MVC + and thus require the Spring MVC integration. Another team might use SWF in a Portlet + environment, and thus require the Portlet MVC integration. Another team might mix and match. + A major benefit of SWF is that it allows you to define reusable, self-contained controller + modules that can execute in any environment. + + + + + Architectural layers + + Spring Web Flow is a layered framework. A diagram of Spring Web Flow's layered architecture is + shown below: + + + + + + + + + + Spring Web Flow layer diagram + + + + + Layer descriptions + + Each layer is partitioned into one or more subsystems that together + carry out the layer's role within the overall system. This section notes + the purpose of each layer and describes each subsystem in the following format: + + + + + Subsystem name - The name of a layer subsystem. + + + + + Description - The purpose of the subsystem. + + + + + Packages - The Java packages that contain the source code for + the subsystem. The packages are rooted at the org.springframework.webflow + root package in the package hierarchy. + + + + + Subsystem interfaces - Central API elements exposed by the subsystem, + typically through Java interfaces. + + + + + Internal dependencies - Dependencies of the subsystem. These could be + other subsystems of the layer or external libraries. + + + + + The Execution Core Layer (Bottom Layer) + + Defines core flow definition and execution public APIs. As the "bottom layer", this + layer is highly stable with no dependencies on any other layer. + + + Execution Core Subsystems + + + + + + + + + Subsystem name + Description + Packages + Subsystem interfaces + Internal dependencies + + + + + Core + + Foundational, generic types usable by all other subsystems. + Contains the default expression parser (OGNL-based) and + core collection types (AttributeMap and company). + + + core, + core.collection + + None + None + + + Util + + Low level utilities used by all other parts of the system. + + + util + + None + None + + + Flow Definition + + Central abstractions for modeling flow definitions. + These abstractions include FlowDefinition, + StateDefinition, and TransitionDefinition + that form the domain language for describing flows. + + + definition + + + FlowDefinition + + Core + + + Flow Definition Registry + + Support for working with registries of flow definitions. Flow + definitions eligible for execution are typically stored in + a registry providing lookup services. + + + definition.registry + + + FlowDefinitionRegistry, + FlowDefinitionLocator + + + Core, + Flow Definition + + + + External Context + + Provides normalized access to a client environment that has called into Spring Web Flow. + + + context, + context.servlet, + context.portlet + + + ExternalContext + + + Core, + context.servlet requires Servlet API 2.3, + context.portlet requires Portlet API 1.0 in addition to Servlet API 2.3 + + + + Conversation + + Manages the creation and cleanup of conversational state. Used by + the execution repository system to begin new user conversations and + track execution state. + + + conversation, + conversation.impl + + + ConversationManager + + + Core, + Util, + External Context + + + + Flow Execution + + Stable runtime abstractions that define the flow definition + execution model. For executing flow definitions + and representing execution state. + + + execution, + execution.support, + execution.factory + + + FlowExecution + + + Core, + External Context, + Flow Definition + + + + Flow Execution Repository + + For persisting paused flow executions beyond a single request + into the server. + + + execution.repository, + execution.repository.support, + execution.repository.continuation + + + FlowExecutionRepository + + + Core, + Util, + Flow Definition, + Conversation, + Flow Execution, + repository.continuation requires commons-codec 1.0 if using client continuations + + + + Action + + Reusable action implementations. + + + action, + action.portlet + + + None + + + Core, + Util, + Flow Definition, + External Context, + Flow Execution + + + + +
+
+ + The Execution Engine Layer + + Defines an implementation of the flow execution core API, forming the basis + of the state machine or "engine" implementation. More volatile, as it contains + specific implementations of stable execution abstractions. + + + Depends On: Execution Core + + + Execution Engine Subsystems + + + + + + + + + Subsystem name + Description + Packages + Subsystem interfaces + Internal dependencies + + + + + Engine Implementation + + The implementation of the flow execution engine based on a finite state machine. + + + engine, + engine.support, + engine.impl + + None + None + + + Flow Definition Builder + + Abstractions used at configuration-time for building and assembling Flow definitions + executable by this engine implementation. Flows are typically defined + in externalized resources such as XML files. + + + engine.builder, + engine.builder.xml + + + FlowBuilder + + + Engine Implementation, + Spring Beans 1.2.7, + Spring Context 1.2.7, + builder.xml requires JDK 1.5 or Xerces for XSD support + + + + +
+
+ + The Test Layer + + Support for unit testing flow artifacts and system testing flow executions. + + + Depends On: Execution Engine, Execution Core + + + Test Subsystems + + + + + + + + + Subsystem name + Description + Packages + Subsystem interfaces + Internal dependencies + + + + + Engine Artifact Unit Test Support + + Support for unit testing implementations such as Actions in isolation. + + + test + + None + JUnit 3.8.1 + + + Flow Execution Test Support + + Support for testing Flow Executions out-of-container. + + + test.execution + + None + + Spring Beans 1.2.7, + JUnit 3.8.1 + + + + +
+
+ + The Executor Layer + + Stable higher-layer for driving and coordinating the execution of flow definitions. + This layer is decoupled from the more-volatile engine implementation. + + + Depends On: Execution Core + + + Executor Subsystems + + + + + + + + + Subsystem name + Description + Packages + Subsystem interfaces + Internal dependencies + + + + + Core + + Stable, generic flow executor abstractions and support. + + + executor, + executor.support + + + FlowExecutor + + None + + + Spring MVC + + The integration between Spring Web Flow and the Spring MVC framework. + + executor.mvc + None + + Core, + Spring Web MVC 1.2.7, + Portlet MVC requires Spring 2.0 + + + + Struts + + The integration between Spring Web Flow and the Struts Classic framework. + + + executor.struts + + None + + Core, + Struts 1.1 + + + + Java Server Faces (JSF) + + The integration between Spring Web Flow and the Java Server Faces framework. + + + executor.jsf + + None + + Core, + JSF 1.0 + + + + +
+
+ + The System Configuration Layer (Top Layer) + + The top-most layer for configuring the overall Spring Web Flow system for use + within an application. As the top layer, this layer depends on the most. + + + Depends On: Executor, Execution Engine, Execution Core + + + System Configuration Subsystems + + + + + + + + + Subsystem name + Description + Packages + Subsystem interfaces + Internal dependencies + + + + + Spring Configuration Support + + For configuring Spring Web Flow using Spring 1.x and 2.x. + + + config + + None + + Spring Beans 1.2.7, + spring-webflow-config-1.0 XSD support requires Spring 2.0 + + + + +
+ + + As described above, some subsystem packages are optional depending on your use of the + subsystem. For example, use of Spring Web Flow in a Servlet environment entails use of + the ExternalContext context.servlet package which requires the + Servlet API to be in the classpath. In this case the context.portlet package is not + used and the Portlet API is not required. + + + + For the exact list of dependencies, as well as supported product usage configurations, + see the Ivy dependency manager descriptor located within the SWF distribution. + +
+
+ + Support + + Spring Web Flow 1.0 is supported on Spring Framework 1.2.7 or > for the 1.x series and + supported on 2.0 or > for the 2.x series. + + + XML-based flow building requires Xerces 2 or JDK 5.0 (for XSD support). + + + The Spring Web Flow Portlet integration requires Spring Portlet MVC 2.0. + + + Our active community support forum is located at http://forum.springframework.org. + + +
\ No newline at end of file diff --git a/spring-webflow/docs/reference/src/practical.xml b/spring-webflow/docs/reference/src/practical.xml new file mode 100644 index 00000000..5744a8c4 --- /dev/null +++ b/spring-webflow/docs/reference/src/practical.xml @@ -0,0 +1,47 @@ + + + Practical Use of Spring Web Flow + + Sample applications + + It is recommended that you review the Spring Web Flow sample applications included in the + release distribution for best-practice illustrations of the features of this framework. + A description of each sample is provided below: + + + + + Phonebook - the original sample demonstrating most features (including subflows). + + + Sellitem - demonstrates a wizard with conditional transitions, flow scope, flow execution redirects, and continuations. + + + Flowlauncher - demonstrates all the possible ways to launch and resume flows. + + + Itemlist - demonstrates REST-style URLs and inline flows. + + + Shippingrate - demonstrates Spring Web Flow together with Ajax technology. + + + NumberGuess - demonstrates use of stateful middle-tier components to carry out business logic. + + + Birthdate - demonstrates Struts integration and the MultiAction. + + + Fileupload - demonstrates multipart file upload. + + + Phonebook-Portlet - the phonebook sample in a Portlet environment (notice how the flow definitions do not change). + + + Sellitem-JSF - the sellitem sample in a JSF environment (notice how the flow definition is more concise because + JSF takes care of data binding and validation). + + + + + diff --git a/spring-webflow/docs/reference/styles/fopdf.xsl b/spring-webflow/docs/reference/styles/fopdf.xsl new file mode 100644 index 00000000..c34ead23 --- /dev/null +++ b/spring-webflow/docs/reference/styles/fopdf.xsl @@ -0,0 +1,475 @@ + + + + + + + +]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Copyright ©right; 2004-2006 + + + , + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -5em + -5em + + + + + + + + + + + Spring Web Flow + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + bold + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + 0 + 1 + + 1 + + + + + + book toc + + + + 2 + + + + + + + + + + 0 + 0 + 0 + + + 5mm + 10mm + 10mm + + 15mm + 10mm + 0mm + + 18mm + 18mm + + + 0pc + + + + + justify + false + + + 11 + 8 + + + 1.4 + + + + + + + 0.8em + + + + + + 17.4cm + + + + 4pt + 4pt + 4pt + 4pt + + + + 0.1pt + 0.1pt + + + + + 1 + + + + + + + + left + bold + + + pt + + + + + + + + + + + + + + + 0.8em + 0.8em + 0.8em + + + pt + + 0.1em + 0.1em + 0.1em + + + 0.6em + 0.6em + 0.6em + + + pt + + 0.1em + 0.1em + 0.1em + + + 0.4em + 0.4em + 0.4em + + + pt + + 0.1em + 0.1em + 0.1em + + + + + bold + + + pt + + false + 0.4em + 0.6em + 0.8em + + + + + + + + + pt + + + + + 1em + 1em + 1em + #444444 + solid + 0.1pt + 0.5em + 0.5em + 0.5em + 0.5em + 0.5em + 0.5em + + + + 1 + + #F0F0F0 + + + + + + 0 + 1 + + + 90 + + + + + '1' + &admon_gfx_path; + + + + + + figure after + example before + equation before + table before + procedure before + + + + 1 + + + + 0.8em + 0.8em + 0.8em + 0.1em + 0.1em + 0.1em + + + + + + + + + + + + + + + + + diff --git a/spring-webflow/docs/reference/styles/html.css b/spring-webflow/docs/reference/styles/html.css new file mode 100644 index 00000000..54e123b1 --- /dev/null +++ b/spring-webflow/docs/reference/styles/html.css @@ -0,0 +1,277 @@ +body { + text-align: justify; + margin-right: 2em; + margin-left: 2em; +} + +a, +a[accesskey^="h"], +a[accesskey^="n"], +a[accesskey^="u"], +a[accesskey^="p"] { + font-family: Verdana, Arial, helvetica, sans-serif; + font-size: 12px; + color: #003399; +} + +a:active { + color: #003399; +} + +a:visited { + color: #888888; +} + +p { + font-family: Verdana, Arial; +} + +dt { + font-family: Verdana, Arial; + font-size: 12px; +} + +p, dl, dt, dd, blockquote { + color: #000000; + margin-bottom: 3px; + margin-top: 3px; + padding-top: 0px; +} + +ol, ul, p { + margin-top: 6px; + margin-bottom: 6px; +} + +p, blockquote { + font-size: 90%; +} + +p.releaseinfo { + font-size: 100%; + font-weight: bold; + font-family: Verdana, Arial, helvetica, sans-serif; + padding-top: 10px; +} + +p.pubdate { + font-size: 120%; + font-weight: bold; + font-family: Verdana, Arial, helvetica, sans-serif; +} + +td { + font-size: 80%; +} + +td, th, span { + color: #000000; +} + +td[width^="40%"] { + font-family: Verdana, Arial, helvetica, sans-serif; + font-size: 12px; + color: #003399; +} + +table[summary^="Navigation header"] tbody tr th[colspan^="3"] { + font-family: Verdana, Arial, helvetica, sans-serif; +} + +blockquote { + margin-right: 0px; +} + +h1, h2, h3, h4, h6, H6 { + color: #000000; + font-weight: 500; + margin-top: 0px; + padding-top: 14px; + font-family: Verdana, Arial, helvetica, sans-serif; + margin-bottom: 0px; +} + +h2.title { + font-weight: 800; + margin-bottom: 8px; +} + +h2.subtitle { + font-weight: 800; + margin-bottom: 20px; +} + +.firstname, .surname { + font-size: 12px; + font-family: Verdana, Arial, helvetica, sans-serif; +} + +table { + border-collapse: collapse; + border-spacing: 0; + border: 1px black; + empty-cells: hide; + margin: 10px 0px 30px 50px; + width: 90%; +} + +div.table { + margin: 30px 0px 30px 0px; + border: 1px dashed gray; + padding: 10px; +} + +div.table > p.title { + padding-left: 10px; +} + +table[summary^="Navigation footer"] { + border-collapse: collapse; + border-spacing: 0; + border: 1px black; + empty-cells: hide; + margin: 0px; + width: 100%; +} + +table[summary^="Note"], table[summary^="Warning"], table[summary^="Tip"] { + border-collapse: collapse; + border-spacing: 0; + border: 1px black; + empty-cells: hide; + margin: 10px 0px 10px -20px; + width: 100%; +} + +td { + padding: 4pt; + font-family: Verdana, Arial, helvetica, sans-serif; +} + +div.warning TD { + text-align: justify; +} + +h1 { + font-size: 150%; +} + +h2 { + font-size: 110%; +} + +h3 { + font-size: 100%; font-weight: bold; +} + +h4 { + font-size: 90%; font-weight: bold; +} + +h5 { + font-size: 90%; font-style: italic; +} + +h6 { + font-size: 100%; font-style: italic; +} + +tt { + font-size: 110%; + font-family: "Courier New", Courier, monospace; + color: #000000; +} + +.navheader, .navfooter { + border: none; +} + +div.navfooter table { + border: dashed gray; + border-width: 1px 1px 1px 1px; + background-color: #cde48d; +} + +pre { + font-size: 110%; + padding: 5px; + border-style: solid; + border-width: 1px; + border-color: #CCCCCC; + background-color: #F4F4F4; +} + +ul, ol, li { + list-style: disc; +} + +hr { + width: 100%; + height: 1px; + background-color: #CCCCCC; + border-width: 0px; + padding: 0px; +} + +.variablelist { + padding-top: 10px; + padding-bottom: 10px; + margin: 0; +} + +.term { + font-weight:bold; +} + +.mediaobject { + padding-top: 30px; + padding-bottom: 30px; +} + +.legalnotice { + font-family: Verdana, Arial, helvetica, sans-serif; + font-size: 12px; + font-style: italic; +} + +.sidebar { + float: right; + margin: 10px 0px 10px 30px; + padding: 10px 20px 20px 20px; + width: 33%; + border: 1px solid black; + background-color: #F4F4F4; + font-size: 14px; +} + +.property { + font-family: "Courier New", Courier, monospace; +} + +a code { + font-family: Verdana, Arial; + font-size: 12px; +} + +td code { + font-size: 110%; +} + +div.note * td, +div.tip * td, +div.warning * td { + text-align: justify; + font-size: 100%; +} + +.programlisting .interfacename, +.programlisting .literal, +.programlisting .classname { + font-size: 95%; +} + +/* everything in a is displayed in a nice green, comment-like color */ +.programlisting * .lineannotation, +.programlisting * .lineannotation * { + color: green; +} diff --git a/spring-webflow/docs/reference/styles/html.xsl b/spring-webflow/docs/reference/styles/html.xsl new file mode 100644 index 00000000..dd2ca2c1 --- /dev/null +++ b/spring-webflow/docs/reference/styles/html.xsl @@ -0,0 +1,116 @@ + + + + + +]> + + + + + + + + styles/html.css + + + 1 + 0 + 1 + 0 + + + + + + book toc + + + + 3 + + + + + 1 + + + + + + + 1 + &callout_gfx_path; + + + 90 + + + + + '1' + &admon_gfx_path; + + + + + figure after + example before + equation before + table before + procedure before + + + + , + + + + + + + + +
+

Authors

+

+ +

+
+ + + + +
+ + + +
+
+ + + + + +
diff --git a/spring-webflow/docs/reference/styles/html_chunk.xsl b/spring-webflow/docs/reference/styles/html_chunk.xsl new file mode 100644 index 00000000..ab302b50 --- /dev/null +++ b/spring-webflow/docs/reference/styles/html_chunk.xsl @@ -0,0 +1,210 @@ + + + + + +]> + + + + '5' + '1' + styles/html.css + + 1 + 0 + 1 + 0 + + + + book toc + + + 3 + + + 1 + + + + + 1 + &callout_gfx_path; + + 90 + + + '1' + &admon_gfx_path; + + + + figure after + example before + equation before + table before + procedure before + + + + , + + + + + + + + +
+

Authors

+

+ +

+
+ + + +
+ + + +
+
+ + + 1 + + + + + + + + + + + + +
diff --git a/spring-webflow/docs/reference/styles/tld.to.docbook.xsl b/spring-webflow/docs/reference/styles/tld.to.docbook.xsl new file mode 100644 index 00000000..f000f80b --- /dev/null +++ b/spring-webflow/docs/reference/styles/tld.to.docbook.xsl @@ -0,0 +1,245 @@ + + + + + + + + + + + + + + + + + + + + + -intro + + Introduction + + + + One of the view technologies you can use with the Spring Framework + is Java Server Pages (JSPs). To help you implement views using Java Server Pages + the Spring Framework provides you with some tags for evaluating errors, setting + themes and outputting internationalized messages. + + + + This appendix describes the + + + + tag library. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The + + + + tag + + + + + + + + + + + + .table + + + Attributes + + + + 3 + + + + description.span + + + Attribute + + + Runtime.Expression + + + left + + + + + center + + + Attribute + + + + + center + + + Required + + + + + center + + + Runtime.Expression + + + + + + + + center + + Attribute + + + + center + + Required? + + + + center + + Runtime Expression? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + description.span + + + + + + + + + + + + + description.span + + + + + + + + + + + . + + + diff --git a/spring-webflow/docs/reference/styles/xsd.to.docbook.xsl b/spring-webflow/docs/reference/styles/xsd.to.docbook.xsl new file mode 100644 index 00000000..acea84c9 --- /dev/null +++ b/spring-webflow/docs/reference/styles/xsd.to.docbook.xsl @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + -intro + + + Introduction + + + + + This appendix describes the + + + + schema. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The + + + + element + + + [TODO : insert the description of the element here] + + + + + + + . + + + diff --git a/spring-webflow/ivy.xml b/spring-webflow/ivy.xml new file mode 100644 index 00000000..47f1084d --- /dev/null +++ b/spring-webflow/ivy.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-webflow/license.txt b/spring-webflow/license.txt new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/spring-webflow/license.txt @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/spring-webflow/notice.txt b/spring-webflow/notice.txt new file mode 100644 index 00000000..ad7c1121 --- /dev/null +++ b/spring-webflow/notice.txt @@ -0,0 +1,22 @@ + ====================================================================== + == NOTICE file corresponding to section 4 d of the Apache License, == + == Version 2.0, for the Spring Web Flow distribution. == + ====================================================================== + + This product includes software developed by + the Apache Software Foundation (http://www.apache.org). + + The end-user documentation included with a redistribution, if any, + must include the following acknowledgement: + + "This product includes software developed by the Spring Framework + Project (http://www.springframework.org)." + + Alternately, this acknowledgement may appear in the software itself, + if and wherever such third-party acknowledgements normally appear. + + The names "Spring", "Spring Framework", and "Spring Web Flow" must + not be used to endorse or promote products derived from this + software without prior written permission. For written permission, + please contact rod.johnson@interface21.com or juergen.hoeller@interface21.com. + diff --git a/spring-webflow/pom.xml b/spring-webflow/pom.xml new file mode 100644 index 00000000..632c757a --- /dev/null +++ b/spring-webflow/pom.xml @@ -0,0 +1,155 @@ + + + 4.0.0 + org.springframework + spring-webflow + jar + Spring Web Flow + 1.0.1-SNAPSHOT + Spring Web Flow + http://www.springframework.org + + + The Apache Software License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + repo + + + + scm:svn:https://svn.sourceforge.net/svnroot/springframework/spring-projects + scm:svn:https://svn.sourceforge.net/svnroot/springframework/spring-projects + 1.0 + http://svn.sourceforge.net/viewcvs.cgi/springframework/spring-projects + + + Spring Framework + http://www.springframework.org/ + + + + + commons-codec + commons-codec + 1.1 + true + + + commons-logging + commons-logging + 1.0.4 + + + concurrent + concurrent + 1.3.4 + true + + + javax.portlet + portlet-api + 1.0 + provided + + + javax.servlet + servlet-api + 2.4 + provided + + + junit + junit + 3.8.1 + true + + + org.apache.myfaces.core + myfaces-api + 1.1.4 + provided + + + org.apache.myfaces.core + myfaces-impl + 1.1.4 + true + + + struts + struts + 1.2.9 + true + + + + org.springframework + spring-beans + 2.0 + + + org.springframework + spring-binding + ${project.version} + + + org.springframework + spring-context + 2.0 + + + org.springframework + spring-core + 2.0 + + + org.springframework + spring-portlet + 2.0 + true + + + org.springframework + spring-struts + 2.0 + true + + + org.springframework + spring-web + 2.0 + + + org.springframework + spring-webmvc + 2.0 + true + + + + log4j + log4j + 1.2.13 + runtime + true + + + + com.cenqua.clover + clover + 1.3.12 + test + + + easymock + easymock + 2.2 + test + + + org.springframework + spring-mock + 2.0 + test + + + \ No newline at end of file diff --git a/spring-webflow/project.properties b/spring-webflow/project.properties new file mode 100644 index 00000000..59eab701 --- /dev/null +++ b/spring-webflow/project.properties @@ -0,0 +1,11 @@ +# Properties defined in this file are overridable by a local build.properties in this project dir + +# The location of the common build system +common.build.dir=${basedir}/../common-build + +project.base.version=1.0.1 +#project.version=${project.base.version} +#ivy.status=release + +javac.source=1.3 +javac.target=1.3 diff --git a/spring-webflow/readme.txt b/spring-webflow/readme.txt new file mode 100644 index 00000000..dc3edeba --- /dev/null +++ b/spring-webflow/readme.txt @@ -0,0 +1,98 @@ +SPRING WEB FLOW 1.0 (October 2006) +---------------------------------------- +http://www.springframework.org/webflow +http://forum.springframework.org + +1. INTRODUCTION + +Spring Web Flow (SWF) is a component of the Spring Framework's web stack focused on the definition +and execution of user interface (UI) flow within a web application. + +The system allows you to capture a logical flow of your web application as a self-contained module +that can be reused in different situations. Such a flow guides a single user through the implementation +of a business task, and represents a single user conversation. Flows often execute across HTTP requests, +have state, exhibit transactional characteristics, and may be dynamic and/or long-running in nature. + +Spring Web Flow exists at a higher level of abstraction, integrating as a self-contained flow engine +within base frameworks such as Struts, Spring MVC, Portlet MVC, and JSF. SWF provides you the +capability to capture your application's UI flow explicitly in a declarative, portable, +and manageable fashion. SWF is a powerful controller framework based on a finite-state machine, +fully addressing the "C" in MVC. + +2. RELEASE INFO + +Spring Web Flow requires J2SE 1.3 and J2EE 1.3 (Servlet 2.3) or > to run. + +J2SE 5.0 with Ant 1.6 and Ivy 1.3 or > is required to build. A compatible version of Ivy +is shipped with this release. + +SWF release contents: + +"." contains Spring Web Flow distribution units (jars and source zip archives), readme, and copyright +"docs" contains the Spring Web Flow reference manual and API Javadocs +"ivys" contains Ivy dependency descriptors for the Spring Web Flow and Spring Data Binding projects +"projects" contains all buildable projects, including sample applications (each importable into Eclipse) +"projects/common-build" contains the Ant-based "common build system" used by all projects to compile/build/test +"projects/repository" contains Spring Web Flow dependencies (dependent jars) +"projects/spring-webflow/build-spring-webflow" contains the master build file used to build all Spring Web Flow projects +"projects/spring-webflow/spring-binding" contains buildable Spring Data Binding project sources, an internal library used by SWF +"projects/spring-webflow/spring-webflow" contains buildable Spring Web Flow project sources +"projects/spring-webflow/spring-webflow-samples" contains buildable Spring Web Flow sample application sources + +See the readme.txt within the above directories for additional information. + +Spring Web Flow is released under the terms of the Apache Software License (see license.txt). + +3. DISTRIBUTION JAR FILES + +The following distinct jar files are included in the distribution. This list +specifies the respective contents and third-party dependencies. Libraries in [brackets] are +optional, i.e. just necessary for certain functionality. + +* spring-webflow-1.0.jar +- Contents: The Spring Web Flow system +- Dependencies: Commons Logging, spring-beans, spring-core, spring-context, spring-web, spring-binding, OGNL + [Log4J, Commons Codec, Xerces, XML APIs, spring-webmvc, spring-mock, JUnit, Servlet API, + Portlet API, JMX, Struts, JSF] + +* spring-binding-1.0.jar +- Contents: The Spring Data Binding framework, an internal library used by SWF +- Dependencies: Commons Logging, spring-beans, spring-core, spring-context + [Log4J] + +For an exact list of Spring Web Flow project dependencies see "projects/spring-webflow/ivy.xml". + +4. WHERE TO START + +This distribution contains extensive documentation and sample applications illustrating the +features of Spring Web Flow. + +*** A great way to get started is to review and run the sample applications, supplimenting with +reference manual material as needed. To build deployable .war files for all samples, simply +access the projects/spring-webflow/build-spring-webflow directory and execute the "dist" target. +See the readme.txt in that directory for more additional information. *** + +More information on deploying SWF sample applications can be found at: + projects/spring-webflow/spring-webflow-samples/readme.txt + +5. ADDITIONAL RESOURCES + +The Spring Web Flow homepage is located at: + + http://www.springframework.org/webflow + +There you will find resources such as a 'Quick Start' guide and a 'Frequently Asked Questions' +section. + +The Spring Web Flow support forums are located at: + + http://forum.springframework.org + +There you will find an active community supporting the use of the product. + +The Spring Framework portal is located at: + + http://www.springframework.org + +There you will find links to many resources related to the Spring Framework, including on-line access +to Spring and Spring Web Flow documentation. \ No newline at end of file diff --git a/spring-webflow/src/etc/filter.properties b/spring-webflow/src/etc/filter.properties new file mode 100644 index 00000000..3aaac7e3 --- /dev/null +++ b/spring-webflow/src/etc/filter.properties @@ -0,0 +1,31 @@ +# Contains filterable project settings. Setting placeholders in filterable project text +# files will be replaced with these values when the 'statics' build target is run. +# +# You may add static settings directly to this source file in the format: +# setting=value e.g MY_SETTING=myvalue +# This is appropriate usage if you know the setting value will never change. +# +# At build time this source file is copied to the ${target.dir} where additional +# dynamic settings may be appended using the task. Use this approach +# when a setting value depends on the build or the local user's environment. +# +# An example of this approach is shown below: +# +# build.xml +# +# +# +# +# +# +# +# +# This allows for dynamic replacement values that are sourced from local properties files to facilitate +# local user settings. +# +# To refer to filterable settings within project source files like config files, JSPs, or +# other text files use the standard ant placeholder format: +# @SETTING_NAME@ e.g, @MY_SETTING@ and @MY_LOCAL_SETTING@ +# +# Your settings: diff --git a/spring-webflow/src/etc/test-resources/log4j.properties b/spring-webflow/src/etc/test-resources/log4j.properties new file mode 100644 index 00000000..eaa1b2ae --- /dev/null +++ b/spring-webflow/src/etc/test-resources/log4j.properties @@ -0,0 +1,18 @@ +log4j.rootCategory=WARN, stdout, logfile + +log4j.appender.stdout=org.apache.log4j.ConsoleAppender +log4j.appender.stdout.layout=org.apache.log4j.PatternLayout +log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.appender.logfile=org.apache.log4j.RollingFileAppender +log4j.appender.logfile.File=@TEST_RESULTS_DIR@/@PROJECT_NAME@.log +log4j.appender.logfile.MaxFileSize=512KB + +# Keep three backup files +log4j.appender.logfile.MaxBackupIndex=3 +log4j.appender.logfile.layout=org.apache.log4j.PatternLayout +#Pattern to output : date priority [category] - line_separator +log4j.appender.logfile.layout.ConversionPattern=%d %p [%c] - <%m>%n + +log4j.category.org.springframework.webflow=DEBUG +log4j.category.org.springframework.binding=DEBUG \ No newline at end of file diff --git a/spring-webflow/src/main/java/META-INF/spring.handlers b/spring-webflow/src/main/java/META-INF/spring.handlers new file mode 100644 index 00000000..151b6f67 --- /dev/null +++ b/spring-webflow/src/main/java/META-INF/spring.handlers @@ -0,0 +1 @@ +http\://www.springframework.org/schema/webflow-config=org.springframework.webflow.config.WebFlowConfigNamespaceHandler \ No newline at end of file diff --git a/spring-webflow/src/main/java/META-INF/spring.schemas b/spring-webflow/src/main/java/META-INF/spring.schemas new file mode 100644 index 00000000..f787288c --- /dev/null +++ b/spring-webflow/src/main/java/META-INF/spring.schemas @@ -0,0 +1 @@ +http\://www.springframework.org/schema/webflow-config/spring-webflow-config-1.0.xsd=org/springframework/webflow/config/spring-webflow-config-1.0.xsd \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/AbstractAction.java b/spring-webflow/src/main/java/org/springframework/webflow/action/AbstractAction.java new file mode 100644 index 00000000..715b6262 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/AbstractAction.java @@ -0,0 +1,279 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.ClassUtils; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.support.EventFactorySupport; + +/** + * Base action that provides assistance commonly needed by action + * implementations. This includes: + *
    + *
  • Implementing {@link InitializingBean} to receive an init callback + * when deployed within a Spring bean factory. + *
  • Exposing convenient event factory methods to create common result + * {@link Event} objects such as "success" and "error". + *
  • A hook for inserting action pre and post execution logic. + *
+ * + * @author Keith Donald + * @author Erwin Vervaet + */ +public abstract class AbstractAction implements Action, InitializingBean { + + /** + * Logger, usable in subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * Returns the helper delegate for creating action execution result events. + * @return the event factory support + */ + public EventFactorySupport getEventFactorySupport() { + return new EventFactorySupport(); + } + + public void afterPropertiesSet() throws Exception { + try { + initAction(); + } + catch (Exception ex) { + throw new BeanInitializationException("Initialization of this Action failed: " + ex.getMessage(), ex); + } + } + + /** + * Action initializing callback, may be overriden by subclasses to perform + * custom initialization logic. + *

+ * Keep in mind that this hook will only be invoked when this action is + * deployed in a Spring application context since it uses the Spring + * {@link InitializingBean} mechanism to trigger action initialisation. + */ + protected void initAction() throws Exception { + } + + /** + * Returns a "success" result event. + */ + protected Event success() { + return getEventFactorySupport().success(this); + } + + /** + * Returns a "success" result event with the provided result object as a + * parameter. + * @param result the action success result + */ + protected Event success(Object result) { + return getEventFactorySupport().success(this, result); + } + + /** + * Returns an "error" result event. + */ + protected Event error() { + return getEventFactorySupport().error(this); + } + + /** + * Returns an "error" result event caused by the provided exception. + * @param e the exception that caused the error event, to be configured as + * an event attribute + */ + protected Event error(Exception e) { + return getEventFactorySupport().error(this, e); + } + + /** + * Returns a "yes" result event. + */ + protected Event yes() { + return getEventFactorySupport().yes(this); + } + + /** + * Returns a "no" result event. + */ + protected Event no() { + return getEventFactorySupport().no(this); + } + + /** + * Returns yes() if the boolean result is true, no() if false. + * @param booleanResult the boolean + * @return yes or no + */ + protected Event result(boolean booleanResult) { + return getEventFactorySupport().event(this, booleanResult); + } + + /** + * Returns a result event for this action with the specified identifier. + * Typically called as part of return, for example: + * + *

+	 *     protected Event doExecute(RequestContext context) {
+	 *         // do some work
+	 *         if (some condition) {
+	 *             return result("success");
+	 *         } else {
+	 *             return result("error");
+	 *         }
+	 *     }
+	 * 
+ * + * Consider calling the error() or success() factory methods for returning + * common results. + * @param eventId the result event identifier + * @return the action result event + */ + protected Event result(String eventId) { + return getEventFactorySupport().event(this, eventId); + } + + /** + * Returns a result event for this action with the specified identifier and + * the specified set of attributes. Typically called as part of return, for + * example: + * + *
+	 *     protected Event doExecute(RequestContext context) {
+	 *         // do some work
+	 *         AttributeMap resultAttributes = new AttributeMap();
+	 *         resultAttributes.put("name", "value");
+	 *         if (some condition) {
+	 *             return result("success", resultAttributes);
+	 *         } else {
+	 *             return result("error", resultAttributes);
+	 *         }
+	 *     }
+	 * 
+ * + * Consider calling the error() or success() factory methods for returning + * common results. + * @param eventId the result event identifier + * @param resultAttributes the event attributes + * @return the action result event + */ + protected Event result(String eventId, AttributeMap resultAttributes) { + return getEventFactorySupport().event(this, eventId, resultAttributes); + } + + /** + * Returns a result event for this action with the specified identifier and + * a single attribute. + * @param eventId the result id + * @param resultAttributeName the attribute name + * @param resultAttributeValue the attribute value + * @return the action result event + */ + protected Event result(String eventId, String resultAttributeName, Object resultAttributeValue) { + return getEventFactorySupport().event(this, eventId, resultAttributeName, resultAttributeValue); + } + + public final Event execute(RequestContext context) throws Exception { + if (logger.isDebugEnabled()) { + logger.debug("Action '" + getActionNameForLogging() + "' beginning execution"); + } + Event result = doPreExecute(context); + if (result == null) { + result = doExecute(context); + if (logger.isDebugEnabled()) { + if (result != null) { + logger.debug("Action '" + getActionNameForLogging() + "' completed execution; result is '" + result.getId() + "'"); + } + else { + logger.debug("Action '" + getActionNameForLogging() + "' completed execution; result is [null]"); + } + } + doPostExecute(context); + } + else { + if (logger.isInfoEnabled()) { + logger.info("Action execution disallowed; pre-execution result is '" + result.getId() + "'"); + } + } + return result; + } + + // subclassing hooks + + /** + * Internal helper to return the name of this action for logging + * purposes. Defaults to the short class name. + * @see ClassUtils#getShortName(java.lang.Class) + */ + protected String getActionNameForLogging() { + return ClassUtils.getShortName(getClass()); + } + + /** + * Pre-action-execution hook, subclasses may override. If this method + * returns a non-null event, the doExecute() + * method will not be called and the returned event will be used to + * select a transition to trigger in the calling action state. If this + * method returns null, doExecute() will be + * called to obtain an action result event. + *

+ * This implementation just returns null. + * @param context the action execution context, for accessing and setting + * data in "flow scope" or "request scope" + * @return the non-null action result, in which case the + * doExecute() will not be called, or null if + * the doExecute() method should be called to obtain the + * action result + * @throws Exception an unrecoverable exception occured, either + * checked or unchecked + */ + protected Event doPreExecute(RequestContext context) throws Exception { + return null; + } + + /** + * Template hook method subclasses should override to encapsulate their + * specific action execution logic. + * @param context the action execution context, for accessing and setting + * data in "flow scope" or "request scope" + * @return the action result event + * @throws Exception an unrecoverable exception occured, either + * checked or unchecked + */ + protected abstract Event doExecute(RequestContext context) throws Exception; + + /** + * Post-action execution hook, subclasses may override. Will only be called + * if doExecute() was called, e.g. when doPreExecute() + * returned null. + *

+ * This implementation does nothing. + * @param context the action execution context, for accessing and setting + * data in "flow scope" or "request scope" + * @throws Exception an unrecoverable exception occured, either + * checked or unchecked + */ + protected void doPostExecute(RequestContext context) throws Exception { + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/AbstractBeanInvokingAction.java b/spring-webflow/src/main/java/org/springframework/webflow/action/AbstractBeanInvokingAction.java new file mode 100644 index 00000000..6c921354 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/AbstractBeanInvokingAction.java @@ -0,0 +1,150 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.convert.support.DefaultConversionService; +import org.springframework.binding.method.MethodInvoker; +import org.springframework.binding.method.MethodSignature; +import org.springframework.util.Assert; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +/** + * Base class for actions that delegate to methods on beans (POJOs - Plain Old + * Java Objects). Acts as an adapter that adapts an {@link Object} method to the + * Spring Web Flow {@link Action} contract. + *

+ * Subclasses are required to implement the {@link #getBean(RequestContext)} + * method, returning the bean on which a method should be invoked. + * + * @see BeanInvokingActionFactory + * + * @author Keith Donald + */ +public abstract class AbstractBeanInvokingAction extends AbstractAction { + + /** + * The signature of the method to invoke on the target bean, capable of + * resolving the method when used with a {@link MethodInvoker}. Required. + */ + private MethodSignature methodSignature; + + /** + * The method invoker that performs the action->bean method binding, + * accepting a {@link MethodSignature} and + * {@link #getBean(RequestContext) target bean} instance. + */ + private MethodInvoker methodInvoker = new MethodInvoker(); + + /** + * The specification (configuration) for how bean method return values + * should be exposed to an executing flow that invokes this action. + */ + private ActionResultExposer methodResultExposer; + + /** + * The strategy that adapts bean method return values to Event objects. + */ + private ResultEventFactory resultEventFactory = new SuccessEventFactory(); + + /** + * Creates a new bean invoking action. + * @param methodSignature the signature of the method to invoke + */ + protected AbstractBeanInvokingAction(MethodSignature methodSignature) { + Assert.notNull(methodSignature, "The signature of the target method to invoke is required"); + this.methodSignature = methodSignature; + } + + /** + * Returns the signature of the method to invoke on the target bean. + */ + public MethodSignature getMethodSignature() { + return methodSignature; + } + + /** + * Returns the configuration for how bean method return values should be + * exposed to an executing flow that invokes this action. + */ + public ActionResultExposer getMethodResultExposer() { + return methodResultExposer; + } + + /** + * Configures how bean method return values should be exposed to an + * executing flow that invokes this action. This is optional. By default the + * bean method return values do not get exposed to the executing flow. + */ + public void setMethodResultExposer(ActionResultExposer methodResultExposer) { + this.methodResultExposer = methodResultExposer; + } + + /** + * Returns the event adaption strategy used by this action. + */ + protected ResultEventFactory getResultEventFactory() { + return resultEventFactory; + } + + /** + * Set the bean return value->event adaption strategy. Defaults to + * {@link SuccessEventFactory}, so all bean method return values will be + * interpreted as "success". + */ + public void setResultEventFactory(ResultEventFactory resultEventFactory) { + this.resultEventFactory = resultEventFactory; + } + + /** + * Set the conversion service to perform type conversion of event parameters + * to method arguments as neccessary. + * Defaults to {@link DefaultConversionService}. + */ + public void setConversionService(ConversionService conversionService) { + methodInvoker.setConversionService(conversionService); + } + + /** + * Returns the bean method invoker helper. + */ + protected MethodInvoker getMethodInvoker() { + return methodInvoker; + } + + protected Event doExecute(RequestContext context) throws Exception { + Object bean = getBean(context); + Object returnValue = getMethodInvoker().invoke(methodSignature, bean, context); + if (methodResultExposer != null) { + methodResultExposer.exposeResult(returnValue, context); + } + return resultEventFactory.createResultEvent(bean, returnValue, context); + } + + // subclassing hooks + + /** + * Retrieves the bean to invoke a method on. Subclasses need to implement + * this method. + * @param context the flow execution request context + * @return the bean on which to invoke methods + * @throws Exception when the bean cannot be retreived + */ + protected abstract Object getBean(RequestContext context) throws Exception; + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/ActionResultExposer.java b/spring-webflow/src/main/java/org/springframework/webflow/action/ActionResultExposer.java new file mode 100644 index 00000000..98c801a7 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/ActionResultExposer.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import java.io.Serializable; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ScopeType; + +/** + * Specifies how an action result value should be exposed to an executing flow. + * The return value is exposed as an attribute in a configured scope. + * + * @see EvaluateAction + * @see AbstractBeanInvokingAction + * + * @author Keith Donald + */ +public class ActionResultExposer implements Serializable { + + /** + * The name of the attribute to index the return value with. + */ + private String resultName; + + /** + * The scope of the attribute indexing the return value. + */ + private ScopeType resultScope; + + /** + * Creates a action result exposer + * @param resultName the result name + * @param resultScope the result scope + */ + public ActionResultExposer(String resultName, ScopeType resultScope) { + Assert.notNull(resultName, "The result name is required"); + Assert.notNull(resultScope, "The result scope is required"); + this.resultName = resultName; + this.resultScope = resultScope; + } + + /** + * Returns name of the attribute to index the return value with. + */ + public String getResultName() { + return resultName; + } + + /** + * Returns the scope the attribute indexing the return value. + */ + public ScopeType getResultScope() { + return resultScope; + } + + /** + * Expose given bean method return value in given flow execution request + * context. + * @param result the return value + * @param context the request context + */ + public void exposeResult(Object result, RequestContext context) { + resultScope.getScope(context).put(resultName, result); + } + + public String toString() { + return new ToStringCreator(this).append("resultName", resultName).append("resultScope", resultScope).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/AttributeMapperAction.java b/spring-webflow/src/main/java/org/springframework/webflow/action/AttributeMapperAction.java new file mode 100644 index 00000000..35c160da --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/AttributeMapperAction.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.binding.mapping.MappingContext; +import org.springframework.util.Assert; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +/** + * Action that executes an attribute mapper to map information in the request + * context. Both the source and the target of the mapping will be the request + * context. This allows for maximum flexibility when defining attribute mapping + * expressions (e.g. "${flowScope.someAttribute}"). + *

+ * This action always returns the + * {@link org.springframework.webflow.action.AbstractAction#success() success} + * event. If something goes wrong while executing the mapping, an exception + * is thrown. + * + * @see org.springframework.binding.mapping.AttributeMapper + * @see org.springframework.webflow.execution.RequestContext + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class AttributeMapperAction extends AbstractAction { + + /** + * The attribute mapper strategy to delegate to perform the mapping. + */ + private AttributeMapper attributeMapper; + + /** + * Creates a new attribute mapper action that delegates to the configured + * attribute mapper to complete the mapping process. + * @param attributeMapper the mapper + */ + public AttributeMapperAction(AttributeMapper attributeMapper) { + Assert.notNull(attributeMapper, "The attribute mapper is required"); + this.attributeMapper = attributeMapper; + } + + protected Event doExecute(RequestContext context) throws Exception { + // map attributes from and to the request context + attributeMapper.map(context, context, getMappingContext(context)); + return success(); + } + + /** + * Returns a context containing extra data available during attribute mapping. + * The default implementation just returns null. Subclasses can + * override this if necessary. + */ + protected MappingContext getMappingContext(RequestContext context) { + return null; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/BeanInvokingActionFactory.java b/spring-webflow/src/main/java/org/springframework/webflow/action/BeanInvokingActionFactory.java new file mode 100644 index 00000000..97c40583 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/BeanInvokingActionFactory.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.method.MethodKey; +import org.springframework.binding.method.MethodSignature; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.execution.Action; + +/** + * A helper factory for {@link Action} instances that invoke methods on beans + * managed in a Spring bean factory. + *

+ * This factory encapsulates the logic required to take an arbitrary + * java.lang.Object from a Spring bean factory and adapt a method + * on it to the {@link Action} interface. If the bean you want to use is not + * managed in a Spring bean factory, consider subclassing + * {@link AbstractBeanInvokingAction} and using it directly. + * + * @see AbstractBeanInvokingAction + * + * @author Keith Donald + */ +public class BeanInvokingActionFactory { + + /** + * Determines which result event factory should be used for each bean + * invoking action created by this factory. + */ + private ResultEventFactorySelector resultEventFactorySelector = new ResultEventFactorySelector(); + + /** + * Returns the strategy for calculating the result event factory to + * configure for each bean invoking action created by this factory. + */ + public ResultEventFactorySelector getResultEventFactorySelector() { + return resultEventFactorySelector; + } + + /** + * Sets the strategy to calculate the result event factory to configure for + * each bean invoking action created by this factory. + */ + public void setResultEventFactorySelector(ResultEventFactorySelector resultEventFactorySelector) { + this.resultEventFactorySelector = resultEventFactorySelector; + } + + /** + * Factory method that creates a bean invoking action, an adapter that + * adapts a method on an abitrary {@link Object} to the {@link Action} + * interface. This method is an atomic operation that returns a fully + * initialized Action. It encapsulates the selection of the action + * implementation as well as the action assembly. + * @param beanId the id of the bean to be adapted to an Action instance + * @param beanFactory the bean factory where the bean is managed + * @param methodSignature the method to invoke on the bean when the action + * is executed (required) + * @param resultExposer the specification for what to do with the method + * return value (optional) + * @param conversionService the conversion service to be used to convert + * method parameters (optional) + * @param attributes attributes that may be used to affect the bean invoking + * action's construction + * @return the fully configured bean invoking action instance + */ + public Action createBeanInvokingAction(String beanId, BeanFactory beanFactory, MethodSignature methodSignature, + ActionResultExposer resultExposer, ConversionService conversionService, AttributeMap attributes) { + Object bean = beanFactory.getBean(beanId); + AbstractBeanInvokingAction action = new LocalBeanInvokingAction(methodSignature, bean); + action.setMethodResultExposer(resultExposer); + MethodKey methodKey = new MethodKey(bean.getClass(), methodSignature.getMethodName(), methodSignature + .getParameters().getTypesArray()); + action.setResultEventFactory(resultEventFactorySelector.forMethod(methodKey.getMethod())); + action.setConversionService(conversionService); + return action; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/CompositeAction.java b/spring-webflow/src/main/java/org/springframework/webflow/action/CompositeAction.java new file mode 100644 index 00000000..3d677771 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/CompositeAction.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +/** + * An action that will execute an ordered chain of other actions when executed. + *

+ * The event id of the last not-null result returned by the executed actions + * will be used as the result event id of the composite action. Lacking that, + * the action will return the "success" event. + *

+ * The resulting event will have an "actionResults" event attribute + * with a list of all events returned by the executed actions, including the null + * events. This allows you to relate an executed action and its result event by + * their index in the list. + *

+ * This is the classic GoF composite design pattern. + * + * @author Keith Donald + */ +public class CompositeAction extends AbstractAction { + + /** + * The resulting event whill have an attribute of this name which holds a + * list of all events returned by the executed actions. ("actionResults") + */ + public static final String ACTION_RESULTS_ATTRIBUTE_NAME = "actionResults"; + + /** + * The actions to execute. + */ + private Action[] actions; + + /** + * Should execution stop if one action returns an error event? + */ + private boolean stopOnError; + + /** + * Create a composite action composed of given actions. + * @param actions the actions + */ + public CompositeAction(Action[] actions) { + Assert.notEmpty(actions, "At least one action is required"); + this.actions = actions; + } + + /** + * Returns the actions contained by this composite action. + * @return the actions + */ + protected Action[] getActions() { + return actions; + } + + /** + * Returns the stop on error flag. + */ + public boolean isStopOnError() { + return stopOnError; + } + + /** + * Sets the stop on error flag. This determines whether or not execution + * should stop with the first action that returns an error event. In the + * error case, the composite action will also return the "error" event. + */ + public void setStopOnError(boolean stopOnError) { + this.stopOnError = stopOnError; + } + + public Event doExecute(RequestContext context) throws Exception { + Action[] actions = getActions(); + String eventId = getEventFactorySupport().getSuccessEventId(); + MutableAttributeMap eventAttributes = new LocalAttributeMap(); + List actionResults = new ArrayList(actions.length); + for (int i = 0; i < actions.length; i++) { + Event result = actions[i].execute(context); + actionResults.add(result); + if (result != null) { + eventId = result.getId(); + if (isStopOnError() && result.getId().equals(getEventFactorySupport().getErrorEventId())) { + break; + } + } + } + eventAttributes.put(ACTION_RESULTS_ATTRIBUTE_NAME, actionResults); + return new Event(this, eventId, eventAttributes); + } + + public String toString() { + return new ToStringCreator(this).append("actions", getActions()).append("stopOnError", isStopOnError()) + .toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/DefaultMultiActionMethodResolver.java b/spring-webflow/src/main/java/org/springframework/webflow/action/DefaultMultiActionMethodResolver.java new file mode 100644 index 00000000..d1be52dc --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/DefaultMultiActionMethodResolver.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import org.springframework.webflow.action.MultiAction.MethodResolver; +import org.springframework.webflow.execution.RequestContext; + +/** + * Default method resolver used by the MultiAction class. It uses the following + * algorithm to calculate a method name: + *

    + *
  1. If the currently executing action has a "method" property defined, use + * the value as method name.
  2. + *
  3. Else use the name of the current state of the flow execution as a method + * name.
  4. + *
+ * + * @see org.springframework.webflow.action.MultiAction + * + * @author Erwin Vervaet + */ +public class DefaultMultiActionMethodResolver implements MethodResolver { + + public String resolveMethod(RequestContext context) { + // implementation note: not using AnnotatedAction.METHOD_ATTRIBUTE since + // that resides in the engine subsystem + String method = context.getAttributes().getString("method"); + if (method == null) { + if (context.getCurrentState() != null) { + // default to the state id + method = context.getCurrentState().getId(); + } + else { + throw new IllegalStateException("Unable to resolve action method; no 'method' context attribute set"); + } + } + return method; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/EvaluateAction.java b/spring-webflow/src/main/java/org/springframework/webflow/action/EvaluateAction.java new file mode 100644 index 00000000..546e250b --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/EvaluateAction.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import org.springframework.binding.expression.EvaluationContext; +import org.springframework.binding.expression.Expression; +import org.springframework.util.Assert; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +/** + * An action that evaluates an expression and optionally exposes its result. + *

+ * Delegates to a helper {@link ResultEventFactorySelector} strategy to determine how + * to map the evaluation result to an action outcome {@link Event}. + * + * @see Expression + * @see ActionResultExposer + * @see ResultEventFactorySelector + * + * @author Keith Donald + */ +public class EvaluateAction extends AbstractAction { + + /** + * The expression to evaluate when this action is invoked. Required. + */ + private Expression expression; + + /** + * The helper that will expose the expression evaluation result. Optional. + */ + private ActionResultExposer evaluationResultExposer; + + /** + * The selector for the factory that will create the action result event + * callers can respond to. + */ + private ResultEventFactorySelector resultEventFactorySelector = new ResultEventFactorySelector(); + + /** + * Create a new evaluate action. + * @param expression the expression to evaluate + */ + public EvaluateAction(Expression expression) { + this(expression, null); + } + + /** + * Create a new evaluate action. + * @param expression the expression to evaluate + * @param evaluationResultExposer the strategy for how the expression result + * will be exposed to the flow + */ + public EvaluateAction(Expression expression, ActionResultExposer evaluationResultExposer) { + Assert.notNull(expression, "The expression this action should evaluate is required"); + this.expression = expression; + this.evaluationResultExposer = evaluationResultExposer; + } + + protected Event doExecute(RequestContext context) throws Exception { + Object result = expression.evaluate(context, getEvaluationContext(context)); + if (evaluationResultExposer != null) { + evaluationResultExposer.exposeResult(result, context); + } + return resultEventFactorySelector.forResult(result).createResultEvent(this, result, context); + } + + /** + * Template method subclasses may override to customize the expressin + * evaluation context. This implementation returns null. + * @param context the request context + * @return the evaluation context + */ + protected EvaluationContext getEvaluationContext(RequestContext context) { + return null; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/FormAction.java b/spring-webflow/src/main/java/org/springframework/webflow/action/FormAction.java new file mode 100644 index 00000000..aef66c31 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/FormAction.java @@ -0,0 +1,1106 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import java.lang.reflect.Method; + +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyEditorRegistrar; +import org.springframework.beans.PropertyEditorRegistry; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.propertyeditors.PropertiesEditor; +import org.springframework.core.style.StylerUtils; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.validation.BindException; +import org.springframework.validation.DataBinder; +import org.springframework.validation.Errors; +import org.springframework.validation.MessageCodesResolver; +import org.springframework.validation.Validator; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.util.DispatchMethodInvoker; +import org.springframework.webflow.util.ReflectionUtils; + +/** + * Multi-action that implements common logic dealing with input forms. This + * class leverages the Spring Web data binding code to do binding and + * validation. + *

+ * Several action execution methods are provided: + *

    + *
  • {@link #setupForm(RequestContext)} - Prepares the form object for + * display on a form, {@link #createFormObject(RequestContext) creating it} and + * an associated {@link Errors errors object} if necessary, then caching them in + * the configured {@link #getFormObjectScope() form object scope} and + * {@link #getFormErrorsScope() form errors scope}, respectively. Also + * {@link #registerPropertyEditors(PropertyEditorRegistry) installs} any custom + * property editors for formatting form object field values. This action method + * will return the "success" event unless an exception is thrown.
  • + *
  • {@link #bindAndValidate(RequestContext)} - Binds all incoming request + * parameters to the form object and then validates the form object using a + * {@link #setValidator(Validator) registered validator}. This action method + * will return the "success" event if there are no binding or validation errors, + * otherwise it will return the "error" event.
  • + *
  • {@link #bind(RequestContext)} - Binds all incoming request parameters to + * the form object. No additional validation is performed. This action method + * will return the "success" event if there are no binding errors, otherwise it + * will return the "error" event.
  • + *
  • {@link #validate(RequestContext)} - Validates the form object using a + * registered validator. No data binding is performed. This action method will + * return the "success" event if there are no validation errors, otherwise it + * will return the "error" event.
  • + *
  • {@link #resetForm(RequestContext)} - Resets the form by reloading the + * backing form object and reinstalling any custom property editors. Returns + * "success" on completion, an exception is thrown when a failure occurs.
  • + *
+ *

+ * Since this is a multi-action a subclass could add any number of additional + * action execution methods, e.g. "setupReferenceData(RequestContext)", or + * "processSubmit(RequestContext)". + *

+ * Using this action, it becomes very easy to implement form preparation and + * submission logic in your flow. One way to do this follows: + *

    + *
  1. Create a view state to display the form. In a render action of that + * state, invoke {@link #setupForm(RequestContext) setupForm} to prepare the new + * form for display.
  2. + *
  3. On a matching "submit" transition execute an action that invokes + * {@link #bindAndValidate(RequestContext) bindAndValidate} to bind incoming + * request parameters to the form object and validate the form object.
  4. + *
  5. If there are binding or validation errors, the transition will not be + * allowed and the view state will automatically be re-entered. + *
  6. If binding and validation is successful go to an action state called + * "processSubmit" (or any other appropriate name). This will invoke an action + * method called "processSubmit" you must provide on a subclass to process form + * submission, e.g. interacting with the business logic.
  7. + *
  8. If business processing is ok, continue to a view state to display the + * success view.
  9. + *
+ *

+ * Here is an example implementation of such a compact form flow: + * + *

+ *     <view-state id="displayCriteria" view="searchCriteria">
+ *         <render-actions>
+ *             <action bean="formAction" method="setupForm"/>
+ *         </render-actions>
+ *         <transition on="search" to="executeSearch">
+ *             <action bean="formAction" method="bindAndValidate"/>
+ *         </transition>
+ *     </view-state>
+ *                                                                                
+ *     <action-state id="executeSearch">
+ *         <action bean="formAction" method="executeSearch"/>
+ *         <transition on="success" to="displayResults"/>
+ *     </action-state>
+ * 
+ * + *

+ * When you need additional flexibility consider splitting the view state above + * acting as a single logical form state into multiple states. For + * example, you could have one action state handle form setup, a view state + * trigger form display, another action state handle data binding and + * validation, and another process form submission. This would be a bit more + * verbose but would also give you more control over how you respond to specific + * results of fine-grained actions that occur within the flow. + *

+ * Subclassing hooks: + *

    + *
  • A important hook is + * {@link #createFormObject(RequestContext) createFormObject}. You may override + * this to customize where the backing form object instance comes from (e.g + * instantiated transiently in memory or loaded from a database).
  • + *
  • An optional hook method provided by this class is + * {@link #initBinder(RequestContext, DataBinder) initBinder}. This is called + * after a new data binder is created. + *
  • Another optional ook method is + * {@link #registerPropertyEditors(PropertyEditorRegistry)}. By overriding it + * you can register any required property editors for your form. Instead of + * overriding this method, consider setting an explicit + * {@link org.springframework.beans.PropertyEditorRegistrar} strategy as a more + * reusable way to encapsulate custom PropertyEditor installation logic.
  • + *
  • Override {@link #validationEnabled(RequestContext)} to dynamically + * decide whether or not to do validation based on data available in the request + * context. + *
+ *

+ * Note that this action does not provide a referenceData() hook method + * similar to that of Spring MVC's SimpleFormController. If you + * wish to expose reference data to populate form drop downs you can define a + * custom action method in your FormAction subclass that does just that. Simply + * invoke it as either a chained action as part of the setupForm state, or as a + * fine grained state definition itself. + *

+ * For example, you might create this method in your subclass: + * + *

+ * public Event setupReferenceData(RequestContext context) throws Exception {
+ *     MutableAttributeMap requestScope = context.getRequestScope();
+ *     requestScope.put("refData", lookupService.getSupportingFormData());
+ *     return success();
+ * }
+ * 
+ * + * ... and then invoke it like this: + * + *
+ *     <view-state id="displayCriteria" view="searchCriteria">
+ *         <render-actions>
+ *             <action bean="searchFormAction" method="setupForm"/>
+ *             <action bean="searchFormAction" method="setupReferenceData"/>
+ *         </render-actions>
+ *         ...
+ *     </view-state>
+ * 
+ * + * This style of calling multiple action methods in a chain (Chain of + * Responsibility) is preferred to overridding a single action method. In + * general, action method overriding is discouraged. + *

+ * When it comes to validating submitted input data using a registered + * {@link org.springframework.validation.Validator}, this class offers the + * following options: + *

    + *
  • If you don't want validation at all, just call + * {@link #bind(RequestContext)} instead of + * {@link #bindAndValidate(RequestContext)} or don't register a validator.
  • + *
  • If you want piecemeal validation, e.g. in a multi-page wizard, call + * {@link #bindAndValidate(RequestContext)} or {@link #validate(RequestContext)} + * and specify a {@link #VALIDATOR_METHOD_ATTRIBUTE validatorMethod} action + * execution attribute. This will invoke the identified custom validator method + * on the validator. The validator method signature should follow the following + * pattern: + * + *
    + *     public void ${validateMethodName}(${formObjectClass}, Errors)
    + * 
    + * + * For instance, having a action definition like this: + * + *
    + *     <action bean="searchFormAction" method="bindAndValidate">
    + *         <attribute name="validatorMethod" value="validateSearchCriteria"/>
    + *     </action>
    + * 
    + * + * Would result in the + * public void validateSearchCriteria(SearchCriteria, Errors) method + * of the registered validator being called if the form object class would be + * SearchCriteria.
  • + *
  • If you want to do full validation using the + * {@link org.springframework.validation.Validator#validate(java.lang.Object, org.springframework.validation.Errors) validate} + * method of the registered validator, call + * {@link #bindAndValidate(RequestContext)} or {@link #validate(RequestContext)} + * without specifying a "validatorMethod" action execution attribute.
  • + *
+ * + *

+ * FormAction configurable properties
+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
namedefaultdescription
formObjectNameformObjectThe name of the form object. The form object will be set in the + * configured scope using this name.
formObjectClassnullThe form object class for this action. An instance of this class will + * get populated and validated. Required when using a validator.
formObjectScope{@link org.springframework.webflow.execution.ScopeType#FLOW flow}The scope in which the form object will be put. If put in flow scope the + * object will be cached and reused over the life of the flow, preserving + * previous values. Request scope will cause a new fresh form object instance to + * be created on each request into the flow execution.
formErrorsScope{@link org.springframework.webflow.execution.ScopeType#FLASH flash}The scope in which the form object errors instance will be put. If put + * in flash scope form errors will be cached until the next user event is signaled. + *
propertyEditorRegistrarnullThe strategy used to register custom property editors with the data + * binder. This is an alternative to overriding the + * {@link #registerPropertyEditors(PropertyEditorRegistry)} hook method.
validatornullThe validator for this action. The validator must support the specified + * form object class.
messageCodesResolvernullSet the strategy to use for resolving errors into message codes.
+ * + * @see org.springframework.beans.PropertyEditorRegistrar + * @see org.springframework.validation.DataBinder + * @see ScopeType + * + * @author Erwin Vervaet + * @author Keith Donald + */ +public class FormAction extends MultiAction implements InitializingBean { + + /* + * Implementation note: Uses deprecated DataBinder.getErrors() to remain + * compatible with Spring 1.2.x. + */ + + /* + * Implementation note: Introspects BindException at class init time to + * preserve 1.2.x compatability. + */ + private static boolean hasPropertyEditorRegistryAccessor; + + static { + hasPropertyEditorRegistryAccessor = ClassUtils + .hasMethod(BindException.class, "getPropertyEditorRegistry", null); + } + + /** + * The default form object name ("formObject"). + */ + public static final String DEFAULT_FORM_OBJECT_NAME = "formObject"; + + /** + * Optional attribute that identifies the method that should be invoked on + * the configured validator instance, to support piecemeal wizard page + * validation ("validatorMethod"). + */ + public static final String VALIDATOR_METHOD_ATTRIBUTE = "validatorMethod"; + + /** + * The name the form object should be exposed under. Default is + * {@link #DEFAULT_FORM_OBJECT_NAME}. + */ + private String formObjectName = DEFAULT_FORM_OBJECT_NAME; + + /** + * The type of form object, typically an instantiable class. Required if + * {@link #createFormObject(RequestContext)} is not overidden or when + * a validator is used. + */ + private Class formObjectClass; + + /** + * The scope in which the form object should be exposed. Default is + * {@link ScopeType#FLOW}. + */ + private ScopeType formObjectScope = ScopeType.FLOW; + + /** + * The scope in which the form object errors holder should be exposed. + * Default is {@link ScopeType#FLASH}. + */ + private ScopeType formErrorsScope = ScopeType.FLASH; + + /** + * A centralized service for property editor registration, for applying type + * conversion during form object data binding. Can be used as an alternative + * to overriding {@link #registerPropertyEditors(PropertyEditorRegistry)}. + */ + private PropertyEditorRegistrar propertyEditorRegistrar; + + /** + * A validator for the form's form object. + */ + private Validator validator; + + /** + * Strategy for resolving error message codes. + */ + private MessageCodesResolver messageCodesResolver; + + /** + * A cache for dispatched validator methods. + */ + private DispatchMethodInvoker validateMethodInvoker; + + /** + * Bean-style default constructor; creates a initially unconfigured + * FormAction instance relying on default property values. Clients invoking + * this constructor directly must set the {@link #formObjectClass} property + * or override {@link #createFormObject(RequestContext)}. + * @see #setFormObjectClass(Class) + */ + public FormAction() { + } + + /** + * Creates a new form action that manages instance(s) of the specified form + * object class. + * @param formObjectClass the class of the form object (must be instantiable) + */ + public FormAction(Class formObjectClass) { + setFormObjectClass(formObjectClass); + } + + /** + * Return the name of the form object in the configured scope. + */ + public String getFormObjectName() { + return formObjectName; + } + + /** + * Set the name of the form object in the configured scope. The form object + * will be included in the configured scope under this name. + */ + public void setFormObjectName(String formObjectName) { + this.formObjectName = formObjectName; + } + + /** + * Return the form object class for this action. + */ + public Class getFormObjectClass() { + return formObjectClass; + } + + /** + * Set the form object class for this action. An instance of this class will + * get populated and validated. This is a required property if you register + * a validator with the form action ({@link #setValidator(Validator)})! + *

+ * If no form object name is set at the moment this method is called, a + * form object name will be automatically generated based on the provided + * form object class using + * {@link ClassUtils#getShortNameAsProperty(java.lang.Class)}. + */ + public void setFormObjectClass(Class formObjectClass) { + this.formObjectClass = formObjectClass; + // generate a default form object name + if ((formObjectName == null || formObjectName == DEFAULT_FORM_OBJECT_NAME) && formObjectClass != null) { + formObjectName = ClassUtils.getShortNameAsProperty(formObjectClass); + } + } + + /** + * Get the scope in which the form object will be placed. + */ + public ScopeType getFormObjectScope() { + return formObjectScope; + } + + /** + * Set the scope in which the form object will be placed. The default + * if not set is {@link ScopeType#FLOW flow scope}. + */ + public void setFormObjectScope(ScopeType scopeType) { + this.formObjectScope = scopeType; + } + + /** + * Get the scope in which the Errors object will be placed. + */ + public ScopeType getFormErrorsScope() { + return formErrorsScope; + } + + /** + * Set the scope in which the Errors object will be placed. The default + * if not set is {@link ScopeType#FLASH flash scope}. + */ + public void setFormErrorsScope(ScopeType errorsScope) { + this.formErrorsScope = errorsScope; + } + + /** + * Get the property editor registration strategy for this action's data + * binders. + */ + public PropertyEditorRegistrar getPropertyEditorRegistrar() { + return propertyEditorRegistrar; + } + + /** + * Set a property editor registration strategy for this action's data + * binders. This is an alternative to overriding the + * {@link #registerPropertyEditors(PropertyEditorRegistry)} method. + */ + public void setPropertyEditorRegistrar(PropertyEditorRegistrar propertyEditorRegistrar) { + this.propertyEditorRegistrar = propertyEditorRegistrar; + } + + /** + * Returns the validator for this action. + */ + public Validator getValidator() { + return validator; + } + + /** + * Set the validator for this action. When setting a validator, you must also + * set the {@link #setFormObjectClass(Class) form object class}. The validator + * must support the specified form object class. + */ + public void setValidator(Validator validator) { + this.validator = validator; + } + + /** + * Return the strategy to use for resolving errors into message codes. + */ + public MessageCodesResolver getMessageCodesResolver() { + return messageCodesResolver; + } + + /** + * Set the strategy to use for resolving errors into message codes. Applies + * the given strategy to all data binders used by this action. + *

+ * Default is null, i.e. using the default strategy of the data binder. + * @see #createBinder(RequestContext, Object) + * @see org.springframework.validation.DataBinder#setMessageCodesResolver(org.springframework.validation.MessageCodesResolver) + */ + public void setMessageCodesResolver(MessageCodesResolver messageCodesResolver) { + this.messageCodesResolver = messageCodesResolver; + } + + protected void initAction() { + if (getValidator() != null) { + Assert.notNull(getFormObjectClass(), "When using a validator, the form object class is required"); + if (!getValidator().supports(getFormObjectClass())) { + throw new IllegalArgumentException("Validator [" + getValidator() + + "] does not support form object class [" + getFormObjectClass() + "]"); + } + // signature: public void ${validateMethodName}(${formObjectClass}, Errors) + validateMethodInvoker = new DispatchMethodInvoker(validator, new Class[] { getFormObjectClass(), + Errors.class }); + } + } + + // action methods + + /** + * Prepares a form object for display in a new form, creating it and caching + * it in the {@link #getFormObjectScope()} if necessary. Also installs + * custom property editors for formatting form object values in UI controls + * such as text fields. + *

+ * NOTE: This action method is not designed to be overidden and might + * become final in a future version of Spring Web Flow. If + * you need to execute custom form setup logic have your flow call this + * method along with your own custom methods as part of a single action + * chain. + * @param context the action execution context, for accessing and setting + * data in "flow scope" or "request scope" + * @return "success" when binding and validation is successful + * @throws Exception an unrecoverable exception occurs, either + * checked or unchecked + * @see #createFormObject(RequestContext) + */ + public Event setupForm(RequestContext context) throws Exception { + if (logger.isDebugEnabled()) { + logger.debug("Executing setupForm"); + } + // retrieve the form object, creating it if necessary + Object formObject = getFormObject(context); + ensureFormErrorsExposed(context, formObject); + return success(); + } + + /** + * Bind incoming request parameters to allowed fields of the form object and + * then validate the bound form object if a validator is configured. + *

+ * NOTE: This action method is not designed to be overidden and might + * become final in a future version of Spring Web Flow. If + * you need to execute custom bind and validate logic have your flow call + * this method along with your own custom methods as part of a single action + * chain. Alternatively, override the + * {@link #doBind(RequestContext, DataBinder)} or + * {@link #doValidate(RequestContext, Object, Errors)} hooks. + * @param context the action execution context, for accessing and setting + * data in "flow scope" or "request scope" + * @return "success" when binding and validation is successful, "error" if + * there were binding and/or validation errors + * @throws Exception an unrecoverable exception occured, either + * checked or unchecked + */ + public Event bindAndValidate(RequestContext context) throws Exception { + if (logger.isDebugEnabled()) { + logger.debug("Executing bind"); + } + Object formObject = getFormObject(context); + DataBinder binder = createBinder(context, formObject); + doBind(context, binder); + if (getValidator() != null && validationEnabled(context)) { + if (logger.isDebugEnabled()) { + logger.debug("Executing validation"); + } + doValidate(context, formObject, binder.getErrors()); + } + else { + if (logger.isDebugEnabled()) { + if (getValidator() == null) { + logger.debug("No validator is configured, no validation will occur after binding"); + } + else { + logger.debug("Validation was disabled for this bindAndValidate request"); + } + } + } + putFormErrors(context, binder.getErrors()); + return binder.getErrors().hasErrors() ? error() : success(); + } + + /** + * Bind incoming request parameters to allowed fields of the form object. + *

+ * NOTE: This action method is not designed to be overidden and might + * become final in a future version of Spring Web Flow. If + * you need to execute custom data binding logic have your flow call this + * method along with your own custom methods as part of a single action + * chain. Alternatively, override the + * {@link #doBind(RequestContext, DataBinder)} hook. + * @param context the action execution context, for accessing and setting + * data in "flow scope" or "request scope" + * @return "success" if there are no binding errors, "error" otherwise + * @throws Exception an unrecoverable exception occured, either + * checked or unchecked + */ + public Event bind(RequestContext context) throws Exception { + if (logger.isDebugEnabled()) { + logger.debug("Executing bind"); + } + Object formObject = getFormObject(context); + DataBinder binder = createBinder(context, formObject); + doBind(context, binder); + putFormErrors(context, binder.getErrors()); + return binder.getErrors().hasErrors() ? error() : success(); + } + + /** + * Validate the form object by invoking the validator if configured. + *

+ * NOTE: This action method is not designed to be overidden and might + * become final in a future version of Spring Web Flow. If + * you need to execute custom validation logic have your flow call this + * method along with your own custom methods as part of a single action + * chain. Alternatively, override the + * {@link #doValidate(RequestContext, Object, Errors)} hook. + * @param context the action execution context, for accessing and setting + * data in "flow scope" or "request scope" + * @return "success" if there are no validation errors, "error" otherwise + * @throws Exception an unrecoverable exception occured, either + * checked or unchecked + * @see #getValidator() + */ + public Event validate(RequestContext context) throws Exception { + if (getValidator() != null && validationEnabled(context)) { + if (logger.isDebugEnabled()) { + logger.debug("Executing validation"); + } + Object formObject = getFormObject(context); + Errors errors = getFormErrors(context); + doValidate(context, formObject, errors); + return errors.hasErrors() ? error() : success(); + } + else { + if (logger.isDebugEnabled()) { + if (getValidator() == null) { + logger.debug("No validator is configured, no validation will occur"); + } + else { + logger.debug("Validation was disabled for this request"); + } + } + return success(); + } + } + + /** + * Resets the form by clearing out the form object in the specified scope + * and recreating it. + *

+ * NOTE: This action method is not designed to be overidden and might + * become final in a future version of Spring Web Flow. If + * you need to execute custom reset logic have your flow call this method + * along with your own custom methods as part of a single action chain. + * @param context the request context + * @return "success" if the reset action completed successfully + * @throws Exception if an exception occured + * @see #createFormObject(RequestContext) + */ + public Event resetForm(RequestContext context) throws Exception { + Object formObject = initFormObject(context); + initFormErrors(context, formObject); + return success(); + } + + // internal helpers + + /** + * Create the new form object and put it in the configured + * {@link #getFormObjectScope() scope}. + * @param context the flow execution request context + * @return the new form object + * @throws Exception an exception occured creating the form object + */ + private Object initFormObject(RequestContext context) throws Exception { + if (logger.isDebugEnabled()) { + logger.debug("Creating new form object with name '" + getFormObjectName() + "'"); + } + Object formObject = createFormObject(context); + putFormObject(context, formObject); + return formObject; + } + + /** + * Put given form object in the configured scope of given context. + */ + private void putFormObject(RequestContext context, Object formObject) { + if (logger.isDebugEnabled()) { + logger.debug("Putting form object of type [" + formObject.getClass() + "] in scope " + getFormObjectScope() + + " with name '" + getFormObjectName() + "'"); + } + getFormObjectAccessor(context).putFormObject(formObject, getFormObjectName(), getFormObjectScope()); + } + + /** + * Initialize a new form object {@link Errors errors} instance in the + * configured {@link #getFormErrorsScope() scope}. This method also + * registers any {@link PropertiesEditor property editors} used to format + * form object property values. + * @param context the current flow execution request context + * @param formObject the form object for which errors will be tracked + */ + private Errors initFormErrors(RequestContext context, Object formObject) { + if (logger.isDebugEnabled()) { + logger.debug("Creating new form errors for object with name '" + getFormObjectName() + "'"); + } + Errors errors = createBinder(context, formObject).getErrors(); + putFormErrors(context, errors); + return errors; + } + + /** + * Put given errors instance in the configured scope of given context. + */ + private void putFormErrors(RequestContext context, Errors errors) { + if (logger.isDebugEnabled()) { + logger.debug("Putting form errors instance in scope " + getFormErrorsScope()); + } + getFormObjectAccessor(context).putFormErrors(errors, getFormErrorsScope()); + } + + /** + * Make sure a valid Errors instance for given form object is exposed + * in given context. + */ + private void ensureFormErrorsExposed(RequestContext context, Object formObject) { + if (!formErrorsExposed(context)) { + // initialize and expose a fresh errors instance to the flow with + // editors applied + initFormErrors(context, formObject); + } + else { + // trying to reuse an existing errors instance + if (formErrorsValid(context, formObject)) { + // reapply property editors against the existing errors instance + reinstallPropertyEditors(context); + } + else { + // the existing errors instance seems to be invalid + // initialize a new errors instance, but copy over error information + if (logger.isInfoEnabled()) { + logger.info("Fixing inconsistent Errors instance: initializing a new Errors instance " + + "wrapping from object '" + formObject + "' in scope '" + getFormErrorsScope() + + "' and copying over all existing error information."); + } + Errors invalidExistingErrors = + getFormObjectAccessor(context).getFormErrors(getFormObjectName(), getFormErrorsScope()); + Errors newErrors = initFormErrors(context, formObject); + newErrors.addAllErrors(invalidExistingErrors); + } + } + } + + /** + * Check if there is an Errors instance available in given + * context for given form object. + */ + private boolean formErrorsExposed(RequestContext context) { + return getFormObjectAccessor(context).getFormErrors(getFormObjectName(), getFormErrorsScope()) != null; + } + + /** + * Check if the Errors instance available in given context is valid for + * given form object. + */ + private boolean formErrorsValid(RequestContext context, Object formObject) { + Errors errors = getFormObjectAccessor(context).getFormErrors(getFormObjectName(), getFormErrorsScope()); + if (errors instanceof BindException) { + BindException be = (BindException)errors; + if (be.getTarget() != formObject) { + if (logger.isInfoEnabled()) { + logger.info("Inconsistency detected: the Errors instance in '" + getFormErrorsScope() + + "' does NOT wrap the current form object '" + formObject + "' of class " + + formObject.getClass() + + "; instead this Errors instance unexpectedly wraps the target object '" + be.getTarget() + + "' of class: " + be.getTarget().getClass() + "."); + } + return false; + } + else { + return true; + } + } + else { + return true; + } + } + + /** + * Re-registers property editors against the current form errors instance. + * @param context the flow execution request context + */ + private void reinstallPropertyEditors(RequestContext context) { + BindException errors = (BindException) + getFormObjectAccessor(context).getFormErrors(getFormObjectName(), getFormErrorsScope()); + registerPropertyEditors(context, getPropertyEditorRegistry(errors)); + } + + /** + * Obtain a property editor registry from given bind exception (errors + * instance). + */ + private PropertyEditorRegistry getPropertyEditorRegistry(BindException errors) { + Method accessor; + try { + if (hasPropertyEditorRegistryAccessor) { + accessor = errors.getClass().getMethod("getPropertyEditorRegistry", null); + } + else { + // only way to get at the registry in 1.2.8 or <. + accessor = errors.getClass().getDeclaredMethod("getBeanWrapper", null); + accessor.setAccessible(true); + } + } + catch (NoSuchMethodException e) { + throw new IllegalStateException( + "Unable to resolve property editor registry accessor method as expected - this should not happen"); + } + return (PropertyEditorRegistry)ReflectionUtils.invokeMethod(accessor, errors); + } + + /** + * Invoke specified validator method on the validator registered with this + * action. The validator method for piecemeal validation should have the + * following signature: + *

+	 *     public void ${validateMethodName}(${formObjectClass}, Errors)
+	 * 
+ * @param validatorMethod the name of the validator method to invoke + * @param formObject the form object + * @param errors possible binding errors + * @throws Exception when an unrecoverable exception occurs + */ + private void invokeValidatorMethod(String validatorMethod, Object formObject, Errors errors) throws Exception { + if (logger.isDebugEnabled()) { + logger.debug("Invoking piecemeal validator method '" + validatorMethod + "(" + getFormObjectClass() + + ", Errors)'"); + } + getValidateMethodInvoker().invoke(validatorMethod, new Object[] { formObject, errors }); + } + + // accessible helpers (subclasses could override if necessary) + + /** + * Convenience method that returns the form object for this form action. If + * not found in the configured scope, a new form object will be created by a + * call to {@link #createFormObject(RequestContext)} and exposed in the + * configured {@link #getFormObjectScope() scope}. + *

+ * The returned form object will become the + * {@link FormObjectAccessor#setCurrentFormObject(Object, ScopeType) current} + * form object. + * @param context the flow execution request context + * @return the form object + * @throws Exception when an unrecoverable exception occurs + */ + protected Object getFormObject(RequestContext context) throws Exception { + FormObjectAccessor accessor = getFormObjectAccessor(context); + Object formObject = accessor.getFormObject(getFormObjectName(), getFormObjectScope()); + if (formObject == null) { + formObject = initFormObject(context); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Found existing form object with name '" + getFormObjectName() + "' of type [" + + formObject.getClass() + "] in scope " + getFormObjectScope()); + } + accessor.setCurrentFormObject(formObject, getFormObjectScope()); + } + return formObject; + } + + /** + * Convenience method that returns the form object errors for this form + * action. If not found in the configured scope, a new form object errors + * will be created, initialized, and exposed in the confgured + * {@link #getFormErrorsScope() scope}. + *

+ * Keep in mind that an Errors instance wraps a form object, so a form + * object will also be created if required + * (see {@link #getFormObject(RequestContext)}). + * @param context the flow request context + * @return the form errors + * @throws Exception when an unrecoverable exception occurs + */ + protected Errors getFormErrors(RequestContext context) throws Exception { + Object formObject = getFormObject(context); + ensureFormErrorsExposed(context, formObject); + return getFormObjectAccessor(context).getFormErrors(getFormObjectName(), getFormErrorsScope()); + } + + /** + * Create a new binder instance for the given form object and request + * context. Can be overridden to plug in custom DataBinder subclasses. + *

+ * Default implementation creates a standard WebDataBinder, and invokes + * {@link #initBinder(RequestContext, DataBinder)} and + * {@link #registerPropertyEditors(PropertyEditorRegistry)}. + * @param context the action execution context, for accessing and setting + * data in "flow scope" or "request scope" + * @param formObject the form object to bind onto + * @return the new binder instance + * @see WebDataBinder + * @see #initBinder(RequestContext, DataBinder) + * @see #setMessageCodesResolver(MessageCodesResolver) + */ + protected DataBinder createBinder(RequestContext context, Object formObject) { + DataBinder binder = new WebDataBinder(formObject, getFormObjectName()); + if (messageCodesResolver != null) { + binder.setMessageCodesResolver(messageCodesResolver); + } + initBinder(context, binder); + registerPropertyEditors(context, binder); + return binder; + } + + /** + * Bind allowed parameters in the external context request parameter map to + * the form object using given binder. + * @param context the action execution context, for accessing and setting + * data in "flow scope" or "request scope" + * @param binder the data binder to use + */ + protected void doBind(RequestContext context, DataBinder binder) { + if (logger.isDebugEnabled()) { + logger.debug("Binding allowed request parameters in " + + StylerUtils.style(context.getExternalContext().getRequestParameterMap()) + + " to form object with name '" + binder.getObjectName() + "', pre-bind formObject toString = " + + binder.getTarget()); + if (binder.getAllowedFields() != null && binder.getAllowedFields().length > 0) { + logger.debug("(Allowed fields are " + StylerUtils.style(binder.getAllowedFields()) + ")"); + } + else { + logger.debug("(Any field is allowed)"); + } + } + binder.bind(new MutablePropertyValues(context.getRequestParameters().asMap())); + if (logger.isDebugEnabled()) { + logger.debug("Binding completed for form object with name '" + binder.getObjectName() + + "', post-bind formObject toString = " + binder.getTarget()); + logger.debug("There are [" + binder.getErrors().getErrorCount() + "] errors, details: " + + binder.getErrors().getAllErrors()); + } + } + + /** + * Validate given form object using a registered validator. If a + * "validatorMethod" action property is specified for the currently + * executing action, the identified validator method will be invoked. When + * no such property is found, the defualt validate() method + * is invoked. + * @param context the action execution context, for accessing and setting + * data in "flow scope" or "request scope" + * @param formObject the form object + * @param errors the errors instance to record validation errors in + * @throws Exception when an unrecoverable exception occurs + */ + protected void doValidate(RequestContext context, Object formObject, Errors errors) throws Exception { + Assert.notNull(validator, "The validator must not be null when attempting validation -- programmer error"); + String validatorMethodName = context.getAttributes().getString(VALIDATOR_METHOD_ATTRIBUTE); + if (StringUtils.hasText(validatorMethodName)) { + if (logger.isDebugEnabled()) { + logger.debug("Invoking validation method '" + validatorMethodName + "' on validator " + validator); + } + invokeValidatorMethod(validatorMethodName, formObject, errors); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Invoking validator " + validator); + } + getValidator().validate(formObject, errors); + } + if (logger.isDebugEnabled()) { + logger.debug("Validation completed for form object"); + logger.debug("There are [" + errors.getErrorCount() + "] errors, details: " + errors.getAllErrors()); + } + } + + /** + * Returns a dispatcher to invoke validation methods. Subclasses could + * override this to return a custom dispatcher. + */ + protected DispatchMethodInvoker getValidateMethodInvoker() { + return validateMethodInvoker; + } + + /** + * Factory method that returns a new form object accessor for accessing form + * objects in the provided request context. + * @param context the flow request context + * @return the accessor + */ + protected FormObjectAccessor getFormObjectAccessor(RequestContext context) { + return new FormObjectAccessor(context); + } + + // common subclassing hook methods + + /** + * Create the backing form object instance that should be managed by this + * {@link FormAction form action}. By default, will attempt to instantiate + * a new form object instance of type {@link #getFormObjectClass()} + * transiently in memory. + *

+ * Subclasses should override if they need to load the form object from a + * specific location or resource such as a database or filesystem. + *

+ * Subclasses should override if they need to customize how a transient form + * object is assembled during creation. + * @param context the action execution context for accessing flow data + * @return the form object + * @throws IllegalStateException if the {@link #getFormObjectClass()} + * property is not set and this method has not been overridden + * @throws Exception when an unrecoverable exception occurs + */ + protected Object createFormObject(RequestContext context) throws Exception { + if (formObjectClass == null) { + throw new IllegalStateException("Cannot create form object without formObjectClass property being set -- " + + "either set formObjectClass or override createFormObject"); + } + if (logger.isDebugEnabled()) { + logger.debug("Creating new instance of form object class [" + formObjectClass + "]"); + } + return formObjectClass.newInstance(); + } + + /** + * Initialize a new binder instance. This hook allows customization of + * binder settings such as the {@link DataBinder#getAllowedFields() allowed fields}, + * {@link DataBinder#getRequiredFields() required fields} and + * {@link DataBinder#initDirectFieldAccess() direct field access}. Called by + * {@link #createBinder(RequestContext, Object)}. + *

+ * Note that registration of custom property editors should be done in + * {@link #registerPropertyEditors(PropertyEditorRegistry)}, not here! This + * method will only be called when a new data binder is created. + * @param context the action execution context, for accessing and setting + * data in "flow scope" or "request scope" + * @param binder new binder instance + * @see #createBinder(RequestContext, Object) + */ + protected void initBinder(RequestContext context, DataBinder binder) { + } + + /** + * Register custom editors to perform type conversion on fields of your form + * object during data binding and form display. This method is called on + * form errors initialization and + * {@link #initBinder(RequestContext, DataBinder) data binder} initialization. + *

+ * Property editors give you full control over how objects are transformed + * to and from a formatted String form for display on a user interface such + * as a HTML page. + *

+ * This default implementation will call the + * {@link #registerPropertyEditors(PropertyEditorRegistry) simpler form} of + * the method not taking a RequestContext parameter. + * @param context the action execution context, for accessing and setting + * data in "flow scope" or "request scope" + * @param registry the property editor registry to register editors in + * @see #registerPropertyEditors(PropertyEditorRegistry) + */ + protected void registerPropertyEditors(RequestContext context, PropertyEditorRegistry registry) { + registerPropertyEditors(registry); + } + + /** + * Register custom editors to perform type conversion on fields of your form + * object during data binding and form display. This method is called on + * form errors initialization and + * {@link #initBinder(RequestContext, DataBinder) data binder} initialization. + *

+ * Property editors give you full control over how objects are transformed + * to and from a formatted String form for display on a user interface such + * as a HTML page. + *

+ * This default implementation will simply call registerCustomEditors + * on the {@link #getPropertyEditorRegistrar() propertyEditorRegistrar} object + * that has been set for the action, if any. + * @param registry the property editor registry to register editors in + */ + protected void registerPropertyEditors(PropertyEditorRegistry registry) { + if (propertyEditorRegistrar != null) { + if (logger.isDebugEnabled()) { + logger.debug("Registering custom property editors using configured registrar"); + } + propertyEditorRegistrar.registerCustomEditors(registry); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("No property editor registrar set, no custom editors to register"); + } + } + } + + /** + * Return whether validation should be performed given the state of the flow + * request context. Default implementation always returns true. + * @param context the request context, for accessing and setting data in + * "flow scope" or "request scope" + * @return whether or not validation is enabled + */ + protected boolean validationEnabled(RequestContext context) { + return true; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/FormObjectAccessor.java b/spring-webflow/src/main/java/org/springframework/webflow/action/FormObjectAccessor.java new file mode 100644 index 00000000..1426a165 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/FormObjectAccessor.java @@ -0,0 +1,231 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ScopeType; + +/** + * Convenience helper that encapsulates logic on how to retrieve and expose form + * objects and associated errors to and from a flow execution request context. + *

+ * Note: The form object available under the well known attribute name + * {@link #CURRENT_FORM_OBJECT_ATTRIBUTE} will be the last ("current") form + * object set in the request context. The same is true for the associated errors + * object. This implies that special care should be taken when accessing the + * form object using this alias if there are multiple form objects available in + * the flow execution request context! + * + * @see org.springframework.webflow.execution.RequestContext + * @see org.springframework.validation.Errors + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class FormObjectAccessor { + + /** + * The form object instance is aliased under this attribute name in the flow + * context by the default form setup and bind and validate actions. + *

+ * Note that if you would have multiple form objects in the request context, + * the last one that was used would be available using this alias! + *

+ * We need to keep track of the 'current form object' using this attribute + * to be able to deal with the limitations of some clients that can only + * deal with a single form backing object, e.g. Struts when using the Struts + * FlowAction. + */ + private static final String CURRENT_FORM_OBJECT_ATTRIBUTE = "currentFormObject"; + + /** + * The errors prefix. + */ + //use deprecated API to remain compatible with Spring 1.2.x + private static final String ERRORS_PREFIX = BindException.ERROR_KEY_PREFIX; + + /** + * The wrapped request context. + */ + private RequestContext context; + + /** + * Creates a form object accessor that wraps the given context. + * @param context the flow execution request context + */ + public FormObjectAccessor(RequestContext context) { + this.context = context; + } + + /** + * Returns the current form object name. + * @return the current form object name + */ + public static String getCurrentFormObjectName() { + return CURRENT_FORM_OBJECT_ATTRIBUTE; + } + + /** + * Returns the current form object errors attribute name. + * @return the current form object errors attribute name + */ + public static String getCurrentFormErrorsName() { + return ERRORS_PREFIX + getCurrentFormObjectName(); + } + + /** + * Gets the form object from the context, using the well-known attribute + * name {@link #CURRENT_FORM_OBJECT_ATTRIBUTE}. Will try all scopes. + * @return the form object, or null if not found + */ + public Object getCurrentFormObject() { + Object formObject = getCurrentFormObject(ScopeType.REQUEST); + if (formObject != null) { + return formObject; + } + formObject = getCurrentFormObject(ScopeType.FLASH); + if (formObject != null) { + return formObject; + } + formObject = getCurrentFormObject(ScopeType.FLOW); + if (formObject != null) { + return formObject; + } + return getCurrentFormObject(ScopeType.CONVERSATION); + } + + /** + * Gets the form object from the context, using the well-known attribute + * name {@link #CURRENT_FORM_OBJECT_ATTRIBUTE}. + * @param scopeType the scope to obtain the form object from + * @return the form object, or null if not found + */ + public Object getCurrentFormObject(ScopeType scopeType) { + return getFormObject(getCurrentFormObjectName(), scopeType); + } + + /** + * Expose given form object using the well known alias + * {@link #CURRENT_FORM_OBJECT_ATTRIBUTE} in the specified scope. + * @param formObject the form object + * @param scopeType the scope in which to expose the form object + */ + public void setCurrentFormObject(Object formObject, ScopeType scopeType) { + //don't call setFormObject since that would cause infinite recursion! + scopeType.getScope(context).put(getCurrentFormObjectName(), formObject); + } + + /** + * Gets the form object from the context, using the specified name. + * @param formObjectName the name of the form object in the context + * @param scopeType the scope to obtain the form object from + * @return the form object, or null if not found + */ + public Object getFormObject(String formObjectName, ScopeType scopeType) { + return scopeType.getScope(context).get(formObjectName); + } + + /** + * Gets the form object from the context, using the specified name. + * @param formObjectName the name of the form in the context + * @param formObjectClass the class of the form object, which will be + * verified + * @param scopeType the scope to obtain the form object from + * @return the form object, or null if not found + */ + public Object getFormObject(String formObjectName, Class formObjectClass, ScopeType scopeType) { + return scopeType.getScope(context).get(formObjectName, formObjectClass); + } + + /** + * Expose given form object using given name in specified scope. Given + * object will become the current form object. + * @param formObject the form object + * @param formObjectName the name of the form object + * @param scopeType the scope in which to expose the form object + */ + public void putFormObject(Object formObject, String formObjectName, ScopeType scopeType) { + scopeType.getScope(context).put(formObjectName, formObject); + setCurrentFormObject(formObject, scopeType); + } + + /** + * Gets the form object Errors tracker from the context, + * using the form object name {@link #CURRENT_FORM_OBJECT_ATTRIBUTE}. This + * method will search all scopes. + * @return the form object Errors tracker, or null if not found + */ + public Errors getCurrentFormErrors() { + Errors errors = getCurrentFormErrors(ScopeType.REQUEST); + if (errors != null) { + return errors; + } + errors = getCurrentFormErrors(ScopeType.FLASH); + if (errors != null) { + return errors; + } + errors = getCurrentFormErrors(ScopeType.FLOW); + if (errors != null) { + return errors; + } + return getCurrentFormErrors(ScopeType.CONVERSATION); + } + + /** + * Gets the form object Errors tracker from the context, + * using the form object name {@link #CURRENT_FORM_OBJECT_ATTRIBUTE}. + * @param scopeType the scope to obtain the errors from + * @return the form object Errors tracker, or null if not found + */ + public Errors getCurrentFormErrors(ScopeType scopeType) { + return getFormErrors(getCurrentFormObjectName(), scopeType); + } + + /** + * Expose given errors instance using the well known alias + * {@link #CURRENT_FORM_OBJECT_ATTRIBUTE} in the specified scope. + * @param errors the errors instance + * @param scopeType the scope in which to expose the errors instance + */ + public void setCurrentFormErrors(Errors errors, ScopeType scopeType) { + scopeType.getScope(context).put(getCurrentFormErrorsName(), errors); + } + + /** + * Gets the form object Errors tracker from the context, + * using the specified form object name. + * @param formObjectName the name of the Errors object, which will be + * prefixed with {@link BindException#ERROR_KEY_PREFIX} + * @param scopeType the scope to obtain the errors from + * @return the form object errors instance, or null if not found + */ + public Errors getFormErrors(String formObjectName, ScopeType scopeType) { + return (Errors)scopeType.getScope(context).get(ERRORS_PREFIX + formObjectName, Errors.class); + } + + /** + * Expose given errors instance in the specified scope. Given errors + * instance will become the current form errors instance. + * @param errors the errors object + * @param scopeType the scope to expose the errors in + */ + public void putFormErrors(Errors errors, ScopeType scopeType) { + scopeType.getScope(context).put(ERRORS_PREFIX + errors.getObjectName(), errors); + setCurrentFormErrors(errors, scopeType); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/LocalBeanInvokingAction.java b/spring-webflow/src/main/java/org/springframework/webflow/action/LocalBeanInvokingAction.java new file mode 100644 index 00000000..360f10b3 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/LocalBeanInvokingAction.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import java.io.Serializable; + +import org.springframework.binding.method.MethodSignature; +import org.springframework.util.Assert; +import org.springframework.webflow.execution.RequestContext; + +/** + * Thin action proxy that delegates to a method on an arbitrary bean. The bean + * instance is managed locally by this Action in an instance variable. + * + * @author Keith Donald + */ +class LocalBeanInvokingAction extends AbstractBeanInvokingAction implements Serializable { + + /** + * The target bean (any POJO) to invoke. + */ + private Object bean; + + /** + * Creates a bean invoking action that invokes a method on the specified bean. + * The bean may be a proxy providing a layer of indirection if necessary. + * @param bean the bean to invoke + */ + public LocalBeanInvokingAction(MethodSignature methodSignature, Object bean) { + super(methodSignature); + Assert.notNull(bean, "The bean to invoke by this action cannot be null"); + this.bean = bean; + } + + /** + * Returns the target bean to invoke methods on. + */ + public Object getBean() { + return bean; + } + + protected Object getBean(RequestContext context) throws Exception { + return getBean(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/MultiAction.java b/spring-webflow/src/main/java/org/springframework/webflow/action/MultiAction.java new file mode 100644 index 00000000..939c3e14 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/MultiAction.java @@ -0,0 +1,165 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import org.springframework.util.Assert; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.util.DispatchMethodInvoker; + +/** + * Action implementation that bundles two or more action execution methods into + * a single class. Action execution methods defined by subclasses must adhere to + * the following signature: + * + *

+ *     public Event ${method}(RequestContext context) throws Exception;
+ * 
+ * + * When this action is invoked, by default the id of the calling + * action state state is treated as the action execution method name. + * Alternatively, the execution method name may be explicitly specified as a + * attribute of the calling action state. + *

+ * For example, the following action state definition: + * + *

+ *     <action-state id="search">
+ *         <action bean="searchAction"/>
+ *         <transition on="success" to="results"/>
+ *     </action-state>
+ * 
+ * + * ... when entered, executes the method: + * + *
+ *     public Event search(RequestContext context) throws Exception;
+ * 
+ * + * Alternatively (and typically recommended), you may explictly specify the method name: + * + *
+ *     <action-state id="search">
+ *         <action bean="searchAction" method="executeSearch"/>
+ *         <transition on="success" to="results"/>
+ *     </action-state>
+ * 
+ * + *

+ * A typical use of the MultiAction is to centralize all command logic for a + * flow in one place. Another common use is to centralize form setup and submit + * logic in one place, or CRUD (create/read/update/delete) operations for a + * single domain object in one place. + * + * @see MultiAction.MethodResolver + * @see org.springframework.webflow.action.DefaultMultiActionMethodResolver + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class MultiAction extends AbstractAction { + + /** + * A cache for dispatched action execute methods. The default signature is + * public Event ${method}(RequestContext context) throws Exception;. + */ + private DispatchMethodInvoker methodInvoker; + + /** + * The action method resolver strategy. + */ + private MethodResolver methodResolver = new DefaultMultiActionMethodResolver(); + + /** + * Protected default constructor; not invokable for direct MultiAction instantiation. + * Intended for use by subclasses. + *

+ * Sets the target to this multi action instance. + * @see #setTarget(Object) + */ + protected MultiAction() { + setTarget(this); + } + + /** + * Constructs a multi action that invokes methods on the specified target + * object. Note: invokable methods on the target must conform to the multi action + * method signature: + *

+	 *       public Event ${method}(RequestContext context) throws Exception;
+	 * 
+ * @param target the target of this multi action's invocations + */ + public MultiAction(Object target) { + setTarget(target); + } + + /** + * Sets the target of this multi action's invocations. + * @param target the target + */ + protected final void setTarget(Object target) { + methodInvoker = new DispatchMethodInvoker(target, new Class[] { RequestContext.class } ); + } + + /** + * Get the strategy used to resolve action execution method names. + */ + public MethodResolver getMethodResolver() { + return methodResolver; + } + + /** + * Set the strategy used to resolve action execution method names. + * Allows full control over the method resolution algorithm. + * Defaults to {@link DefaultMultiActionMethodResolver}. + */ + public void setMethodResolver(MethodResolver methodResolver) { + this.methodResolver = methodResolver; + } + + protected final Event doExecute(RequestContext context) throws Exception { + String method = getMethodResolver().resolveMethod(context); + Object obj = methodInvoker.invoke(method, new Object[] { context }); + if (obj != null) { + Assert.isInstanceOf(Event.class, obj, + "The '" + method + "' action execution method on target object '" + + methodInvoker.getTarget() + "' did not return an Event object but '" + + obj + "' of type " + obj.getClass().getName() + " -- " + + "Programmer error; make sure the method signature conforms to " + + "'public Event ${method}(RequestContext context) throws Exception;'."); + } + return (Event)obj; + } + + /** + * Strategy interface used by the MultiAction to map a request context to + * the name of an action execution method. + * + * @author Keith Donald + * @author Erwin Vervaet + */ + public interface MethodResolver { + + /** + * Resolve a method name from given flow execution request context. + * @param context the flow execution request context + * @return the name of the method that should handle action + * execution + */ + public String resolveMethod(RequestContext context); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/ResultEventFactory.java b/spring-webflow/src/main/java/org/springframework/webflow/action/ResultEventFactory.java new file mode 100644 index 00000000..6e8eab2e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/ResultEventFactory.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +/** + * A strategy for creating an {@link Event} object from an arbitrary object + * such as an expression evaluation result or bean method return value. + * + * @author Keith Donald + */ +public interface ResultEventFactory { + + /** + * Create an event instance from the result object. + * @param source the source of the event + * @param resultObject the result object, typically the return value of a + * bean method + * @param context a flow execution request context + * @return the event + */ + public Event createResultEvent(Object source, Object resultObject, RequestContext context); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/ResultEventFactorySelector.java b/spring-webflow/src/main/java/org/springframework/webflow/action/ResultEventFactorySelector.java new file mode 100644 index 00000000..0f1e9804 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/ResultEventFactorySelector.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import java.lang.reflect.Method; + +/** + * Helper that selects the {@link ResultEventFactory} to use for + * a particular result object. + * + * @see EvaluateAction + * @see BeanInvokingActionFactory + * + * @author Keith Donald + */ +public class ResultEventFactorySelector { + + /** + * The event factory instance for mapping a return value to a success event. + */ + private SuccessEventFactory successEventFactory = new SuccessEventFactory(); + + /** + * The event factory instance for mapping a result object to an event, using + * the type of the result object as the mapping criteria. + */ + private ResultObjectBasedEventFactory resultObjectBasedEventFactory = new ResultObjectBasedEventFactory(); + + /** + * Select the appropriate result event factory for attempts to invoke the + * given method. + * @param method the method + * @return the result event factory + */ + public ResultEventFactory forMethod(Method method) { + return forType(method.getReturnType()); + } + + /** + * Select the appropriate result event factory for the given result. + * @param result the result + * @return the result event factory + */ + public ResultEventFactory forResult(Object result) { + if (result == null) { + return successEventFactory; + } + else { + return forType(result.getClass()); + } + } + + /** + * Select the appropriate result event factory for given result type. + * This implementation returns {@link ResultObjectBasedEventFactory} if the + * type is {@link ResultObjectBasedEventFactory#isMappedValueType(Class) mapped} + * by that result event factory, otherwise {@link SuccessEventFactory} is + * returned. + * @param resultType the result type + * @return the result event factory + */ + protected ResultEventFactory forType(Class resultType) { + if (resultObjectBasedEventFactory.isMappedValueType(resultType)) { + return resultObjectBasedEventFactory; + } + else { + return successEventFactory; + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/ResultObjectBasedEventFactory.java b/spring-webflow/src/main/java/org/springframework/webflow/action/ResultObjectBasedEventFactory.java new file mode 100644 index 00000000..024e421d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/ResultObjectBasedEventFactory.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import org.springframework.core.JdkVersion; +import org.springframework.core.enums.LabeledEnum; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.support.EventFactorySupport; + +/** + * Result object-to-event adapter interface that tries to do a + * sensible conversion of the result object into a web flow event. + * It uses the following conversion table: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Result object typeEvent idRemarks
null{@link org.springframework.webflow.execution.support.EventFactorySupport#getNullEventId()} 
{@link java.lang.Boolean} or boolean{@link org.springframework.webflow.execution.support.EventFactorySupport#getYesEventId()}/ + * {@link org.springframework.webflow.execution.support.EventFactorySupport#getNoEventId()} 
{@link org.springframework.core.enums.LabeledEnum}{@link org.springframework.core.enums.LabeledEnum#getLabel()}The result object will included in the event as an attribute + * named "result".
{@link java.lang.Enum}{@link java.lang.Enum#name()}The result object will included in the event as an attribute + * named "result".
{@link java.lang.String}The string. 
{@link org.springframework.webflow.execution.Event}The resulting event object. 
+ * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class ResultObjectBasedEventFactory extends EventFactorySupport implements ResultEventFactory { + + public Event createResultEvent(Object source, Object resultObject, RequestContext context) { + if (resultObject == null) { + // this handles the case where the declared result return type is mapped + // by this class but the value is null + return event(source, getNullEventId()); + } + else if (isBoolean(resultObject.getClass())) { + return event(source, ((Boolean)resultObject).booleanValue()); + } + else if (isLabeledEnum(resultObject.getClass())) { + String resultId = ((LabeledEnum)resultObject).getLabel(); + return event(source, resultId, getResultAttributeName(), resultObject); + } + else if (isJdk5Enum(resultObject.getClass())) { + // java.lang.Enum.toString() returns the name! + return event(source, resultObject.toString(), getResultAttributeName(), resultObject); + } + else if (isString(resultObject.getClass())) { + return event(source, (String)resultObject); + } + else if (isEvent(resultObject.getClass())) { + return (Event)resultObject; + } + else { + throw new IllegalArgumentException("Cannot deal with result object '" + resultObject + + "' of type '" + resultObject.getClass() + "'"); + } + } + + /** + * Check whether or not given type is mapped to a corresponding + * event using special mapping rules. + */ + public boolean isMappedValueType(Class type) { + return isBoolean(type) || isLabeledEnum(type) || isJdk5Enum(type) || isString(type) || isEvent(type); + } + + // internal helpers to determine the 'type' of a class + + private boolean isBoolean(Class type) { + return Boolean.class.equals(type) || boolean.class.equals(type); + } + + private boolean isLabeledEnum(Class type) { + return LabeledEnum.class.isAssignableFrom(type); + } + + private boolean isJdk5Enum(Class type) { + if (JdkVersion.getMajorJavaVersion() >= JdkVersion.JAVA_15) { + return type.isEnum(); + } + else { + return false; + } + } + + private boolean isString(Class type) { + return String.class.equals(type); + } + + private boolean isEvent(Class type) { + return Event.class.isAssignableFrom(type); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/SetAction.java b/spring-webflow/src/main/java/org/springframework/webflow/action/SetAction.java new file mode 100644 index 00000000..b5e59e06 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/SetAction.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import org.springframework.binding.expression.EvaluationContext; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.SettableExpression; +import org.springframework.util.Assert; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ScopeType; + +/** + * An action that sets an attribute in a {@link ScopeType scope} when executed. + * Always returns the "success" event. + * + * @author Keith Donald + */ +public class SetAction extends AbstractAction { + + /** + * The expression for setting the scoped attribute value. + */ + private SettableExpression attributeExpression; + + /** + * The target scope. + */ + private ScopeType scope; + + /** + * The expression for resolving the scoped attribute value. + */ + private Expression valueExpression; + + /** + * Creates a new set attribute action. + * @param attributeExpression the writeable attribute expression + * @param scope the target scope of the attribute + * @param valueExpression the evaluatable attribute value expression + */ + public SetAction(SettableExpression attributeExpression, ScopeType scope, Expression valueExpression) { + Assert.notNull(attributeExpression, "The attribute expression is required"); + Assert.notNull(scope, "The scope type is required"); + Assert.notNull(valueExpression, "The value expression is required"); + this.attributeExpression = attributeExpression; + this.scope = scope; + this.valueExpression = valueExpression; + } + + protected Event doExecute(RequestContext context) throws Exception { + EvaluationContext evaluationContext = getEvaluationContext(context); + Object value = valueExpression.evaluate(context, evaluationContext); + MutableAttributeMap scopeMap = scope.getScope(context); + attributeExpression.evaluateToSet(scopeMap, value, evaluationContext); + return success(); + } + + /** + * Template method subclasses may override to customize the expression + * evaluation context. This implementation returns null. + * @param context the request context + * @return the evaluation context + */ + protected EvaluationContext getEvaluationContext(RequestContext context) { + return null; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/SuccessEventFactory.java b/spring-webflow/src/main/java/org/springframework/webflow/action/SuccessEventFactory.java new file mode 100644 index 00000000..fbee260b --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/SuccessEventFactory.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.support.EventFactorySupport; + +/** + * Default implementation of the resultObject-to-event mapping interface. + * Always returns the "success" event. + * + * @author Keith Donald + */ +public class SuccessEventFactory extends EventFactorySupport implements ResultEventFactory { + + public Event createResultEvent(Object source, Object resultObject, RequestContext context) { + return success(source, resultObject); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/package.html b/spring-webflow/src/main/java/org/springframework/webflow/action/package.html new file mode 100644 index 00000000..677da84e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/package.html @@ -0,0 +1,15 @@ + + +

+Common action implementations invokable by flow definitions. +

+

+When implementing custom actions, consider subclassing {@link org.springframework.webflow.action.AbstractAction}. +Alternatively, you could also subclass {@link org.springframework.webflow.action.MultiAction} to bundle several action +execution methods in a single class. +

+

+The {@link org.springframework.webflow.action.FormAction} provides powerful input form handling functionality. +

+ + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/portlet/SetPortletModeAction.java b/spring-webflow/src/main/java/org/springframework/webflow/action/portlet/SetPortletModeAction.java new file mode 100644 index 00000000..fce9f4f4 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/portlet/SetPortletModeAction.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action.portlet; + +import javax.portlet.ActionResponse; +import javax.portlet.PortletMode; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.webflow.action.AbstractAction; +import org.springframework.webflow.context.portlet.PortletExternalContext; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +/** + * Action implementation that changes a PortletResponse mode. The action only + * generates the + * {@link org.springframework.webflow.action.AbstractAction#success()} event. + * All error cases result in an exception being thrown. + *

+ * This class is usefull when you want to change the current PortletMode before + * entering a specific state, e.g. it can be the first state in a subflow. + *

+ * Note: if you can, change the PortletMode using Portlet URLs (PortletURL class + * or portlet TAG). + * + * @author J.Enrique Ruiz + * @author César Ordiñana + * @author Erwin Vervaet + */ +public class SetPortletModeAction extends AbstractAction { + + /** + * The portlet mode to set can be specified in an action state action + * attribute with this name ("portletMode"). + */ + public static final String PORTLET_MODE_ATTRIBUTE = "portletMode"; + + /** + * The default portlet mode. Default is "view". + */ + private PortletMode portletMode = PortletMode.VIEW; + + /** + * Returns the mode that will be set in the response. + */ + public PortletMode getPortletMode() { + return portletMode; + } + + /** + * Sets the mode that will be set in the response. + */ + public void setPortletMode(PortletMode portletMode) { + Assert.notNull(portletMode, "The portlet mode is required and cannot be null"); + this.portletMode = portletMode; + } + + /** + * Sets the PortletMode. + * @param context the action execution context, for accessing and setting + * data in "flow scope" or "request scope" + * @return the action result event + * @throws Exception an unrecoverable exception occured, either + * checked or unchecked + */ + protected Event doExecute(RequestContext context) throws Exception { + Assert.isInstanceOf(PortletExternalContext.class, context.getExternalContext(), "'" + + ClassUtils.getShortName(this.getClass()) + "' can only work with 'PortletExternalContext': "); + PortletExternalContext portletContext = (PortletExternalContext)context.getExternalContext(); + if (portletContext.getResponse() instanceof ActionResponse) { + PortletMode mode = + (PortletMode)context.getAttributes().get(PORTLET_MODE_ATTRIBUTE, PortletMode.class, getPortletMode()); + ((ActionResponse)portletContext.getResponse()).setPortletMode(mode); + return success(); + } + else { + // portlet mode and the window state can be changed through + // ActionResponse only, if this is not the case, it means that this + // action has been invoked directly in a RenderRequest + throw new IllegalStateException( + "SetPortletModeAction can only be invoked within a Action request -- " + + "make sure you are not invoking it in a RenderRequest"); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/action/portlet/package.html b/spring-webflow/src/main/java/org/springframework/webflow/action/portlet/package.html new file mode 100644 index 00000000..c55506e5 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/action/portlet/package.html @@ -0,0 +1,7 @@ + + +

+Action implementations that define logic specific to flows executing in a JSR-168 Portlet environment. +

+ + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/config/ExecutionAttributesBeanDefinitionParser.java b/spring-webflow/src/main/java/org/springframework/webflow/config/ExecutionAttributesBeanDefinitionParser.java new file mode 100644 index 00000000..08cf43d3 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/config/ExecutionAttributesBeanDefinitionParser.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.config; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.config.MapFactoryBean; +import org.springframework.beans.factory.config.TypedStringValue; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; +import org.springframework.webflow.engine.support.ApplicationViewSelector; +import org.w3c.dom.Element; + +/** + * {@link BeanDefinitionParser} for the <execution-attributes> tag. + * + * @author Ben Hale + */ +class ExecutionAttributesBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + // elements and attributes + + private static final String ATTRIBUTE_ELEMENT = "attribute"; + + private static final String NAME_ATTRIBUTE = "name"; + + private static final String TYPE_ATTRIBUTE = "type"; + + private static final String VALUE_ATTRIBUTE = "value"; + + // properties + + private static final String SOURCE_MAP_PROPERTY = "sourceMap"; + + protected Class getBeanClass(Element element) { + return MapFactoryBean.class; + } + + protected void doParse(Element element, BeanDefinitionBuilder definitionBuilder) { + List attributeElements = DomUtils.getChildElementsByTagName(element, ATTRIBUTE_ELEMENT); + Map attributeMap = new ManagedMap(attributeElements.size()); + putAttributes(attributeMap, attributeElements); + putSpecialAttributes(attributeMap, element); + definitionBuilder.addPropertyValue(SOURCE_MAP_PROPERTY, attributeMap); + } + + /** + * Add all attributes defined in given list of attribute elements to given map. + */ + private void putAttributes(Map attributeMap, List attributeElements) { + for (Iterator i = attributeElements.iterator(); i.hasNext();) { + Element attributeElement = (Element)i.next(); + String type = attributeElement.getAttribute(TYPE_ATTRIBUTE); + Object value; + if (StringUtils.hasText(type)) { + value = new TypedStringValue(attributeElement.getAttribute(VALUE_ATTRIBUTE), type); + } else { + value = attributeElement.getAttribute(VALUE_ATTRIBUTE); + } + attributeMap.put(attributeElement.getAttribute(NAME_ATTRIBUTE), value); + } + } + + /** + * Add all non-generic (special) attributes defined in given element + * to given map. + */ + private void putSpecialAttributes(Map attributeMap, Element element) { + putAlwaysRedirectOnPauseAttribute(attributeMap, + DomUtils.getChildElementByTagName(element, ApplicationViewSelector.ALWAYS_REDIRECT_ON_PAUSE_ATTRIBUTE)); + } + + /** + * Parse the "alwaysRedirectOnPause" attribute from given element and + * add it to given map. + */ + private void putAlwaysRedirectOnPauseAttribute(Map attributeMap, Element element) { + if (element != null) { + Boolean value = Boolean.valueOf(element.getAttribute(VALUE_ATTRIBUTE)); + attributeMap.put(ApplicationViewSelector.ALWAYS_REDIRECT_ON_PAUSE_ATTRIBUTE, value); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/config/ExecutionListenersBeanDefinitionParser.java b/spring-webflow/src/main/java/org/springframework/webflow/config/ExecutionListenersBeanDefinitionParser.java new file mode 100644 index 00000000..df2e39d2 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/config/ExecutionListenersBeanDefinitionParser.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.config; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.springframework.beans.factory.config.RuntimeBeanReference; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.ManagedMap; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.util.xml.DomUtils; +import org.springframework.webflow.execution.factory.ConditionalFlowExecutionListenerLoader; +import org.w3c.dom.Element; + +/** + * {@link BeanDefinitionParser} for the <execution-listeners> + * tag. + * + * @author Ben Hale + */ +class ExecutionListenersBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + // elements and attributes + + private static final String LISTENER_ELEMENT= "listener"; + + // properties + + private static final String LISTENERS_PROPERTY = "listeners"; + + private static final String CRITERIA_ATTRIBUTE = "criteria"; + + private static final String REF_ATTRIBUTE = "ref"; + + protected Class getBeanClass(Element element) { + return ConditionalFlowExecutionListenerLoader.class; + } + + protected void doParse(Element element, BeanDefinitionBuilder definitionBuilder) { + List listenerElements = DomUtils.getChildElementsByTagName(element, LISTENER_ELEMENT); + definitionBuilder.addPropertyValue(LISTENERS_PROPERTY, getListenersWithCriteria(listenerElements)); + } + + /** + * Creates a map of listeners with their associated criteria. + * @param listeners the list of listener elements from the bean definition + * @return a map containing keys that are references to given listeners + * and values of string that represent the criteria + */ + private Map getListenersWithCriteria(List listeners) { + Map listenersWithCriteria = new ManagedMap(listeners.size()); + for (Iterator i = listeners.iterator(); i.hasNext();) { + Element listenerElement = (Element)i.next(); + RuntimeBeanReference ref = new RuntimeBeanReference(listenerElement.getAttribute(REF_ATTRIBUTE)); + String criteria = listenerElement.getAttribute(CRITERIA_ATTRIBUTE); + listenersWithCriteria.put(ref, criteria); + } + return listenersWithCriteria; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/config/ExecutorBeanDefinitionParser.java b/spring-webflow/src/main/java/org/springframework/webflow/config/ExecutorBeanDefinitionParser.java new file mode 100644 index 00000000..6ef3d740 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/config/ExecutorBeanDefinitionParser.java @@ -0,0 +1,110 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.config; + +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.ParserContext; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; +import org.w3c.dom.Element; + +/** + * {@link BeanDefinitionParser} for the <executor> tag. + * + * @author Ben Hale + */ +class ExecutorBeanDefinitionParser extends AbstractBeanDefinitionParser { + + // elements and attributes + + private static final String EXECUTION_ATTRIBUTES_ELEMENT = "execution-attributes"; + + private static final String EXECUTION_LISTENERS_ELEMENT = "execution-listeners"; + + private static final String REGISTRY_REF_ATTRIBUTE = "registry-ref"; + + private static final String REPOSITORY_TYPE_ATTRIBUTE = "repository-type"; + + // properties + + private static final String DEFINITION_LOCATOR_PROPERTY = "definitionLocator"; + + private static final String EXECUTION_ATTRIBUTES_PROPERTY = "executionAttributes"; + + private static final String EXECUTION_LISTENER_LOADER_PROPERTY = "executionListenerLoader"; + + private static final String REPOSITORY_TYPE_PROPERTY = "repositoryType"; + + protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder + .rootBeanDefinition(FlowExecutorFactoryBean.class); + definitionBuilder.addPropertyReference(DEFINITION_LOCATOR_PROPERTY, getRegistryRef(element)); + addExecutionAttributes(element, parserContext, definitionBuilder); + addExecutionListenerLoader(element, parserContext, definitionBuilder); + definitionBuilder.addPropertyValue(REPOSITORY_TYPE_PROPERTY, getRepositoryType(element)); + return definitionBuilder.getBeanDefinition(); + } + + /** + * Returns the name of the registry detailed in the bean definition. + * @param element the element to extract the registry name from + * @return the name of the registry + */ + private String getRegistryRef(Element element) { + String registryRef = element.getAttribute(REGISTRY_REF_ATTRIBUTE); + if (!StringUtils.hasText(registryRef)) { + throw new IllegalArgumentException("The 'registry-ref' attribute of the 'executor' element must have a value"); + } + return registryRef; + } + + /** + * Returns the name of the repository type enum field detailed in the bean + * definition. + * @param element the element to extract the repository type from + * @return the type of the repository + */ + private String getRepositoryType(Element element) { + return element.getAttribute(REPOSITORY_TYPE_ATTRIBUTE).toUpperCase(); + } + + /** + * Parse execution attribute definitions contained in given element. + */ + private void addExecutionAttributes(Element element, ParserContext parserContext, + BeanDefinitionBuilder definitionBuilder) { + Element attributesElement = DomUtils.getChildElementByTagName(element, EXECUTION_ATTRIBUTES_ELEMENT); + if (attributesElement != null) { + definitionBuilder.addPropertyValue(EXECUTION_ATTRIBUTES_PROPERTY, parserContext.getDelegate() + .parseCustomElement(attributesElement, definitionBuilder.getBeanDefinition())); + } + } + + /** + * Parse execution listener definitions contained in given element. + */ + private void addExecutionListenerLoader(Element element, ParserContext parserContext, + BeanDefinitionBuilder definitionBuilder) { + Element listenersElement = DomUtils.getChildElementByTagName(element, EXECUTION_LISTENERS_ELEMENT); + if (listenersElement != null) { + definitionBuilder.addPropertyValue(EXECUTION_LISTENER_LOADER_PROPERTY, parserContext.getDelegate() + .parseCustomElement(listenersElement, definitionBuilder.getBeanDefinition())); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/config/FlowExecutorFactoryBean.java b/spring-webflow/src/main/java/org/springframework/webflow/config/FlowExecutorFactoryBean.java new file mode 100644 index 00000000..6b93bcdd --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/config/FlowExecutorFactoryBean.java @@ -0,0 +1,411 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.config; + +import java.util.Map; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; +import org.springframework.webflow.conversation.ConversationManager; +import org.springframework.webflow.conversation.impl.SessionBindingConversationManager; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.registry.FlowDefinitionLocator; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.engine.impl.FlowExecutionImplStateRestorer; +import org.springframework.webflow.execution.FlowExecutionFactory; +import org.springframework.webflow.execution.FlowExecutionListener; +import org.springframework.webflow.execution.factory.FlowExecutionListenerLoader; +import org.springframework.webflow.execution.factory.StaticFlowExecutionListenerLoader; +import org.springframework.webflow.execution.repository.FlowExecutionRepository; +import org.springframework.webflow.execution.repository.continuation.ClientContinuationFlowExecutionRepository; +import org.springframework.webflow.execution.repository.continuation.ContinuationFlowExecutionRepository; +import org.springframework.webflow.execution.repository.support.FlowExecutionStateRestorer; +import org.springframework.webflow.execution.repository.support.SimpleFlowExecutionRepository; +import org.springframework.webflow.executor.FlowExecutor; +import org.springframework.webflow.executor.FlowExecutorImpl; + +/** + * The default flow executor factory implementation. As a FactoryBean, + * this class has been designed for use as a Spring managed bean. + *

+ * This factory encapsulates the construction and assembly of a + * {@link FlowExecutor}, including the provision of its + * {@link FlowExecutionRepository} strategy. + *

+ * The {@link #setDefinitionLocator(FlowDefinitionLocator) definition locator} + * property is required, all other properties are optional. + *

+ * This class has been designed with subclassing in mind. If you want to do advanced + * Spring Web Flow customization, e.g. using a custom + * {@link org.springframework.webflow.executor.FlowExecutor} implementation, + * consider subclassing this class and overriding one or more of the provided + * hook methods. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class FlowExecutorFactoryBean implements FactoryBean, InitializingBean { + + /** + * The locator the executor will use to access flow definitions registered + * in a central registry. Required. + */ + private FlowDefinitionLocator definitionLocator; + + /** + * Execution attributes to apply. + */ + private MutableAttributeMap executionAttributes; + + /** + * The loader that will determine which listeners to attach to flow definition executions. + */ + private FlowExecutionListenerLoader executionListenerLoader; + + /** + * The conversation manager to be used by the flow execution repository to + * store state associated with conversations driven by Spring Web Flow. + */ + private ConversationManager conversationManager; + + /** + * The maximum number of allowed concurrent conversations in the session. + */ + private Integer maxConversations; + + /** + * The type of execution repository to configure with executors created by + * this factory. Optional. Will fallback to default value if not set. + */ + private RepositoryType repositoryType; + + /** + * The maximum number of allowed continuations for a single conversation. + * Only used when the repository type is {@link RepositoryType#CONTINUATION}. + */ + private Integer maxContinuations; + + /** + * The flow executor this factory bean creates. + */ + private FlowExecutor flowExecutor; + + /** + * Spring Web Flow executor system defaults. + */ + private FlowSystemDefaults defaults = new FlowSystemDefaults(); + + /** + * Sets the flow definition locator that will locate flow definitions needed + * for execution. Typically also a {@link FlowDefinitionRegistry}. Required. + * @param definitionLocator the flow definition locator (registry) + */ + public void setDefinitionLocator(FlowDefinitionLocator definitionLocator) { + this.definitionLocator = definitionLocator; + } + + /** + * Sets the system attributes that apply to flow executions launched by the + * executor created by this factory. Execution attributes may affect flow + * execution behavior. + *

+ * Note: this method simply accepts a generic java.util.Map + * to allow for easy configuration by Spring. The map entries should consist + * of non-null String keys with object values. + * @param executionAttributes the flow execution system attributes + */ + public void setExecutionAttributes(Map executionAttributes) { + this.executionAttributes = new LocalAttributeMap(executionAttributes); + } + + /** + * Convenience setter that sets a single listener that always applies to flow + * executions launched by the executor created by this factory. + * @param executionListener the flow execution listener + */ + public void setExecutionListener(FlowExecutionListener executionListener) { + setExecutionListeners(new FlowExecutionListener[] { executionListener }); + } + + /** + * Convenience setter that sets a list of listeners that always apply to + * flow executions launched by the executor created by this factory. + * @param executionListeners the flow execution listeners + */ + public void setExecutionListeners(FlowExecutionListener[] executionListeners) { + setExecutionListenerLoader(new StaticFlowExecutionListenerLoader(executionListeners)); + } + + /** + * Sets the strategy for loading the listeners that will observe executions + * of a flow definition. Allows full control over what listeners should + * apply to executions of a flow definition launched by the executor created + * by this factory. + */ + public void setExecutionListenerLoader(FlowExecutionListenerLoader executionListenerLoader) { + this.executionListenerLoader = executionListenerLoader; + } + + /** + * Sets the type of flow execution repository that should be configured for + * the flow executors created by this factory. This factory encapsulates the + * construction of the repository implementation corresponding to the + * provided type. + * @param repositoryType the flow execution repository type + */ + public void setRepositoryType(RepositoryType repositoryType) { + this.repositoryType = repositoryType; + } + + /** + * Set the maximum number of continuation snapshots allowed for a single + * conversation when using the {@link RepositoryType#CONTINUATION continuation} + * flow execution repository. + * @see ContinuationFlowExecutionRepository#setMaxContinuations(int) + */ + public void setMaxContinuations(int maxContinuations) { + this.maxContinuations = new Integer(maxContinuations); + } + + /** + * Returns the configured maximum number of continuation snapshots allowed + * for a single conversation when using the + * {@link RepositoryType#CONTINUATION continuation} flow execution repository. + * @return the configured value or null if the user did not explicitly + * specify a value and wants to use the default + */ + protected Integer getMaxContinuations() { + return maxContinuations; + } + + /** + * Sets the strategy for managing conversations that should be configured + * for flow executors created by this factory. + *

+ * The conversation manager is used by the flow execution repository + * subsystem to begin and end new conversations that store execution state. + *

+ * By default, a {@link SessionBindingConversationManager} is used. Do not + * use {@link #setMaxConversations(int)} when using this method. + */ + public void setConversationManager(ConversationManager conversationManager) { + this.conversationManager = conversationManager; + } + + /** + * Set the maximum number of allowed concurrent conversations in the session. This + * is a convenience setter to allow easy configuration of the maxConversations + * property of the default {@link SessionBindingConversationManager}. Do not use + * this when using {@link #setConversationManager(ConversationManager)}. + * @see SessionBindingConversationManager#setMaxConversations(int) + */ + public void setMaxConversations(int maxConversations) { + this.maxConversations = new Integer(maxConversations); + } + + /** + * Returns the configured maximum number of allowed concurrent conversations + * in the session. Will only be used when using the default conversation manager, + * e.g. when no explicit conversation manager has been configured using + * {@link #setConversationManager(ConversationManager)}. + * @return the configured value or null if the user did not explicitly + * specify a value and wants to use the default + */ + protected Integer getMaxConversations() { + return maxConversations; + } + + /** + * Set system defaults that should be used. + * @param defaults the defaults to use. + */ + public void setDefaults(FlowSystemDefaults defaults) { + this.defaults = defaults; + } + + // implementing InitializingBean + + public void afterPropertiesSet() throws Exception { + Assert.notNull(definitionLocator, "The flow definition locator is required"); + + // apply defaults + executionAttributes = defaults.applyExecutionAttributes(executionAttributes); + repositoryType = defaults.applyIfNecessary(repositoryType); + + // pass all available parameters to the hook methods so that they + // can participate in the construction process + + // a factory for flow executions + FlowExecutionFactory executionFactory = + createFlowExecutionFactory(executionAttributes, executionListenerLoader); + + // a strategy to restore deserialized flow executions + FlowExecutionStateRestorer executionStateRestorer = + createFlowExecutionStateRestorer(definitionLocator, executionAttributes, executionListenerLoader); + + // a repository to store flow executions + FlowExecutionRepository executionRepository = + createExecutionRepository(repositoryType, executionStateRestorer, conversationManager); + + // combine all pieces of the puzzle to get an operational flow executor + flowExecutor = createFlowExecutor(definitionLocator, executionFactory, executionRepository); + } + + // subclassing hook methods + + /** + * Create the conversation manager to be used in the default case, e.g. when no + * explicit conversation manager has been configured using + * {@link #setConversationManager(ConversationManager)}. This implementation + * return a {@link SessionBindingConversationManager}. + * @return the default conversation manager + */ + protected ConversationManager createDefaultConversationManager() { + SessionBindingConversationManager conversationManager = new SessionBindingConversationManager(); + if (getMaxConversations() != null) { + conversationManager.setMaxConversations(getMaxConversations().intValue()); + } + return conversationManager; + } + + /** + * Create the flow execution factory to be used by the executor produced by this + * factory bean. Configure the execution factory appropriately. Subclasses may + * override if they which to use a custom execution factory, e.g. to use a custom + * FlowExecution implementation. + * @param executionAttributes execution attributes to apply to created executions + * @param executionListenerLoader decides which listeners to apply to created executions + * @return a new flow execution factory instance + */ + protected FlowExecutionFactory createFlowExecutionFactory( + AttributeMap executionAttributes, FlowExecutionListenerLoader executionListenerLoader) { + FlowExecutionImplFactory executionFactory = new FlowExecutionImplFactory(); + executionFactory.setExecutionAttributes(executionAttributes); + if (executionListenerLoader != null) { + executionFactory.setExecutionListenerLoader(executionListenerLoader); + } + return executionFactory; + } + + /** + * Create the flow execution state restorer to be used by the executor produced by + * this factory bean. Configure the state restorer appropriately. Subclasses may + * override if they which to use a custom state restorer implementation. + * @param definitionLocator the definition locator to use + * @param executionAttributes execution attributes to apply to restored executions + * @param executionListenerLoader decides which listeners should apply to restored + * flow executions + * @return a new state restorer instance + */ + protected FlowExecutionStateRestorer createFlowExecutionStateRestorer( + FlowDefinitionLocator definitionLocator, AttributeMap executionAttributes, + FlowExecutionListenerLoader executionListenerLoader) { + FlowExecutionImplStateRestorer executionStateRestorer = new FlowExecutionImplStateRestorer(definitionLocator); + executionStateRestorer.setExecutionAttributes(executionAttributes); + if (executionListenerLoader != null) { + executionStateRestorer.setExecutionListenerLoader(executionListenerLoader); + } + return executionStateRestorer; + } + + /** + * Factory method for creating the flow execution repository for saving and + * loading executing flows. Subclasses may override to customize the + * repository implementation used. + * @param repositoryType a hint indicating what type of repository to create + * @param executionStateRestorer the execution state restorer strategy to be used by + * the repository + * @param conversationManager the conversation manager specified by the user, + * could be null in which case the default conversation manager should be used + * @return a new flow execution repository instance + */ + protected FlowExecutionRepository createExecutionRepository( + RepositoryType repositoryType, FlowExecutionStateRestorer executionStateRestorer, + ConversationManager conversationManager) { + if (repositoryType == RepositoryType.CLIENT) { + if (conversationManager == null) { + // use the default no-op conversation manager + return new ClientContinuationFlowExecutionRepository(executionStateRestorer); + } + else { + // use the conversation manager specified by the user + return new ClientContinuationFlowExecutionRepository(executionStateRestorer, conversationManager); + } + } + else { + // determine the conversation manager to use + ConversationManager conversationManagerToUse = conversationManager; + if (conversationManagerToUse == null) { + conversationManagerToUse = createDefaultConversationManager(); + } + + if (repositoryType == RepositoryType.SIMPLE) { + return new SimpleFlowExecutionRepository(executionStateRestorer, conversationManagerToUse); + } + else if (repositoryType == RepositoryType.CONTINUATION) { + ContinuationFlowExecutionRepository repository = + new ContinuationFlowExecutionRepository(executionStateRestorer, conversationManagerToUse); + if (getMaxContinuations() != null) { + repository.setMaxContinuations(getMaxContinuations().intValue()); + } + return repository; + } + else if (repositoryType == RepositoryType.SINGLEKEY) { + SimpleFlowExecutionRepository repository = new SimpleFlowExecutionRepository( + executionStateRestorer, conversationManagerToUse); + repository.setAlwaysGenerateNewNextKey(false); + return repository; + } + else { + throw new IllegalStateException("Cannot create execution repository - unsupported repository type " + + repositoryType); + } + } + } + + /** + * Create the flow executor instance created by this factory bean and configure + * it appropriately. Subclasses may override if they which to use a custom executor + * implementation. + * @param definitionLocator the definition locator to use + * @param executionFactory the execution factory to use + * @param executionRepository the execution repository to use + * @return a new flow executor instance + */ + protected FlowExecutor createFlowExecutor( + FlowDefinitionLocator definitionLocator, FlowExecutionFactory executionFactory, + FlowExecutionRepository executionRepository) { + return new FlowExecutorImpl(definitionLocator, executionFactory, executionRepository); + } + + // implementing FactoryBean + + public Class getObjectType() { + return FlowExecutor.class; + } + + public boolean isSingleton() { + return true; + } + + public Object getObject() throws Exception { + return flowExecutor; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/config/FlowSystemDefaults.java b/spring-webflow/src/main/java/org/springframework/webflow/config/FlowSystemDefaults.java new file mode 100644 index 00000000..17d994e6 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/config/FlowSystemDefaults.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.config; + +import java.io.Serializable; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.engine.support.ApplicationViewSelector; + +/** + * Encapsulates overall flow system configuration defaults. Allows for + * centralized application of, and if necessary, overridding of system-wide + * default values. + * + * @author Keith Donald + */ +public class FlowSystemDefaults implements Serializable { + + /** + * The default 'alwaysRedirectOnPause' execution attribute value. + */ + private boolean alwaysRedirectOnPause = true; + + /** + * The default flow execution repository type. + */ + private RepositoryType repositoryType = RepositoryType.CONTINUATION; + + /** + * Overrides the alwaysRedirectOnPause execution attribute default. Defaults + * to "true". + * @param alwaysRedirectOnPause the new default value + * @see ApplicationViewSelector#ALWAYS_REDIRECT_ON_PAUSE_ATTRIBUTE + */ + public void setAlwaysRedirectOnPause(boolean alwaysRedirectOnPause) { + this.alwaysRedirectOnPause = alwaysRedirectOnPause; + } + + /** + * Overrides the default repository type. + * @param repositoryType the new default value + */ + public void setRepositoryType(RepositoryType repositoryType) { + this.repositoryType = repositoryType; + } + + /** + * Applies default execution attributes if necessary. Defaults will only + * apply in the case where the user did not configure a value, or explicitly + * requested the 'default' value. + * @param executionAttributes the user-configured execution attribute map + * @return the map with defaults applied as appropriate + */ + public MutableAttributeMap applyExecutionAttributes(MutableAttributeMap executionAttributes) { + if (executionAttributes == null) { + executionAttributes = new LocalAttributeMap(1, 1); + } + if (!executionAttributes.contains(ApplicationViewSelector.ALWAYS_REDIRECT_ON_PAUSE_ATTRIBUTE)) { + executionAttributes.put(ApplicationViewSelector.ALWAYS_REDIRECT_ON_PAUSE_ATTRIBUTE, + new Boolean(alwaysRedirectOnPause)); + } + return executionAttributes; + } + + /** + * Applies the default repository type if requested by the user. + * @param selectedType the selected repository type (may be null if no + * selection was made) + * @return the repository type, with the default applied if necessary + */ + public RepositoryType applyIfNecessary(RepositoryType selectedType) { + if (selectedType == null) { + return repositoryType; + } + else { + return selectedType; + } + } + + public String toString() { + return new ToStringCreator(this).append("alwaysRedirectOnPause", alwaysRedirectOnPause).append( + "repositoryType", repositoryType).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/config/RegistryBeanDefinitionParser.java b/spring-webflow/src/main/java/org/springframework/webflow/config/RegistryBeanDefinitionParser.java new file mode 100644 index 00000000..821b92f2 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/config/RegistryBeanDefinitionParser.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.config; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; +import org.springframework.webflow.engine.builder.xml.XmlFlowRegistryFactoryBean; +import org.w3c.dom.Element; + +/** + * {@link BeanDefinitionParser} for the <registry> tag. + * + * @author Ben Hale + */ +class RegistryBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + + // elements and attributes + + private static final String LOCATION_ELEMENT = "location"; + + // properties + + private static final String FLOW_LOCATIONS_PROPERTY = "flowLocations"; + + private static final String PATH_ATTRIBUTE = "path"; + + protected Class getBeanClass(Element element) { + return XmlFlowRegistryFactoryBean.class; + } + + protected void doParse(Element element, BeanDefinitionBuilder definitionBuilder) { + List locationElements = DomUtils.getChildElementsByTagName(element, LOCATION_ELEMENT); + List locations = getLocations(locationElements); + definitionBuilder.addPropertyValue(FLOW_LOCATIONS_PROPERTY, locations.toArray(new String[locations.size()])); + } + + /** + * Parse location definitions from given list of location elements. + */ + private List getLocations(List locationElements) { + List locations = new ArrayList(locationElements.size()); + for (Iterator i = locationElements.iterator(); i.hasNext();) { + Element locationElement = (Element)i.next(); + String path = locationElement.getAttribute(PATH_ATTRIBUTE); + if (StringUtils.hasText(path)) { + locations.add(path); + } + } + return locations; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/config/RepositoryType.java b/spring-webflow/src/main/java/org/springframework/webflow/config/RepositoryType.java new file mode 100644 index 00000000..ddc666da --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/config/RepositoryType.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.config; + +import org.springframework.core.enums.StaticLabeledEnum; +import org.springframework.webflow.execution.repository.continuation.ClientContinuationFlowExecutionRepository; +import org.springframework.webflow.execution.repository.continuation.ContinuationFlowExecutionRepository; +import org.springframework.webflow.execution.repository.support.SimpleFlowExecutionRepository; + +/** + * Type-safe enumeration of logical flow execution repository types. + * + * @see org.springframework.webflow.execution.repository.FlowExecutionRepository + * + * @author Keith Donald + */ +public class RepositoryType extends StaticLabeledEnum { + + /** + * The 'simple' flow execution repository type. + * @see SimpleFlowExecutionRepository + */ + public static RepositoryType SIMPLE = new RepositoryType(0, "Simple"); + + /** + * The 'continuation' flow execution repository type. + * @see ContinuationFlowExecutionRepository + */ + public static RepositoryType CONTINUATION = new RepositoryType(1, "Continuation"); + + /** + * The 'client' (continuation) flow execution repository type. + * @see ClientContinuationFlowExecutionRepository + */ + public static RepositoryType CLIENT = new RepositoryType(2, "Client"); + + /** + * The 'singleKey' flow execution repository type. + * @see SimpleFlowExecutionRepository + * @see SimpleFlowExecutionRepository#setAlwaysGenerateNewNextKey(boolean) + */ + public static RepositoryType SINGLEKEY = new RepositoryType(3, "Single Key"); + + /** + * Private constructor because this is a typesafe enum! + */ + private RepositoryType(int code, String label) { + super(code, label); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/config/WebFlowConfigNamespaceHandler.java b/spring-webflow/src/main/java/org/springframework/webflow/config/WebFlowConfigNamespaceHandler.java new file mode 100644 index 00000000..4a05c5e6 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/config/WebFlowConfigNamespaceHandler.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.config; + +import org.springframework.beans.factory.xml.BeanDefinitionParser; +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +/** + * NamespaceHandler for the webflow-config namespace. + *

+ * Provides {@link BeanDefinitionParser bean definition parsers} for the + * <executor> and <registry> tags. An + * executor tag can include an execution-listeners + * tag and a registry tag can include location + * tags. + *

+ * Using the executor tag you can configure a + * {@link FlowExecutorFactoryBean} that creates a + * {@link org.springframework.webflow.executor.FlowExecutor}. The + * executor tag allows you to specify the repository type and a + * reference to a registry. + * + *

+ *       <flow:executor id="registry" registry-ref="registry" repository-type="continuation" >
+ *           <flow:execution-listeners>
+ *               <flow:listener ref="listener1" />
+ *               <flow:listener ref="listener2" ref="*" />
+ *               <flow:listener ref="listener3" ref="flow1, flow2, flow3" />
+ *           <flow:execution-listeners />
+ *       </flow:executor>
+ * 
+ * + *

+ * Using the registry tag you can configure an + * {@link org.springframework.webflow.engine.builder.xml.XmlFlowRegistryFactoryBean} + * to create a registry for use by any number of executors. The + * registry tag supports in-line flow definition locations. + * + *

+ *       <flow:registry id="registry">
+ *           <flow:location path="/path/to/flow.xml" />
+ *           <flow:location path="/path/with/wildcards/*-flow.xml" />
+ *       </flow:registry>
+ * 
+ * + * @author Ben Hale + */ +public class WebFlowConfigNamespaceHandler extends NamespaceHandlerSupport { + + public void init() { + registerBeanDefinitionParser("execution-attributes", new ExecutionAttributesBeanDefinitionParser()); + registerBeanDefinitionParser("execution-listeners", new ExecutionListenersBeanDefinitionParser()); + registerBeanDefinitionParser("executor", new ExecutorBeanDefinitionParser()); + registerBeanDefinitionParser("registry", new RegistryBeanDefinitionParser()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/config/package.html b/spring-webflow/src/main/java/org/springframework/webflow/config/package.html new file mode 100644 index 00000000..6bc00aa1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/config/package.html @@ -0,0 +1,15 @@ + + +

+High-level flow system configuration support within a Spring environment. +

+

+This is the highest-layer package in the framework, responsible for providing +a clean interface for configuring Spring Web Flow. +

+

+This package also defines a Spring 2.0 custom XML namespace for configuring +the Spring Web Flow engine in a Spring 2.0 environment. +

+ + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/config/spring-webflow-config-1.0.xsd b/spring-webflow/src/main/java/org/springframework/webflow/config/spring-webflow-config-1.0.xsd new file mode 100644 index 00000000..6280e27a --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/config/spring-webflow-config-1.0.xsd @@ -0,0 +1,290 @@ + + + + + + +Provides an easy was to configure a flow executor and an XML flow definition registry. +]]> + + + + + + + + + +Each flow definition registered in this registry is assigned a unique identifier. By default, +this identifier is the name of the externalized resource minus its file extension. For example, +a registry containing flow definitions built from the files "orderitem-flow.xml" and "shipping-flow.xml" +would index those definitions by "orderitem-flow" and "shipping-flow" by default. +
+A flow registry is used by a flow executor at runtime to launch new executions of flow definitions. +]]> +
+
+ + + + + + + + +Individual paths such as: +
+	/WEB-INF/flows/orderitem-flow.xml
+
+... are supported as well as wildcard paths such as: +
+	/WEB-INF/flows/**/*-flow.xml
+
+]]> +
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
\ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/ExternalContext.java b/spring-webflow/src/main/java/org/springframework/webflow/context/ExternalContext.java new file mode 100644 index 00000000..df537042 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/ExternalContext.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.context; + +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.core.collection.ParameterMap; +import org.springframework.webflow.core.collection.SharedAttributeMap; + +/** + * A facade that provides normalized access to an external system that has + * interacted with Spring Web Flow. + *

+ * This context object provides a normalized interface for internal web flow + * artifacts to use to reason on and manipulate the state of an external actor + * calling into SWF to execute flows. It represents the context about a single, + * external client request to manipulate a flow execution. + *

+ * The design of this interface was inspired by JSF's own ExternalContext + * abstraction and shares the same name for consistency. If a particular + * external client type does not support all methods defined by this interface, + * they can just be implemented as returning an empty map or null. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface ExternalContext { + + /** + * Returns the path (or identifier) of the application that is executing. + * @return the application context path (e.g. "/myapp") + */ + public String getContextPath(); + + /** + * Returns the path (or identifier) of the dispatcher within the + * application that dispatched this request. + * @return the dispatcher path (e.g. "/dispatcher") + */ + public String getDispatcherPath(); + + /** + * Returns the path info of this external request. Could be null. + * @return the request path info (e.g. "/flows.htm") + */ + public String getRequestPathInfo(); + + /** + * Provides access to the parameters associated with the user request that + * led to SWF being called. This map is expected to be immutable and cannot + * be changed. + * @return the immutable request parameter map + */ + public ParameterMap getRequestParameterMap(); + + /** + * Provides access to the external request attribute map, providing a + * storage for data local to the current user request and accessible to both + * internal and external SWF artifacts. + * @return the mutable request attribute map + */ + public MutableAttributeMap getRequestMap(); + + /** + * Provides access to the external session map, providing a storage for data + * local to the current user session and accessible to both internal and + * external SWF artifacts. + * @return the mutable session attribute map + */ + public SharedAttributeMap getSessionMap(); + + /** + * Provides access to the global external session map, providing a storage for data + * globally accross the user session and accessible to both internal and + * external SWF artifacts. + *

+ * Note: most external context implementations do not distinguish between the concept of a + * "local" user session scope and a "global" session scope. The Portlet world does, but + * not the Servlet for example. In those cases calling this method returns the same + * map as calling {@link #getSessionMap()}. + * @return the mutable global session attribute map + */ + public SharedAttributeMap getGlobalSessionMap(); + + /** + * Provides access to the external application map, providing a storage for + * data local to the current user application and accessible to both + * internal and external SWF artifacts. + * @return the mutable application attribute map + */ + public SharedAttributeMap getApplicationMap(); + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/ExternalContextHolder.java b/spring-webflow/src/main/java/org/springframework/webflow/context/ExternalContextHolder.java new file mode 100644 index 00000000..b54de754 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/ExternalContextHolder.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.context; + +import org.springframework.util.Assert; + +/** + * Simple holder class that associates an {@link ExternalContext} instance with + * the current thread. The ExternalContext will be inherited by any child + * threads spawned by the current thread. + *

+ * Used as a central holder for the current ExternalContext in Spring Web Flow, + * wherever necessary. Often used by artifacts needing access to the current + * application session. + * + * @see ExternalContext + * + * @author Keith Donald + */ +public final class ExternalContextHolder { + + private static ThreadLocal externalContextHolder = new ThreadLocal(); + + /** + * Associate the given ExternalContext with the current thread. + * @param externalContext the current ExternalContext, or null + * to reset the thread-bound context + */ + public static void setExternalContext(ExternalContext externalContext) { + externalContextHolder.set(externalContext); + } + + /** + * Return the ExternalContext associated with the current thread, if any. + * @return the current ExternalContext, or null if none + */ + public static ExternalContext getExternalContext() { + Assert.state(externalContextHolder.get() != null, "No external context is bound to this thread"); + return (ExternalContext)externalContextHolder.get(); + } + + // not instantiable + private ExternalContextHolder() { + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/package.html b/spring-webflow/src/main/java/org/springframework/webflow/context/package.html new file mode 100644 index 00000000..186525e6 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/package.html @@ -0,0 +1,5 @@ + + +The external context subsystem for accessing the environment of a client that has called into Spring Web Flow. + + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletContextMap.java b/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletContextMap.java new file mode 100644 index 00000000..8201b81d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletContextMap.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.springframework.webflow.context.portlet; + +import java.util.Iterator; + +import javax.portlet.PortletContext; + +import org.springframework.binding.collection.SharedMap; +import org.springframework.binding.collection.StringKeyedMapAdapter; +import org.springframework.webflow.core.collection.CollectionUtils; + +/** + * A shared map backed by the Portlet context for accessing application scoped + * attributes. + * + * @author Keith Donald + */ +public class PortletContextMap extends StringKeyedMapAdapter implements SharedMap { + + /** + * The wrapped portlet context. + */ + private PortletContext context; + + /** + * Create a new map wrapping given portlet context. + */ + public PortletContextMap(PortletContext context) { + this.context = context; + } + + protected Object getAttribute(String key) { + return context.getAttribute(key); + } + + protected void setAttribute(String key, Object value) { + context.setAttribute(key, value); + } + + protected void removeAttribute(String key) { + context.removeAttribute(key); + } + + protected Iterator getAttributeNames() { + return CollectionUtils.toIterator(context.getAttributeNames()); + } + + public Object getMutex() { + return context; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletExternalContext.java b/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletExternalContext.java new file mode 100644 index 00000000..bcc44bb2 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletExternalContext.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.springframework.webflow.context.portlet; + +import java.util.Map; + +import javax.portlet.PortletContext; +import javax.portlet.PortletRequest; +import javax.portlet.PortletResponse; +import javax.portlet.PortletSession; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.LocalParameterMap; +import org.springframework.webflow.core.collection.LocalSharedAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.core.collection.ParameterMap; +import org.springframework.webflow.core.collection.SharedAttributeMap; + +/** + * Provides contextual information about a JSR-168 Portlet environment that has + * called into Spring Web Flow. + * + * @author Keith Donald + */ +public class PortletExternalContext implements ExternalContext { + + /** + * The context. + */ + private PortletContext context; + + /** + * The request. + */ + private PortletRequest request; + + /** + * The response. + */ + private PortletResponse response; + + /** + * Create an external context wrapping given Portlet context, request and response. + * @param context the Portlet context + * @param request the Portlet request + * @param response the Portlet response + */ + public PortletExternalContext(PortletContext context, PortletRequest request, PortletResponse response) { + this.context = context; + this.request = request; + this.response = response; + } + + public String getContextPath() { + return request.getContextPath(); + } + + public String getDispatcherPath() { + return null; + } + + public String getRequestPathInfo() { + return null; + } + + public ParameterMap getRequestParameterMap() { + return new LocalParameterMap(new PortletRequestParameterMap(request)); + } + + public MutableAttributeMap getRequestMap() { + return new LocalAttributeMap(new PortletRequestMap(request)); + } + + public SharedAttributeMap getSessionMap() { + return new LocalSharedAttributeMap(new PortletSessionMap(request, PortletSession.PORTLET_SCOPE)); + } + + public SharedAttributeMap getGlobalSessionMap() { + return new LocalSharedAttributeMap(new PortletSessionMap(request, PortletSession.APPLICATION_SCOPE)); + } + + public SharedAttributeMap getApplicationMap() { + return new LocalSharedAttributeMap(new PortletContextMap(context)); + } + + /** + * Returns the {@link PortletRequest#USER_INFO} map as a mutable attribute map. + * @return the Portlet user info + */ + public MutableAttributeMap getUserInfoMap() { + Map userInfo = (Map)request.getAttribute(PortletRequest.USER_INFO); + if (userInfo != null) { + return new LocalAttributeMap(userInfo); + } else { + return null; + } + } + + /** + * Returns the wrapped Portlet context. + */ + public PortletContext getContext() { + return context; + } + + /** + * Returns the wrapped Portlet request. + */ + public PortletRequest getRequest() { + return request; + } + + /** + * Returns the wrapped Portlet response. + */ + public PortletResponse getResponse() { + return response; + } + + public String toString() { + return new ToStringCreator(this).append("requestParameterMap", getRequestParameterMap()).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletRequestMap.java b/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletRequestMap.java new file mode 100644 index 00000000..948d0f31 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletRequestMap.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.springframework.webflow.context.portlet; + +import java.util.Iterator; + +import javax.portlet.PortletRequest; + +import org.springframework.binding.collection.StringKeyedMapAdapter; +import org.springframework.webflow.core.collection.CollectionUtils; + +/** + * Map backed by the Portlet request for accessing request scoped attributes. + * + * @author Keith Donald + */ +public class PortletRequestMap extends StringKeyedMapAdapter { + + /** + * The wrapped portlet request. + */ + private PortletRequest request; + + /** + * Create a new map wrapping the attributes of given portlet request. + */ + public PortletRequestMap(PortletRequest request) { + this.request = request; + } + + protected Object getAttribute(String key) { + return request.getAttribute(key); + } + + protected void setAttribute(String key, Object value) { + request.setAttribute(key, value); + } + + protected void removeAttribute(String key) { + request.removeAttribute(key); + } + + protected Iterator getAttributeNames() { + return CollectionUtils.toIterator(request.getAttributeNames()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletRequestParameterMap.java b/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletRequestParameterMap.java new file mode 100644 index 00000000..63d98b92 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletRequestParameterMap.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.springframework.webflow.context.portlet; + +import java.util.Iterator; + +import javax.portlet.PortletRequest; + +import org.springframework.binding.collection.CompositeIterator; +import org.springframework.binding.collection.StringKeyedMapAdapter; +import org.springframework.web.portlet.multipart.MultipartActionRequest; +import org.springframework.webflow.core.collection.CollectionUtils; + +/** + * Map backed by the Portlet request parameter map for accessing request local + * portlet parameters. + * + * @author Keith Donald + */ +public class PortletRequestParameterMap extends StringKeyedMapAdapter { + + /** + * The wrapped portlet request. + */ + private PortletRequest request; + + /** + * Create a new map wrapping the parameters of given portlet request. + */ + public PortletRequestParameterMap(PortletRequest request) { + this.request = request; + } + + protected Object getAttribute(String key) { + if (request instanceof MultipartActionRequest) { + MultipartActionRequest multipartRequest = (MultipartActionRequest)request; + Object data = multipartRequest.getFileMap().get(key); + if (data != null) { + return data; + } + } + String[] parameters = request.getParameterValues(key); + if (parameters == null) { + return null; + } else if (parameters.length == 1) { + return parameters[0]; + } else { + return parameters; + } + } + + protected void setAttribute(String key, Object value) { + throw new UnsupportedOperationException("PortletRequest parameter maps are immutable"); + } + + protected void removeAttribute(String key) { + throw new UnsupportedOperationException("PortletRequest parameter maps are immutable"); + } + + protected Iterator getAttributeNames() { + if (request instanceof MultipartActionRequest) { + MultipartActionRequest multipartRequest = (MultipartActionRequest)request; + CompositeIterator iterator = new CompositeIterator(); + iterator.add(multipartRequest.getFileMap().keySet().iterator()); + iterator.add(CollectionUtils.toIterator(request.getParameterNames())); + return iterator; + } + else { + return CollectionUtils.toIterator(request.getParameterNames()); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletSessionMap.java b/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletSessionMap.java new file mode 100644 index 00000000..44e55cec --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/PortletSessionMap.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.springframework.webflow.context.portlet; + +import java.util.Iterator; + +import javax.portlet.PortletRequest; +import javax.portlet.PortletSession; + +import org.springframework.binding.collection.SharedMap; +import org.springframework.binding.collection.StringKeyedMapAdapter; +import org.springframework.web.util.WebUtils; +import org.springframework.webflow.context.servlet.HttpSessionMapBindingListener; +import org.springframework.webflow.core.collection.AttributeMapBindingListener; +import org.springframework.webflow.core.collection.CollectionUtils; + +/** + * Shared map backed by the Portlet session for accessing session scoped + * attributes in a Portlet environment. + * + * @author Keith Donald + */ +public class PortletSessionMap extends StringKeyedMapAdapter implements SharedMap { + + /** + * The wrapped portlet request, providing access to the session. + */ + private PortletRequest request; + + /** + * The scope to access in the session, either APPLICATION (global) or + * PORTLET. + */ + private int scope; + + /** + * Create a new map wrapping the session associated with given request. + * @param request the current portlet request + * @param scope the scope to access in the session, either + * {@link PortletSession#APPLICATION_SCOPE} (global) or + * {@link PortletSession#PORTLET_SCOPE} + */ + public PortletSessionMap(PortletRequest request, int scope) { + this.request = request; + this.scope = scope; + } + + /** + * Return the portlet session associated with the wrapped request, or null + * if no such session exits. + */ + private PortletSession getSession() { + return request.getPortletSession(false); + } + + protected Object getAttribute(String key) { + PortletSession session = getSession(); + if (session == null) { + return null; + } + Object value = session.getAttribute(key, scope); + if (value instanceof HttpSessionMapBindingListener) { + // unwrap + return ((HttpSessionMapBindingListener)value).getListener(); + } else { + return value; + } + } + + protected void setAttribute(String key, Object value) { + PortletSession session = request.getPortletSession(true); + if (value instanceof AttributeMapBindingListener) { + // wrap + session.setAttribute(key, new HttpSessionMapBindingListener((AttributeMapBindingListener)value, this), scope); + } + else { + session.setAttribute(key, value, scope); + } + } + + protected void removeAttribute(String key) { + PortletSession session = getSession(); + if (session != null) { + session.removeAttribute(key, scope); + } + } + + protected Iterator getAttributeNames() { + PortletSession session = getSession(); + return session == null ? CollectionUtils.EMPTY_ITERATOR : CollectionUtils.toIterator(session + .getAttributeNames(scope)); + } + + public Object getMutex() { + PortletSession session = request.getPortletSession(true); + Object mutex = session.getAttribute(WebUtils.SESSION_MUTEX_ATTRIBUTE, scope); + return mutex != null ? mutex : session; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/package.html b/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/package.html new file mode 100644 index 00000000..be157edb --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/portlet/package.html @@ -0,0 +1,7 @@ + + +The representation of a client request into Spring Web Flow from a JSR-168 Portlet environment. +

+Portlets are specified in the Java Portlet Standard. + + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpServletContextMap.java b/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpServletContextMap.java new file mode 100644 index 00000000..e5a08280 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpServletContextMap.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.springframework.webflow.context.servlet; + +import java.util.Iterator; + +import javax.servlet.ServletContext; + +import org.springframework.binding.collection.SharedMap; +import org.springframework.binding.collection.StringKeyedMapAdapter; +import org.springframework.webflow.core.collection.CollectionUtils; + +/** + * Map backed by the Servlet context for accessing application scoped + * attributes. + * + * @author Keith Donald + */ +public class HttpServletContextMap extends StringKeyedMapAdapter implements SharedMap { + + /** + * The wrapped servlet context. + */ + private ServletContext context; + + /** + * Create a map wrapping given servlet context. + */ + public HttpServletContextMap(ServletContext context) { + this.context = context; + } + + protected Object getAttribute(String key) { + return context.getAttribute(key); + } + + protected void setAttribute(String key, Object value) { + context.setAttribute(key, value); + } + + protected void removeAttribute(String key) { + context.removeAttribute(key); + } + + protected Iterator getAttributeNames() { + return CollectionUtils.toIterator(context.getAttributeNames()); + } + + public Object getMutex() { + return context; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpServletRequestMap.java b/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpServletRequestMap.java new file mode 100644 index 00000000..e7d5b281 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpServletRequestMap.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.springframework.webflow.context.servlet; + +import java.util.Iterator; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.binding.collection.StringKeyedMapAdapter; +import org.springframework.webflow.core.collection.CollectionUtils; + +/** + * Map backed by the Servlet HTTP request attribute map for accessing request + * local attributes. + * + * @author Keith Donald + */ +public class HttpServletRequestMap extends StringKeyedMapAdapter { + + /** + * The wrapped HTTP request. + */ + private HttpServletRequest request; + + /** + * Create a new map wrapping the attributes of given request. + */ + public HttpServletRequestMap(HttpServletRequest request) { + this.request = request; + } + + protected Object getAttribute(String key) { + return request.getAttribute(key); + } + + protected void setAttribute(String key, Object value) { + request.setAttribute(key, value); + } + + protected void removeAttribute(String key) { + request.removeAttribute(key); + } + + protected Iterator getAttributeNames() { + return CollectionUtils.toIterator(request.getAttributeNames()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpServletRequestParameterMap.java b/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpServletRequestParameterMap.java new file mode 100644 index 00000000..23b5c278 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpServletRequestParameterMap.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.springframework.webflow.context.servlet; + +import java.util.Iterator; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.binding.collection.CompositeIterator; +import org.springframework.binding.collection.StringKeyedMapAdapter; +import org.springframework.web.multipart.MultipartHttpServletRequest; +import org.springframework.webflow.core.collection.CollectionUtils; + +/** + * Map backed by the Servlet HTTP request parameter map for accessing request + * parameters. Also provides support for multi-part requests, providing + * transparent access to the request "fileMap" as a request parameter entry. + * + * @author Keith Donald + */ +public class HttpServletRequestParameterMap extends StringKeyedMapAdapter { + + /** + * The wrapped HTTP request. + */ + private HttpServletRequest request; + + /** + * Create a new map wrapping the parameters of given request. + */ + public HttpServletRequestParameterMap(HttpServletRequest request) { + this.request = request; + } + + protected Object getAttribute(String key) { + if (request instanceof MultipartHttpServletRequest) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest)request; + Object data = multipartRequest.getFileMap().get(key); + if (data != null) { + return data; + } + } + String[] parameters = request.getParameterValues(key); + if (parameters == null) { + return null; + } else if (parameters.length == 1) { + return parameters[0]; + } else { + return parameters; + } + } + + protected void setAttribute(String key, Object value) { + throw new UnsupportedOperationException("HttpServletRequest parameter maps are immutable"); + } + + protected void removeAttribute(String key) { + throw new UnsupportedOperationException("HttpServletRequest parameter maps are immutable"); + } + + protected Iterator getAttributeNames() { + if (request instanceof MultipartHttpServletRequest) { + MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest)request; + CompositeIterator iterator = new CompositeIterator(); + iterator.add(multipartRequest.getFileMap().keySet().iterator()); + iterator.add(CollectionUtils.toIterator(request.getParameterNames())); + return iterator; + } + else { + return CollectionUtils.toIterator(request.getParameterNames()); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpSessionMap.java b/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpSessionMap.java new file mode 100644 index 00000000..3659b3bb --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpSessionMap.java @@ -0,0 +1,105 @@ +/* + * Copyright 2004-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.springframework.webflow.context.servlet; + +import java.util.Iterator; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import org.springframework.binding.collection.SharedMap; +import org.springframework.binding.collection.StringKeyedMapAdapter; +import org.springframework.web.util.WebUtils; +import org.springframework.webflow.core.collection.AttributeMapBindingListener; +import org.springframework.webflow.core.collection.CollectionUtils; + +/** + * A Shared Map backed by the Servlet HTTP session, for accessing session scoped + * attributes. + * + * @author Keith Donald + */ +public class HttpSessionMap extends StringKeyedMapAdapter implements SharedMap { + + /** + * The wrapped HTTP request, providing access to the session. + */ + private HttpServletRequest request; + + /** + * Create a map wrapping the session of given request. + */ + public HttpSessionMap(HttpServletRequest request) { + this.request = request; + } + + /** + * Internal helper to get the HTTP session associated with the wrapped + * request, or null if there is no such session. + *

+ * Note that this method will not force session creation. + */ + private HttpSession getSession() { + return request.getSession(false); + } + + protected Object getAttribute(String key) { + HttpSession session = getSession(); + if (session == null) { + return null; + } + Object value = session.getAttribute(key); + if (value instanceof HttpSessionMapBindingListener) { + // unwrap + return ((HttpSessionMapBindingListener)value).getListener(); + } else { + return value; + } + } + + protected void setAttribute(String key, Object value) { + // force session creation + HttpSession session = request.getSession(true); + if (value instanceof AttributeMapBindingListener) { + // wrap + session.setAttribute(key, + new HttpSessionMapBindingListener((AttributeMapBindingListener)value, this)); + } + else { + session.setAttribute(key, value); + } + } + + protected void removeAttribute(String key) { + HttpSession session = getSession(); + if (session != null) { + session.removeAttribute(key); + } + } + + protected Iterator getAttributeNames() { + HttpSession session = getSession(); + return session == null ? CollectionUtils.EMPTY_ITERATOR : CollectionUtils.toIterator(session + .getAttributeNames()); + } + + public Object getMutex() { + // force session creation + HttpSession session = request.getSession(true); + Object mutex = session.getAttribute(WebUtils.SESSION_MUTEX_ATTRIBUTE); + return mutex != null ? mutex : session; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpSessionMapBindingListener.java b/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpSessionMapBindingListener.java new file mode 100644 index 00000000..adb5c2f3 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/HttpSessionMapBindingListener.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.springframework.webflow.context.servlet; + +import java.util.Map; + +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionBindingListener; + +import org.springframework.webflow.core.collection.AttributeMapBindingEvent; +import org.springframework.webflow.core.collection.AttributeMapBindingListener; +import org.springframework.webflow.core.collection.LocalAttributeMap; + +/** + * Helper class that adapts a generic {@link AttributeMapBindingListener} to a + * HTTP specific {@link HttpSessionBindingListener}. Calls will be forwarded to + * the wrapped listener. + * + * @author Keith Donald + */ +public class HttpSessionMapBindingListener implements HttpSessionBindingListener { + + private AttributeMapBindingListener listener; + + private Map sessionMap; + + /** + * Create a new wrapper for given listener. + * @param listener the listener to wrap + * @param sessionMap the session map containing the listener + */ + public HttpSessionMapBindingListener(AttributeMapBindingListener listener, Map sessionMap) { + this.listener = listener; + this.sessionMap = sessionMap; + } + + /** + * Returns the wrapped listener. + */ + public AttributeMapBindingListener getListener() { + return listener; + } + + /** + * Returns the session map containing the listener. + */ + public Map getSessionMap() { + return sessionMap; + } + + public void valueBound(HttpSessionBindingEvent event) { + listener.valueBound(getContextBindingEvent(event)); + } + + public void valueUnbound(HttpSessionBindingEvent event) { + listener.valueUnbound(getContextBindingEvent(event)); + } + + /** + * Create a attribute map binding event for given HTTP session binding + * event. + */ + private AttributeMapBindingEvent getContextBindingEvent(HttpSessionBindingEvent event) { + return new AttributeMapBindingEvent(new LocalAttributeMap(sessionMap), event.getName(), listener); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/ServletExternalContext.java b/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/ServletExternalContext.java new file mode 100644 index 00000000..2996de71 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/ServletExternalContext.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.springframework.webflow.context.servlet; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.LocalParameterMap; +import org.springframework.webflow.core.collection.LocalSharedAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.core.collection.ParameterMap; +import org.springframework.webflow.core.collection.SharedAttributeMap; + +/** + * Provides contextual information about an HTTP Servlet environment that has + * interacted with Spring Web Flow. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class ServletExternalContext implements ExternalContext { + + /** + * The context. + */ + private ServletContext context; + + /** + * The request. + */ + private HttpServletRequest request; + + /** + * The response. + */ + private HttpServletResponse response; + + /** + * Create a new external context wrapping given servlet HTTP request and + * response and given servlet context. + * @param context the servlet context + * @param request the HTTP request + * @param response the HTTP response + */ + public ServletExternalContext(ServletContext context, HttpServletRequest request, HttpServletResponse response) { + this.context = context; + this.request = request; + this.response = response; + } + + public String getContextPath() { + return request.getContextPath(); + } + + public String getDispatcherPath() { + return request.getServletPath(); + } + + public String getRequestPathInfo() { + return request.getPathInfo(); + } + + public ParameterMap getRequestParameterMap() { + return new LocalParameterMap(new HttpServletRequestParameterMap(request)); + } + + public MutableAttributeMap getRequestMap() { + return new LocalAttributeMap(new HttpServletRequestMap(request)); + } + + public SharedAttributeMap getSessionMap() { + return new LocalSharedAttributeMap(new HttpSessionMap(request)); + } + + public SharedAttributeMap getGlobalSessionMap() { + return getSessionMap(); + } + + public SharedAttributeMap getApplicationMap() { + return new LocalSharedAttributeMap(new HttpServletContextMap(context)); + } + + /** + * Return the wrapped HTTP servlet context. + */ + public ServletContext getContext() { + return context; + } + + /** + * Return the wrapped HTTP servlet request. + */ + public HttpServletRequest getRequest() { + return request; + } + + /** + * Return the wrapped HTTP servlet response. + */ + public HttpServletResponse getResponse() { + return response; + } + + public String toString() { + return new ToStringCreator(this).append("requestParameterMap", getRequestParameterMap()).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/package.html b/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/package.html new file mode 100644 index 00000000..b364cd1a --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/context/servlet/package.html @@ -0,0 +1,5 @@ + + +The representation of a client request into Spring Web Flow from an HTTP Servlet environment. + + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/Conversation.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/Conversation.java new file mode 100644 index 00000000..137b46fc --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/Conversation.java @@ -0,0 +1,82 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation; + +/** + * A service interface for working with state associated with a single logical + * user interaction called a "conversation". + *

+ * A conversation provides a "task" context that is begun and eventually ends. + * Between the beginning and the end attributes can be placed in and read from a + * conversation's context. + *

+ * Once begun, the conversation can be locked to obtain exclusive access to + * manipulating it. Once the conversation is "done", it can be ended. + *

+ * Note that the attributes associated with a conversation are not + * "conversation scope" as defined for a flow execution. They can be + * any attributes, possibly technical in nature, associated with the + * conversation. + * + * @author Keith Donald + */ +public interface Conversation { + + /** + * Returns the unique id assigned to this conversation. This id remains the + * same throughout the life of the conversation. + * @return the conversation id + */ + public ConversationId getId(); + + /** + * Lock this conversation. May block until the lock is available, if someone + * else has acquired the lock. + */ + public void lock(); + + /** + * Returns the conversation attribute with the specified name. + * @param name the attribute name + * @return the attribute value + */ + public Object getAttribute(Object name); + + /** + * Puts a conversation attribute into this context. + * @param name the attribute name + * @param value the attribute value + */ + public void putAttribute(Object name, Object value); + + /** + * Removes a conversation attribute. + * @param name the attribute name + */ + public void removeAttribute(Object name); + + /** + * Ends this conversation. This method should only be called once to + * terminate the conversation and cleanup any allocated resources. + */ + public void end(); + + /** + * Unlock this conversation, making it available for others for + * manipulation. + */ + public void unlock(); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationException.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationException.java new file mode 100644 index 00000000..9116a213 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationException.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation; + +import org.springframework.webflow.core.FlowException; + +/** + * The root of the conversation service exception hierarchy. + * + * @author Keith Donald + */ +public abstract class ConversationException extends FlowException { + + /** + * Creates a conversation service exception. + * @param message a descriptive message + */ + public ConversationException(String message) { + super(message); + } + + /** + * Creates a conversation service exception. + * @param message a descriptive message + * @param cause the root cause of the problem + */ + public ConversationException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationId.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationId.java new file mode 100644 index 00000000..beb3984e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationId.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation; + +import java.io.Serializable; + +/** + * An id that uniquely identifies a conversation managed by + * {@link ConversationManager}. + * + * @author Ben Hale + * @author Keith Donald + */ +public abstract class ConversationId implements Serializable { + + /** + * Subclasses should override toString to return a parseable string form of + * the key. + * @see java.lang.Object#toString() + * @see ConversationManager#parseConversationId(String) + */ + public abstract String toString(); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationManager.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationManager.java new file mode 100644 index 00000000..7cec5538 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationManager.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation; + +/** + * A service for managing conversations. This interface is the entry point into + * the conversation subsystem. + * + * @author Keith Donald + */ +public interface ConversationManager { + + /** + * Begin a new conversation. + * @param conversationParameters descriptive conversation parameters + * @return a service interface allowing access to the conversation context + * @throws ConversationException an exception occured + */ + public Conversation beginConversation(ConversationParameters conversationParameters) throws ConversationException; + + /** + * Get the conversation with the provided id. + * @param id the conversation id + * @return the conversation + * @throws NoSuchConversationException the id provided was invalid + */ + public Conversation getConversation(ConversationId id) throws ConversationException; + + /** + * Parse the string-encoded conversationId into its object form. + * Essentially, the reverse of {@link ConversationId#toString()}. + * @param encodedId the encoded id + * @return the parsed conversation id + * @throws ConversationException an exception occured parsing the id + */ + public ConversationId parseConversationId(String encodedId) throws ConversationException; +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationParameters.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationParameters.java new file mode 100644 index 00000000..6b6f6c95 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationParameters.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation; + +import java.io.Serializable; + +import org.springframework.core.style.ToStringCreator; + +/** + * Simple parameter object for clumping together input needed to begin a new + * conversation. + * + * @author Keith Donald + */ +public class ConversationParameters implements Serializable { + + /** + * The conversation name. + */ + private String name; + + /** + * The conversation caption. + */ + private String caption; + + /** + * The conversation description. + */ + private String description; + + /** + * Creates new conversation input parameters. + * @param name the name of the conversation + * @param caption a short description + * @param description a long description + */ + public ConversationParameters(String name, String caption, String description) { + this.name = name; + this.caption = caption; + this.description = description; + } + + /** + * Returns the name of the conversation. + * @return the conversation name + */ + public String getName() { + return name; + } + + /** + * Returns the short description. + * @return the conversation caption + */ + public String getCaption() { + return caption; + } + + /** + * Returns the long description. + * @return the description. + */ + public String getDescription() { + return description; + } + + public String toString() { + return new ToStringCreator(this).append("name", name).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/NoSuchConversationException.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/NoSuchConversationException.java new file mode 100644 index 00000000..d27b8124 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/NoSuchConversationException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation; + +/** + * Thrown when no logical conversation exists with the specified + * conversationId. This might occur if the conversation ended, + * expired, or was otherwise invalidated, but a client view still references it. + * + * @author Keith Donald + */ +public class NoSuchConversationException extends ConversationException { + + /** + * The unique conversation identifier that was invalid. + */ + private ConversationId conversationId; + + /** + * Create a new conversation lookup exception. + * @param conversationId the conversation id + */ + public NoSuchConversationException(ConversationId conversationId) { + super("No conversation could be found with id '" + conversationId + + "' -- perhaps this conversation has ended? "); + this.conversationId = conversationId; + } + + /** + * Returns the conversation id that was not found. + */ + public ConversationId getConversationId() { + return conversationId; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ContainedConversation.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ContainedConversation.java new file mode 100644 index 00000000..d2988b72 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ContainedConversation.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation.impl; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.webflow.conversation.Conversation; +import org.springframework.webflow.conversation.ConversationId; + +/** + * Internal {@link Conversation} implementation used by the conversation + * container. + *

+ * This is an internal helper class of the {@link SessionBindingConversationManager}. + * + * @author Erwin Vervaet + */ +class ContainedConversation implements Conversation, Serializable { + + private static final Log logger = LogFactory.getLog(SessionBindingConversationManager.class); + + private ConversationContainer container; + + private ConversationId id; + + private transient ConversationLock lock; + + private Map attributes; + + /** + * Create a new contained conversation. + * @param container the container containing the conversation + * @param id the unique id assigned to the conversation + */ + public ContainedConversation(ConversationContainer container, ConversationId id) { + this.container = container; + this.id = id; + this.lock = ConversationLockFactory.createLock(); + this.attributes = new HashMap(); + } + + public ConversationId getId() { + return id; + } + + public void lock() { + if (logger.isDebugEnabled()) { + logger.debug("Locking conversation " + id); + } + lock.lock(); + } + + public Object getAttribute(Object name) { + return attributes.get(name); + } + + public void putAttribute(Object name, Object value) { + if (logger.isDebugEnabled()) { + logger.debug("Putting conversation attribute '" + name + "' with value " + value); + } + attributes.put(name, value); + } + + public void removeAttribute(Object name) { + if (logger.isDebugEnabled()) { + logger.debug("Removing conversation attribute '" + name + "'"); + } + attributes.remove(name); + } + + public void end() { + if (logger.isDebugEnabled()) { + logger.debug("Ending conversation " + id); + } + container.removeConversation(getId()); + } + + public void unlock() { + if (logger.isDebugEnabled()) { + logger.debug("Unlocking conversation " + id); + } + lock.unlock(); + } + + public String toString() { + return getId().toString(); + } + + // id based equality + + public boolean equals(Object obj) { + if (!(obj instanceof ContainedConversation)) { + return false; + } + return id.equals(((ContainedConversation)obj).id); + } + + public int hashCode() { + return id.hashCode(); + } + + // custom serialisation + + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + lock = ConversationLockFactory.createLock(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ConversationContainer.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ConversationContainer.java new file mode 100644 index 00000000..4888d0fa --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ConversationContainer.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation.impl; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.springframework.webflow.conversation.Conversation; +import org.springframework.webflow.conversation.ConversationId; +import org.springframework.webflow.conversation.ConversationParameters; +import org.springframework.webflow.conversation.NoSuchConversationException; + +/** + * Container for conversations that is stored in the session. When the + * session expires this container will go with it, implicitly expiring all + * contained conversations. + *

+ * This is an internal helper class of the {@link SessionBindingConversationManager}. + * + * @author Erwin Vervaet + */ +class ConversationContainer implements Serializable { + + /** + * Maximum number of conversations in this container. -1 for + * unlimited. + */ + private int maxConversations; + + /** + * The contained conversations. A list of {@link ContainedConversation} objects. + */ + private List conversations; + + /** + * Create a new conversation container. + * @param maxConversations the maximum number of allowed concurrent + * conversations, -1 for unlimited + */ + public ConversationContainer(int maxConversations) { + this.maxConversations = maxConversations; + this.conversations = new ArrayList(); + } + + /** + * Create a new conversation based on given parameters and add it to the + * container. + * @param id the unique id of the conversation + * @param parameters descriptive parameters + * @return the created conversation + */ + public synchronized Conversation createAndAddConversation(ConversationId id, ConversationParameters parameters) { + ContainedConversation conversation = new ContainedConversation(this, id); + conversations.add(conversation); + if (maxExceeded()) { + // end oldest conversation + ((Conversation)conversations.get(0)).end(); + } + return conversation; + } + + /** + * Return the identified conversation. + * @param id the id to lookup + * @return the conversation + * @throws NoSuchConversationException if the conversation cannot be + * found + */ + public synchronized Conversation getConversation(ConversationId id) throws NoSuchConversationException { + for (Iterator it = conversations.iterator(); it.hasNext();) { + ContainedConversation conversation = (ContainedConversation)it.next(); + if (conversation.getId().equals(id)) { + return conversation; + } + } + throw new NoSuchConversationException(id); + } + + /** + * Remove identified conversation from this container. + */ + public synchronized void removeConversation(ConversationId id) { + for (Iterator it = conversations.iterator(); it.hasNext();) { + ContainedConversation conversation = (ContainedConversation)it.next(); + if (conversation.getId().equals(id)) { + it.remove(); + break; + } + } + } + + /** + * Has the maximum number of allowed concurrent conversations in the + * session been exceeded? + */ + private boolean maxExceeded() { + return maxConversations > 0 && conversations.size() > maxConversations; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ConversationLock.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ConversationLock.java new file mode 100644 index 00000000..7524eb3e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ConversationLock.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation.impl; + +/** + * A normalized interface for conversation locks, used to obtain exclusive + * access to a conversation. + * + * @author Keith Donald + */ +public interface ConversationLock { + + /** + * Acquire the conversation lock. + */ + public void lock(); + + /** + * Release the conversation lock. + */ + public void unlock(); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ConversationLockFactory.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ConversationLockFactory.java new file mode 100644 index 00000000..0d6aa3a5 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ConversationLockFactory.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.JdkVersion; + +/** + * Simple utility class for creating conversation lock instances based on the + * current execution environment. + * + * @author Keith Donald + * @author Rob Harrop + */ +public class ConversationLockFactory { + + private static final Log logger = LogFactory.getLog(ConversationLockFactory.class); + + private static boolean utilConcurrentPresent; + + static { + try { + Class.forName("EDU.oswego.cs.dl.util.concurrent.ReentrantLock"); + utilConcurrentPresent = true; + } + catch (ClassNotFoundException ex) { + utilConcurrentPresent = false; + } + } + + /** + * When running on Java 1.5+, returns a jdk5 concurrent lock. When running on older JDKs with + * the 'util.concurrent' package available, returns a util concurrent lock. + * In all other cases a "no-op" lock is returned. + */ + public static ConversationLock createLock() { + if (JdkVersion.getMajorJavaVersion() >= JdkVersion.JAVA_15) { + return new JdkConcurrentConversationLock(); + } + else if (utilConcurrentPresent) { + return new UtilConcurrentConversationLock(); + } + else { + logger.warn("Unable to enable conversation locking. Switch to Java 5 or above, " + + "or put the 'util.concurrent' package on the classpath " + + "to enable locking in your environment."); + return NoOpConversationLock.INSTANCE; + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/JdkConcurrentConversationLock.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/JdkConcurrentConversationLock.java new file mode 100644 index 00000000..8078077e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/JdkConcurrentConversationLock.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation.impl; + +import java.io.Serializable; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +/** + * A conversation lock that relies on a {@link ReentrantLock} within Java 5's + * util.concurrent.locks package. + * + * @author Keith Donald + */ +class JdkConcurrentConversationLock implements ConversationLock, Serializable { + + /** + * The lock. + */ + private Lock lock = new ReentrantLock(); + + public void lock() { + lock.lock(); + } + + public void unlock() { + lock.unlock(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/NoOpConversationLock.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/NoOpConversationLock.java new file mode 100644 index 00000000..c237c2f8 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/NoOpConversationLock.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation.impl; + +import java.io.ObjectStreamException; +import java.io.Serializable; + +/** + * A singleton lock that doesn't do anything. For use when conversations don't + * require or choose not to implement locking. + * + * @author Keith Donald + */ +class NoOpConversationLock implements ConversationLock, Serializable { + + /** + * The singleton instance. + */ + public static final NoOpConversationLock INSTANCE = new NoOpConversationLock(); + + /** + * Private constructor to avoid instantiation. + */ + private NoOpConversationLock() { + } + + public void lock() { + // no-op + } + + public void unlock() { + // no-op + } + + // resolve the singleton instance + private Object readResolve() throws ObjectStreamException { + return INSTANCE; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/SessionBindingConversationManager.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/SessionBindingConversationManager.java new file mode 100644 index 00000000..cab5f031 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/SessionBindingConversationManager.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.webflow.context.ExternalContextHolder; +import org.springframework.webflow.conversation.Conversation; +import org.springframework.webflow.conversation.ConversationException; +import org.springframework.webflow.conversation.ConversationId; +import org.springframework.webflow.conversation.ConversationManager; +import org.springframework.webflow.conversation.ConversationParameters; +import org.springframework.webflow.core.collection.SharedAttributeMap; +import org.springframework.webflow.util.RandomGuidUidGenerator; +import org.springframework.webflow.util.UidGenerator; + +/** + * Simple implementation of a conversation manager that stores conversations in + * the session attribute map. + *

+ * Using the {@link #setMaxConversations(int) maxConversations} property, you can + * limit the number of concurrently active conversations allowed in a single + * session. If the default is exceeded, the conversation manager will automatically + * end the oldest conversation. The default is 5, which should be fine for most + * situations. Set it to -1 for no limit. Setting maxConversations to 1 allows + * easy resource cleanup in situations where there should only be one active + * conversation per session. + * + * @author Erwin Vervaet + */ +public class SessionBindingConversationManager implements ConversationManager { + + private static final Log logger = LogFactory.getLog(SessionBindingConversationManager.class); + + /** + * Key of the session attribute holding the conversation container. + */ + private static final String CONVERSATION_CONTAINER_KEY = "webflow.conversation.container"; + + /** + * The conversation uid generation strategy to use. + */ + private UidGenerator conversationIdGenerator = new RandomGuidUidGenerator(); + + /** + * The maximum number of active conversations allowed in a session. + * The default is 5. This is high enough for most practical situations and low enough + * to avoid excessive resource usage or easy denial of service attacks. + */ + private int maxConversations = 5; + + /** + * Returns the used generator for conversation ids. Defaults to + * {@link RandomGuidUidGenerator}. + */ + public UidGenerator getConversationIdGenerator() { + return conversationIdGenerator; + } + + /** + * Sets the configured generator for conversation ids. + */ + public void setConversationIdGenerator(UidGenerator uidGenerator) { + this.conversationIdGenerator = uidGenerator; + } + + /** + * Returns the maximum number of allowed concurrent conversations. The + * default is 5. + */ + public int getMaxConversations() { + return maxConversations; + } + + /** + * Set the maximum number of allowed concurrent conversations. Set to -1 for + * no limit. The default is 5. + */ + public void setMaxConversations(int maxConversations) { + this.maxConversations = maxConversations; + } + + public Conversation beginConversation(ConversationParameters conversationParameters) throws ConversationException { + ConversationId conversationId = new SimpleConversationId(conversationIdGenerator.generateUid()); + if (logger.isDebugEnabled()) { + logger.debug("Beginning conversation " + conversationParameters + + "; unique conversation id = " + conversationId); + } + return getConversationContainer().createAndAddConversation(conversationId, conversationParameters); + } + + public Conversation getConversation(ConversationId id) throws ConversationException { + if (logger.isDebugEnabled()) { + logger.debug("Getting conversation " + id); + } + return getConversationContainer().getConversation(id); + } + + public ConversationId parseConversationId(String encodedId) throws ConversationException { + return new SimpleConversationId(conversationIdGenerator.parseUid(encodedId)); + } + + // internal helpers + + /** + * Obtain the conversation container from the session. Create a new empty + * container and add it to the session if no existing container can be + * found. + */ + private ConversationContainer getConversationContainer() { + SharedAttributeMap sessionMap = ExternalContextHolder.getExternalContext().getSessionMap(); + synchronized (sessionMap.getMutex()) { + ConversationContainer container = (ConversationContainer)sessionMap.get(CONVERSATION_CONTAINER_KEY); + if (container == null) { + container = new ConversationContainer(maxConversations); + sessionMap.put(CONVERSATION_CONTAINER_KEY, container); + } + return container; + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/SimpleConversationId.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/SimpleConversationId.java new file mode 100644 index 00000000..44c6386b --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/SimpleConversationId.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation.impl; + +import java.io.Serializable; + +import org.springframework.webflow.conversation.ConversationId; +import org.springframework.webflow.conversation.ConversationManager; + +/** + * An id that uniquely identifies a conversation managed by a + * {@link ConversationManager}. + *

+ * This key consists of a unique string that is typically a GUID. + * + * @author Ben Hale + */ +public class SimpleConversationId extends ConversationId { + + /** + * The id value. + */ + private Serializable id; + + /** + * Creates a new simple conversation id. + * @param id the id value + */ + public SimpleConversationId(Serializable id) { + this.id = id; + } + + public boolean equals(Object o) { + if (!(o instanceof SimpleConversationId)) { + return false; + } + return id.equals(((SimpleConversationId)o).id); + } + + public int hashCode() { + return id.hashCode(); + } + + public String toString() { + return id.toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/UtilConcurrentConversationLock.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/UtilConcurrentConversationLock.java new file mode 100644 index 00000000..37aea3fd --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/UtilConcurrentConversationLock.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation.impl; + + +import org.springframework.core.NestedRuntimeException; + +import EDU.oswego.cs.dl.util.concurrent.ReentrantLock; + +/** + * A conversation lock that relies on a {@link ReentrantLock} within Doug Lea's + * util.concurrent + * package. For use on JDK 1.3 and 1.4. + * + * @author Keith Donald + * @author Rob Harrop + */ +class UtilConcurrentConversationLock implements ConversationLock { + + /** + * The {@link ReentrantLock} instance. + */ + private final ReentrantLock lock = new ReentrantLock(); + + /** + * Acquires the lock. + * @throws SystemInterruptedException if the lock cannot be acquired due to interruption + */ + public void lock() { + try { + lock.acquire(); + } + catch (InterruptedException e) { + throw new SystemInterruptedException("Unable to acquire lock.", e); + } + } + + /** + * Releases the lock. + */ + public void unlock() { + lock.release(); + } + + /** + * Exception indicating that some {@link Thread} was + * {@link Thread#interrupt() interrupted} during processing and as + * such processing was halted. + *

+ * Only used to wrap the checked {@link InterruptedException java.lang.InterruptedException}. + */ + public static class SystemInterruptedException extends NestedRuntimeException { + + /** + * Creates a new SystemInterruptedException. + * @param msg the Exception message + */ + public SystemInterruptedException(String msg) { + super(msg); + } + + /** + * Creates a new SystemInterruptedException. + * @param msg the Exception message + * @param cause the root cause of this Exception + */ + public SystemInterruptedException(String msg, Throwable cause) { + super(msg, cause); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/package.html b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/package.html new file mode 100644 index 00000000..2a66e7d5 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/package.html @@ -0,0 +1,10 @@ + + +

+Conversation manager implementations. +

+

+This package depends on the root conversation package. +

+ + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/package.html b/spring-webflow/src/main/java/org/springframework/webflow/conversation/package.html new file mode 100644 index 00000000..e07aa8d2 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/package.html @@ -0,0 +1,19 @@ + + +

+The conversation subsystem for beginning and ending conversations that manage the state of user interactions. +

+

+The central concept defined by this package is the +{@link org.springframework.webflow.conversation.ConversationManager}, representing +a service interface for managing conversations. +

+

+This package serves as a portable conversation management abstraction and does not depend on +the Spring Web Flow engine. It is used by the flow execution repository subsystem to store conversation related +state. It is important to realize that this subsystem does not define a conceptual conversation, +it just servers as a technical layer to manage state associated with conversations, as implemented by the +SWF engine. +

+ + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/DefaultExpressionParserFactory.java b/spring-webflow/src/main/java/org/springframework/webflow/core/DefaultExpressionParserFactory.java new file mode 100644 index 00000000..b0d3cc31 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/DefaultExpressionParserFactory.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core; + +import org.springframework.binding.expression.ExpressionParser; + +/** + * Static helper factory that creates instances of the default expression parser + * used by Spring Web Flow when requested. Marked final with a private + * constructor to prevent subclassing. + *

+ * The default is an OGNL based expression parser. Also asserts that OGNL is in + * the classpath when this class is loaded. + * + * @author Keith Donald + */ +public final class DefaultExpressionParserFactory { + + /** + * The singleton instance. + */ + private static ExpressionParser INSTANCE; + + // static factory - not instantiable + private DefaultExpressionParserFactory() { + } + + /** + * Returns the default expression parser. The returned expression parser is + * a thread-safe object. + * @return the expression parser + */ + public static synchronized ExpressionParser getExpressionParser() { + if (INSTANCE == null) { + INSTANCE = createDefaultExpressionParser(); + } + return INSTANCE; + } + + /** + * Create the default expression parser. + * @return the default expression parser + */ + private static ExpressionParser createDefaultExpressionParser() { + try { + Class.forName("ognl.Ognl"); + return new WebFlowOgnlExpressionParser(); + } catch (ClassNotFoundException e) { + throw new IllegalStateException( + "Unable to load the default expression parser: OGNL could not be found in the classpath. " + + "Please add OGNL 2.x to your classpath or set the default ExpressionParser instance to something that is in the classpath. " + + "Details: " + e.getMessage()); + } catch (NoClassDefFoundError e) { + throw new IllegalStateException( + "Unable to construct the default expression parser: ognl.Ognl could not be instantiated. " + + "Please add OGNL 2.x to your classpath or set the default ExpressionParser instance to something that is in the classpath. " + + "Details: " + e); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/FlowException.java b/spring-webflow/src/main/java/org/springframework/webflow/core/FlowException.java new file mode 100644 index 00000000..b9abb194 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/FlowException.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core; + +import org.springframework.core.NestedRuntimeException; + +/** + * Root class for exceptions thrown by the Spring Web Flow system. All other + * exceptions within the system should be assignable to this class. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public abstract class FlowException extends NestedRuntimeException { + + /** + * Creates a new flow exception. + * @param msg the message + * @param cause the cause + */ + public FlowException(String msg, Throwable cause) { + super(msg, cause); + } + + /** + * Creates a new flow exception. + * @param msg the message + */ + public FlowException(String msg) { + super(msg); + } + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/WebFlowOgnlExpressionParser.java b/spring-webflow/src/main/java/org/springframework/webflow/core/WebFlowOgnlExpressionParser.java new file mode 100644 index 00000000..2132a51b --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/WebFlowOgnlExpressionParser.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core; + +import java.util.Map; + +import ognl.OgnlException; +import ognl.PropertyAccessor; + +import org.springframework.binding.collection.MapAdaptable; +import org.springframework.binding.expression.support.OgnlExpressionParser; +import org.springframework.webflow.core.collection.MutableAttributeMap; + +/** + * An extension of {@link OgnlExpressionParser} that registers web flow specific + * property accessors. + * + * @author Keith Donald + */ +class WebFlowOgnlExpressionParser extends OgnlExpressionParser { + + /** + * Creates a webflow-specific ognl expression parser. + */ + public WebFlowOgnlExpressionParser() { + addPropertyAccessor(MapAdaptable.class, new MapAdaptablePropertyAccessor()); + addPropertyAccessor(MutableAttributeMap.class, new MutableAttributeMapPropertyAccessor()); + } + + /** + * The {@link MapAdaptable} property accessor. + * + * @author Keith Donald + */ + private static class MapAdaptablePropertyAccessor implements PropertyAccessor { + public Object getProperty(Map context, Object target, Object name) throws OgnlException { + return ((MapAdaptable)target).asMap().get(name); + } + + public void setProperty(Map context, Object target, Object name, Object value) throws OgnlException { + throw new UnsupportedOperationException( + "Cannot mutate immutable attribute collections; operation disallowed"); + } + } + + /** + * The {@link MutableAttributeMap} property accessor. + * + * @author Keith Donald + */ + private static class MutableAttributeMapPropertyAccessor extends MapAdaptablePropertyAccessor { + public void setProperty(Map context, Object target, Object name, Object value) throws OgnlException { + ((MutableAttributeMap)target).put((String)name, value); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/collection/AttributeMap.java b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/AttributeMap.java new file mode 100644 index 00000000..7861cd4e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/AttributeMap.java @@ -0,0 +1,349 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core.collection; + +import java.util.Collection; + +import org.springframework.binding.collection.MapAdaptable; + +/** + * An immutable interface for accessing attributes in a backing map with string keys. + *

+ * Implementations can optionally support {@link AttributeMapBindingListener listeners} + * that will be notified when they're bound in or unbound from the map. + * + * @author Keith Donald + */ +public interface AttributeMap extends MapAdaptable { + + /** + * Get an attribute value out of this map, returning null if + * not found. + * @param attributeName the attribute name + * @return the attribute value + */ + public Object get(String attributeName); + + /** + * Returns the size of this map. + * @return the nubmer of entries in the map + */ + public int size(); + + /** + * Is this attribute map empty with a size of 0? + * @return true if empty, false if not + */ + public boolean isEmpty(); + + /** + * Does the attribute with the provided name exist in this map? + * @param attributeName the attribute name + * @return true if so, false otherwise + */ + public boolean contains(String attributeName); + + /** + * Does the attribute with the provided name exist in this map and is its + * value of the specified required type? + * @param attributeName the attribute name + * @param requiredType the required class of the attribute value + * @return true if so, false otherwise + * @throws IllegalArgumentException when the value is not of the required + * type + */ + public boolean contains(String attributeName, Class requiredType) throws IllegalArgumentException; + + /** + * Get an attribute value, returning the default value if no value is found. + * @param attributeName the name of the attribute + * @param defaultValue the default value + * @return the attribute value, falling back to the default if no such + * attribute exists + */ + public Object get(String attributeName, Object defaultValue); + + /** + * Get an attribute value, asserting the value is of the required type. + * @param attributeName the name of the attribute + * @param requiredType the required type of the attribute value + * @return the attribute value, or null if not found + * @throws IllegalArgumentException when the value is not of the required + * type + */ + public Object get(String attributeName, Class requiredType) throws IllegalArgumentException; + + /** + * Get an attribute value, asserting the value is of the required type and + * returning the default value if not found. + * @param attributeName the name of the attribute + * @param requiredType the value required type + * @param defaultValue the default value + * @return the attribute value, or the default if not found + * @throws IllegalArgumentException when the value (if found) is not of the + * required type + */ + public Object get(String attributeName, Class requiredType, Object defaultValue) throws IllegalStateException; + + /** + * Get the value of a required attribute, throwing an exception of no + * attribute is found. + * @param attributeName the name of the attribute + * @return the attribute value + * @throws IllegalArgumentException when the attribute is not found + */ + public Object getRequired(String attributeName) throws IllegalArgumentException; + + /** + * Get the value of a required attribute and make sure it is of the required + * type. + * @param attributeName name of the attribute to get + * @param requiredType the required type of the attribute value + * @return the attribute value + * @throws IllegalArgumentException when the attribute is not found or not + * of the required type + */ + public Object getRequired(String attributeName, Class requiredType) throws IllegalArgumentException; + + /** + * Returns a string attribute value in the map, returning null + * if no value was found. + * @param attributeName the attribute name + * @return the string attribute value + * @throws IllegalArgumentException if the attribute is present but not a + * string + */ + public String getString(String attributeName) throws IllegalArgumentException; + + /** + * Returns a string attribute value in the map, returning the default value + * if no value was found. + * @param attributeName the attribute name + * @param defaultValue the default + * @return the string attribute value + * @throws IllegalArgumentException if the attribute is present but not a + * string + */ + public String getString(String attributeName, String defaultValue) throws IllegalArgumentException; + + /** + * Returns a string attribute value in the map, throwing an exception if the + * attribute is not present and of the correct type. + * @param attributeName the attribute name + * @return the string attribute value + * @throws IllegalArgumentException if the attribute is not present or + * present but not a string + */ + public String getRequiredString(String attributeName) throws IllegalArgumentException; + + /** + * Returns a collection attribute value in the map. + * @param attributeName the attribute name + * @return the collection attribute value + * @throws IllegalArgumentException if the attribute is present but not a + * collection + */ + public Collection getCollection(String attributeName) throws IllegalArgumentException; + + /** + * Returns a collection attribute value in the map and make sure it is of + * the required type. + * @param attributeName the attribute name + * @param requiredType the required type of the attribute value + * @return the collection attribute value + * @throws IllegalArgumentException if the attribute is present but not a + * collection of the required type + */ + public Collection getCollection(String attributeName, Class requiredType) throws IllegalArgumentException; + + /** + * Returns a collection attribute value in the map, throwing an exception if + * the attribute is not present or not a collection. + * @param attributeName the attribute name + * @return the collection attribute value + * @throws IllegalArgumentException if the attribute is not present or is + * present but not a collection + */ + public Collection getRequiredCollection(String attributeName) throws IllegalArgumentException; + + /** + * Returns a collection attribute value in the map, throwing an exception if + * the attribute is not present or not a collection of the required type. + * @param attributeName the attribute name + * @param requiredType the required collection type + * @return the collection attribute value + * @throws IllegalArgumentException if the attribute is not present or is + * present but not a collection of the required type + */ + public Collection getRequiredCollection(String attributeName, Class requiredType) throws IllegalArgumentException; + + /** + * Returns an array attribute value in the map and makes sure it is of the + * required type. + * @param attributeName the attribute name + * @param requiredType the required type of the attribute value + * @return the array attribute value + * @throws IllegalArgumentException if the attribute is present but not an + * array of the required type + */ + public Object[] getArray(String attributeName, Class requiredType) throws IllegalArgumentException; + + /** + * Returns an array attribute value in the map, throwing an exception if the + * attribute is not present or not an array of the required type. + * @param attributeName the attribute name + * @param requiredType the required array type + * @return the collection attribute value + * @throws IllegalArgumentException if the attribute is not present or is + * present but not a array of the required type + */ + public Object[] getRequiredArray(String attributeName, Class requiredType) throws IllegalArgumentException; + + /** + * Returns a number attribute value in the map that is of the specified + * type, returning null if no value was found. + * @param attributeName the attribute name + * @param requiredType the required number type + * @return the number attribute value + * @throws IllegalArgumentException if the attribute is present but not a + * number of the required type + */ + public Number getNumber(String attributeName, Class requiredType) throws IllegalArgumentException; + + /** + * Returns a number attribute value in the map of the specified type, + * returning the default value if no value was found. + * @param attributeName the attribute name + * @param defaultValue the default + * @return the number attribute value + * @throws IllegalArgumentException if the attribute is present but not a + * number of the required type + */ + public Number getNumber(String attributeName, Class requiredType, Number defaultValue) + throws IllegalArgumentException; + + /** + * Returns a number attribute value in the map, throwing an exception if the + * attribute is not present and of the correct type. + * @param attributeName the attribute name + * @return the number attribute value + * @throws IllegalArgumentException if the attribute is not present or + * present but not a number of the required type + */ + public Number getRequiredNumber(String attributeName, Class requiredType) throws IllegalArgumentException; + + /** + * Returns an integer attribute value in the map, returning + * null if no value was found. + * @param attributeName the attribute name + * @return the integer attribute value + * @throws IllegalArgumentException if the attribute is present but not an + * integer + */ + public Integer getInteger(String attributeName) throws IllegalArgumentException; + + /** + * Returns an integer attribute value in the map, returning the default + * value if no value was found. + * @param attributeName the attribute name + * @param defaultValue the default + * @return the integer attribute value + * @throws IllegalArgumentException if the attribute is present but not an + * integer + */ + public Integer getInteger(String attributeName, Integer defaultValue) throws IllegalArgumentException; + + /** + * Returns an integer attribute value in the map, throwing an exception if + * the attribute is not present and of the correct type. + * @param attributeName the attribute name + * @return the integer attribute value + * @throws IllegalArgumentException if the attribute is not present or + * present but not an integer + */ + public Integer getRequiredInteger(String attributeName) throws IllegalArgumentException; + + /** + * Returns a long attribute value in the map, returning null + * if no value was found. + * @param attributeName the attribute name + * @return the long attribute value + * @throws IllegalArgumentException if the attribute is present but not a + * long + */ + public Long getLong(String attributeName) throws IllegalArgumentException; + + /** + * Returns a long attribute value in the map, returning the default value if + * no value was found. + * @param attributeName the attribute name + * @param defaultValue the default + * @return the long attribute value + * @throws IllegalArgumentException if the attribute is present but not a + * long + */ + public Long getLong(String attributeName, Long defaultValue) throws IllegalArgumentException; + + /** + * Returns a long attribute value in the map, throwing an exception if the + * attribute is not present and of the correct type. + * @param attributeName the attribute name + * @return the long attribute value + * @throws IllegalArgumentException if the attribute is not present or + * present but not a long + */ + public Long getRequiredLong(String attributeName) throws IllegalArgumentException; + + /** + * Returns a boolean attribute value in the map, returning null + * if no value was found. + * @param attributeName the attribute name + * @return the long attribute value + * @throws IllegalArgumentException if the attribute is present but not a + * boolean + */ + public Boolean getBoolean(String attributeName) throws IllegalArgumentException; + + /** + * Returns a boolean attribute value in the map, returning the default value + * if no value was found. + * @param attributeName the attribute name + * @param defaultValue the default + * @return the boolean attribute value + * @throws IllegalArgumentException if the attribute is present but not a + * boolean + */ + public Boolean getBoolean(String attributeName, Boolean defaultValue) throws IllegalArgumentException; + + /** + * Returns a boolean attribute value in the map, throwing an exception if + * the attribute is not present and of the correct type. + * @param attributeName the attribute name + * @return the boolean attribute value + * @throws IllegalArgumentException if the attribute is not present or + * present but is not a boolean + */ + public Boolean getRequiredBoolean(String attributeName) throws IllegalArgumentException; + + /** + * Returns a new attribute map containing the union of this map with the + * provided map. + * @param attributes the map to combine with this map + * @return a new, combined map + */ + public AttributeMap union(AttributeMap attributes); + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/collection/AttributeMapBindingEvent.java b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/AttributeMapBindingEvent.java new file mode 100644 index 00000000..9a420f0d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/AttributeMapBindingEvent.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core.collection; + +import java.util.EventObject; + +/** + * Holder for information about the binding or unbinding event in an + * {@link AttributeMap}. + * + * @see AttributeMapBindingListener + * + * @author Ben Hale + */ +public class AttributeMapBindingEvent extends EventObject { + + private String attributeName; + + private Object attributeValue; + + /** + * Creates an event for map binding that contains information about the + * event. + * @param source the source map that this attribute was bound in + * @param attributeName the name that this attribute was bound with + * @param attributeValue the attribute + */ + public AttributeMapBindingEvent(AttributeMap source, String attributeName, Object attributeValue) { + super(source); + this.source = source; + this.attributeName = attributeName; + this.attributeValue = attributeValue; + } + + /** + * Returns the name the attribute was bound with. + */ + public String getAttributeName() { + return attributeName; + } + + /** + * Returns the value of the attribute. + */ + public Object getAttributeValue() { + return attributeValue; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/collection/AttributeMapBindingListener.java b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/AttributeMapBindingListener.java new file mode 100644 index 00000000..09bfbdd1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/AttributeMapBindingListener.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core.collection; + +/** + * Causes an object to be notified when it is bound or unbound from + * an {@link AttributeMap}. + *

+ * Note that this is an optional feature and not all {@link AttributeMap} + * implementations support it. + * + * @see AttributeMap + * + * @author Ben Hale + */ +public interface AttributeMapBindingListener { + + /** + * Called when the implementing instance is bound into an + * AttributeMap. + * @param event information about the binding event + */ + void valueBound(AttributeMapBindingEvent event); + + /** + * Called when the implementing instance is unbound from an + * AttributeMap. + * @param event information about the unbinding event + */ + void valueUnbound(AttributeMapBindingEvent event); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/collection/CollectionUtils.java b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/CollectionUtils.java new file mode 100644 index 00000000..e70d8ea7 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/CollectionUtils.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core.collection; + +import java.io.Serializable; +import java.util.Collections; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; + +/** + * A utility class for working with attribute and parameter collections used by + * Spring Web FLow. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class CollectionUtils { + + /** + * The shared, singleton empty iterator instance. + */ + public static final Iterator EMPTY_ITERATOR = new EmptyIterator(); + + /** + * The shared, singleton empty attribute map instance. + */ + public static final AttributeMap EMPTY_ATTRIBUTE_MAP = new LocalAttributeMap(Collections.EMPTY_MAP); + + /** + * Private constructor to avoid instantiation. + */ + private CollectionUtils() { + } + + /** + * Factory method that adapts an enumeration to an iterator. + * @param enumeration the enumeration + * @return the iterator + */ + public static Iterator toIterator(Enumeration enumeration) { + return new EnumerationIterator(enumeration); + } + + /** + * Factory method that returns a unmodifiable attribute map with a single + * entry. + * @param attributeName the attribute name + * @param attributeValue the attribute value + * @return the unmodifiable map with a single element + */ + public static AttributeMap singleEntryMap(String attributeName, Object attributeValue) { + return new LocalAttributeMap(attributeName, attributeValue); + } + + /** + * Add all given objects to given target list. No duplicates will be added. + * The contains() method of the given target list will be used to determine + * whether or not an object is already in the list. + * @param target the collection to which to objects will be added + * @param objects the objects to add + * @return whether or not the target collection changed + */ + public static boolean addAllNoDuplicates(List target, Object[] objects) { + if (objects == null || objects.length == 0) { + return false; + } + else { + boolean changed = false; + for (int i = 0; i < objects.length; i++) { + if (!target.contains(objects[i])) { + target.add(objects[i]); + changed = true; + } + } + return changed; + } + } + + /** + * Iterator iterating over no elements (hasNext() always returns false). + */ + private static class EmptyIterator implements Iterator, Serializable { + + private EmptyIterator() { + } + + public boolean hasNext() { + return false; + } + + public Object next() { + throw new UnsupportedOperationException("There are no elements"); + } + + public void remove() { + throw new UnsupportedOperationException("There are no elements"); + } + } + + /** + * Iterator wrapping an Enumeration. + */ + private static class EnumerationIterator implements Iterator { + + private Enumeration enumeration; + + public EnumerationIterator(Enumeration enumeration) { + this.enumeration = enumeration; + } + + public boolean hasNext() { + return enumeration.hasMoreElements(); + } + + public Object next() { + return enumeration.nextElement(); + } + + public void remove() throws UnsupportedOperationException { + throw new UnsupportedOperationException("Not supported"); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/collection/LocalAttributeMap.java b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/LocalAttributeMap.java new file mode 100644 index 00000000..04496e41 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/LocalAttributeMap.java @@ -0,0 +1,318 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core.collection; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.binding.collection.MapAccessor; +import org.springframework.core.style.StylerUtils; +import org.springframework.util.Assert; + +/** + * A generic, mutable attribute map with string keys. + * + * @author Keith Donald + */ +public class LocalAttributeMap implements MutableAttributeMap, Serializable { + + /** + * The backing map storing the attributes. + */ + private Map attributes; + + /** + * A helper for accessing attributes. Marked transient and restored on + * deserialization. + */ + private transient MapAccessor attributeAccessor; + + /** + * Creates a new attribute map, initially empty. + */ + public LocalAttributeMap() { + initAttributes(createTargetMap()); + } + + /** + * Creates a new attribute map, initially empty. + * @param size the initial size + * @param loadFactor the load factor + */ + public LocalAttributeMap(int size, int loadFactor) { + initAttributes(createTargetMap(size, loadFactor)); + } + + /** + * Creates a new attribute map with a single entry. + */ + public LocalAttributeMap(String attributeName, Object attributeValue) { + initAttributes(createTargetMap(1, 1)); + put(attributeName, attributeValue); + } + + /** + * Creates a new attribute map wrapping the specified map. + */ + public LocalAttributeMap(Map map) { + Assert.notNull(map, "The target map is required"); + initAttributes(map); + } + + // implementing attribute map + + public Map asMap() { + return attributeAccessor.asMap(); + } + + public int size() { + return attributes.size(); + } + + public Object get(String attributeName) { + return attributes.get(attributeName); + } + + public boolean isEmpty() { + return attributes.isEmpty(); + } + + public boolean contains(String attributeName) { + return attributes.containsKey(attributeName); + } + + public boolean contains(String attributeName, Class requiredType) throws IllegalArgumentException { + return attributeAccessor.containsKey(attributeName, requiredType); + } + + public Object get(String attributeName, Object defaultValue) { + return attributeAccessor.get(attributeName, defaultValue); + } + + public Object get(String attributeName, Class requiredType) throws IllegalArgumentException { + return attributeAccessor.get(attributeName, requiredType); + } + + public Object get(String attributeName, Class requiredType, Object defaultValue) throws IllegalStateException { + return attributeAccessor.get(attributeName, requiredType, defaultValue); + } + + public Object getRequired(String attributeName) throws IllegalArgumentException { + return attributeAccessor.getRequired(attributeName); + } + + public Object getRequired(String attributeName, Class requiredType) throws IllegalArgumentException { + return attributeAccessor.getRequired(attributeName, requiredType); + } + + public String getString(String attributeName) throws IllegalArgumentException { + return attributeAccessor.getString(attributeName); + } + + public String getString(String attributeName, String defaultValue) throws IllegalArgumentException { + return attributeAccessor.getString(attributeName, defaultValue); + } + + public String getRequiredString(String attributeName) throws IllegalArgumentException { + return attributeAccessor.getRequiredString(attributeName); + } + + public Collection getCollection(String attributeName) throws IllegalArgumentException { + return attributeAccessor.getCollection(attributeName); + } + + public Collection getCollection(String attributeName, Class requiredType) throws IllegalArgumentException { + return attributeAccessor.getCollection(attributeName, requiredType); + } + + public Collection getRequiredCollection(String attributeName) throws IllegalArgumentException { + return attributeAccessor.getRequiredCollection(attributeName); + } + + public Collection getRequiredCollection(String attributeName, Class requiredType) throws IllegalArgumentException { + return attributeAccessor.getRequiredCollection(attributeName, requiredType); + } + + public Object[] getArray(String attributeName, Class requiredType) throws IllegalArgumentException { + return attributeAccessor.getArray(attributeName, requiredType); + } + + public Object[] getRequiredArray(String attributeName, Class requiredType) throws IllegalArgumentException { + return attributeAccessor.getRequiredArray(attributeName, requiredType); + } + + public Number getNumber(String attributeName, Class requiredType) throws IllegalArgumentException { + return attributeAccessor.getNumber(attributeName, requiredType); + } + + public Number getNumber(String attributeName, Class requiredType, Number defaultValue) + throws IllegalArgumentException { + return attributeAccessor.getNumber(attributeName, requiredType, defaultValue); + } + + public Number getRequiredNumber(String attributeName, Class requiredType) throws IllegalArgumentException { + return attributeAccessor.getRequiredNumber(attributeName, requiredType); + } + + public Integer getInteger(String attributeName) throws IllegalArgumentException { + return attributeAccessor.getInteger(attributeName); + } + + public Integer getInteger(String attributeName, Integer defaultValue) throws IllegalArgumentException { + return attributeAccessor.getInteger(attributeName, defaultValue); + } + + public Integer getRequiredInteger(String attributeName) throws IllegalArgumentException { + return attributeAccessor.getRequiredInteger(attributeName); + } + + public Long getLong(String attributeName) throws IllegalArgumentException { + return attributeAccessor.getLong(attributeName); + } + + public Long getLong(String attributeName, Long defaultValue) throws IllegalArgumentException { + return attributeAccessor.getLong(attributeName, defaultValue); + } + + public Long getRequiredLong(String attributeName) throws IllegalArgumentException { + return attributeAccessor.getRequiredLong(attributeName); + } + + public Boolean getBoolean(String attributeName) throws IllegalArgumentException { + return attributeAccessor.getBoolean(attributeName); + } + + public Boolean getBoolean(String attributeName, Boolean defaultValue) throws IllegalArgumentException { + return attributeAccessor.getBoolean(attributeName, defaultValue); + } + + public Boolean getRequiredBoolean(String attributeName) throws IllegalArgumentException { + return attributeAccessor.getRequiredBoolean(attributeName); + } + + public AttributeMap union(AttributeMap attributes) { + if (attributes == null) { + return new LocalAttributeMap(getMapInternal()); + } + else { + Map map = createTargetMap(); + map.putAll(getMapInternal()); + map.putAll(attributes.asMap()); + return new LocalAttributeMap(map); + } + } + + // implementing MutableAttributeMap + + public Object put(String attributeName, Object attributeValue) { + return getMapInternal().put(attributeName, attributeValue); + } + + public MutableAttributeMap putAll(AttributeMap attributes) { + if (attributes == null) { + return this; + } + getMapInternal().putAll(attributes.asMap()); + return this; + } + + public Object remove(String attributeName) { + return getMapInternal().remove(attributeName); + } + + public MutableAttributeMap clear() throws UnsupportedOperationException { + getMapInternal().clear(); + return this; + } + + public MutableAttributeMap replaceWith(AttributeMap attributes) throws UnsupportedOperationException { + clear(); + putAll(attributes); + return this; + } + + // helpers for subclasses + + /** + * Initializes this attribute map. + * @param attributes the attributes + */ + protected void initAttributes(Map attributes) { + this.attributes = attributes; + attributeAccessor = new MapAccessor(this.attributes); + } + + /** + * Returns the wrapped, modifiable map implementation. + */ + protected Map getMapInternal() { + return attributes; + } + + // helpers + + /** + * Factory method that returns the target map storing the data in this + * attribute map. + * @return the target map + */ + protected Map createTargetMap() { + return new HashMap(); + } + + /** + * Factory method that returns the target map storing the data in this + * attribute map. + * @param size the initial size of the map + * @param loadFactor the load factor + * @return the target map + */ + protected Map createTargetMap(int size, int loadFactor) { + return new HashMap(size, loadFactor); + } + + public boolean equals(Object o) { + if (!(o instanceof LocalAttributeMap)) { + return false; + } + LocalAttributeMap other = (LocalAttributeMap)o; + return getMapInternal().equals(other.getMapInternal()); + } + + public int hashCode() { + return getMapInternal().hashCode(); + } + + // custom serialization + + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + attributeAccessor = new MapAccessor(attributes); + } + + public String toString() { + return StylerUtils.style(attributes); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/collection/LocalParameterMap.java b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/LocalParameterMap.java new file mode 100644 index 00000000..9ee3e09e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/LocalParameterMap.java @@ -0,0 +1,324 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core.collection; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.springframework.binding.collection.MapAccessor; +import org.springframework.binding.convert.ConversionException; +import org.springframework.binding.convert.ConversionExecutor; +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.convert.support.DefaultConversionService; +import org.springframework.core.style.StylerUtils; +import org.springframework.util.Assert; +import org.springframework.web.multipart.MultipartFile; + +/** + * An immutable parameter map storing String-keyed, String-valued parameters + * in a backing {@link Map} implementation. This base provides convenient + * operations for accessing parameters in a typed-manner. + * + * @author Keith Donald + */ +public class LocalParameterMap implements ParameterMap, Serializable { + + /** + * The backing map storing the parameters. + */ + private Map parameters; + + /** + * A helper for accessing parameters. Marked transient and restored on + * deserialization. + */ + private transient MapAccessor parameterAccessor; + + /** + * A helper for converting string parameter values. Marked transient and + * restored on deserialization. + */ + private transient ConversionService conversionService; + + /** + * Creates a new parameter map from the provided map. + *

+ * It is expected that the contents of the backing map adhere to the + * parameter map contract; that is, map entries have string keys, string + * values, and remain unmodifiable. + * @param parameters the contents of this parameter map + */ + public LocalParameterMap(Map parameters) { + this(parameters, new DefaultConversionService()); + } + + /** + * Creates a new parameter map from the provided map. + *

+ * It is expected that the contents of the backing map adhere to the + * parameter map contract; that is, map entries have string keys, string + * values, and remain unmodifiable. + * @param parameters the contents of this parameter map + * @param conversionService a helper for performing type conversion of map + * entry values + */ + public LocalParameterMap(Map parameters, ConversionService conversionService) { + initParameters(parameters); + this.conversionService = conversionService; + } + + public boolean equals(Object o) { + if (!(o instanceof LocalParameterMap)) { + return false; + } + LocalParameterMap other = (LocalParameterMap)o; + return parameters.equals(other.parameters); + } + + public int hashCode() { + return parameters.hashCode(); + } + + public Map asMap() { + return Collections.unmodifiableMap(parameterAccessor.asMap()); + } + + public boolean isEmpty() { + return parameters.isEmpty(); + } + + public int size() { + return parameters.size(); + } + + public boolean contains(String parameterName) { + return parameters.containsKey(parameterName); + } + + public String get(String parameterName) { + return get(parameterName, (String)null); + } + + public String get(String parameterName, String defaultValue) { + if (!parameters.containsKey(parameterName)) { + return defaultValue; + } + Object value = parameters.get(parameterName); + if (value.getClass().isArray()) { + parameterAccessor.assertKeyValueInstanceOf(parameterName, value, String[].class); + String[] array = (String[])value; + if (array.length == 0) { + return null; + } + else { + Object first = ((String[])value)[0]; + parameterAccessor.assertKeyValueInstanceOf(parameterName, first, String.class); + return (String)first; + } + + } + else { + parameterAccessor.assertKeyValueInstanceOf(parameterName, value, String.class); + return (String)value; + } + } + + public String[] getArray(String parameterName) { + if (!parameters.containsKey(parameterName)) { + return null; + } + Object value = parameters.get(parameterName); + if (value.getClass().isArray()) { + parameterAccessor.assertKeyValueInstanceOf(parameterName, value, String[].class); + return (String[])value; + } + else { + parameterAccessor.assertKeyValueInstanceOf(parameterName, value, String.class); + return new String[] { (String)value }; + } + } + + public Object[] getArray(String parameterName, Class targetElementType) throws ConversionException { + String[] parameters = getArray(parameterName); + return parameters != null ? convert(parameters, targetElementType) : null; + } + + public Object get(String parameterName, Class targetType) throws ConversionException { + return get(parameterName, targetType, null); + } + + public Object get(String parameterName, Class targetType, Object defaultValue) throws ConversionException { + if (defaultValue != null) { + assertAssignableTo(targetType, defaultValue.getClass()); + } + String parameter = get(parameterName); + return parameter != null ? convert(parameter, targetType) : defaultValue; + } + + public String getRequired(String parameterName) throws IllegalArgumentException { + parameterAccessor.assertContainsKey(parameterName); + return get(parameterName); + } + + public String[] getRequiredArray(String parameterName) throws IllegalArgumentException { + parameterAccessor.assertContainsKey(parameterName); + return getArray(parameterName); + } + + public Object[] getRequiredArray(String parameterName, Class targetElementType) throws IllegalArgumentException, + ConversionException { + String[] parameters = getRequiredArray(parameterName); + return convert(parameters, targetElementType); + } + + public Object getRequired(String parameterName, Class targetType) throws IllegalArgumentException, + ConversionException { + return convert(getRequired(parameterName), targetType); + } + + public Number getNumber(String parameterName, Class targetType) throws ConversionException { + assertAssignableTo(Number.class, targetType); + return (Number)get(parameterName, targetType); + } + + public Number getNumber(String parameterName, Class targetType, Number defaultValue) throws ConversionException { + assertAssignableTo(Number.class, targetType); + return (Number)get(parameterName, targetType, defaultValue); + } + + public Number getRequiredNumber(String parameterName, Class targetType) throws IllegalArgumentException, + ConversionException { + assertAssignableTo(Number.class, targetType); + return (Number)getRequired(parameterName, targetType); + } + + public Integer getInteger(String parameterName) throws ConversionException { + return (Integer)get(parameterName, Integer.class); + } + + public Integer getInteger(String parameterName, Integer defaultValue) throws ConversionException { + return (Integer)get(parameterName, Integer.class, defaultValue); + } + + public Integer getRequiredInteger(String parameterName) throws IllegalArgumentException, ConversionException { + return (Integer)getRequired(parameterName, Integer.class); + } + + public Long getLong(String parameterName) throws ConversionException { + return (Long)get(parameterName, Long.class); + } + + public Long getLong(String parameterName, Long defaultValue) throws ConversionException { + return (Long)get(parameterName, Long.class, defaultValue); + } + + public Long getRequiredLong(String parameterName) throws IllegalArgumentException, ConversionException { + return (Long)getRequired(parameterName, Long.class); + } + + public Boolean getBoolean(String parameterName) throws ConversionException { + return (Boolean)get(parameterName, Boolean.class); + } + + public Boolean getBoolean(String parameterName, Boolean defaultValue) throws ConversionException { + return (Boolean)get(parameterName, Boolean.class, defaultValue); + } + + public Boolean getRequiredBoolean(String parameterName) throws IllegalArgumentException, ConversionException { + return (Boolean)getRequired(parameterName, Boolean.class); + } + + public MultipartFile getMultipartFile(String parameterName) { + return (MultipartFile)parameterAccessor.get(parameterName, MultipartFile.class); + } + + public MultipartFile getRequiredMultipartFile(String parameterName) throws IllegalArgumentException { + return (MultipartFile)parameterAccessor.getRequired(parameterName, MultipartFile.class); + } + + public AttributeMap asAttributeMap() { + return new LocalAttributeMap(getMapInternal()); + } + + /** + * Initializes this parameter map. + * @param parameters the parameters + */ + protected void initParameters(Map parameters) { + this.parameters = parameters; + parameterAccessor = new MapAccessor(this.parameters); + } + + /** + * Returns the wrapped, modifiable map implementation. + */ + protected Map getMapInternal() { + return parameters; + } + + // internal helpers + + /** + * Convert given String parameter to specified target type. + */ + private Object convert(String parameter, Class targetType) throws ConversionException { + return conversionService.getConversionExecutor(String.class, targetType).execute(parameter); + } + + /** + * Convert given array of String parameters to specified target type and + * return the resulting array. + */ + private Object[] convert(String[] parameters, Class targetElementType) throws ConversionException { + List list = new ArrayList(parameters.length); + ConversionExecutor converter = conversionService.getConversionExecutor(String.class, targetElementType); + for (int i = 0; i < parameters.length; i++) { + list.add(converter.execute(parameters[i])); + } + return list.toArray((Object[])Array.newInstance(targetElementType, parameters.length)); + } + + /** + * Make sure clazz is assignable from requiredType. + */ + private void assertAssignableTo(Class clazz, Class requiredType) { + Assert.isTrue(clazz.isAssignableFrom(requiredType), "The provided required type must be assignable to [" + + clazz + "]"); + } + + // custom serialization + + private void writeObject(ObjectOutputStream out) throws IOException { + out.defaultWriteObject(); + } + + private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { + in.defaultReadObject(); + parameterAccessor = new MapAccessor(parameters); + conversionService = new DefaultConversionService(); + } + + public String toString() { + return StylerUtils.style(parameters); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/collection/LocalSharedAttributeMap.java b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/LocalSharedAttributeMap.java new file mode 100644 index 00000000..f557870e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/LocalSharedAttributeMap.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core.collection; + +import org.springframework.binding.collection.SharedMap; + +/** + * An attribute map that exposes a mutex that application code can synchronize + * on. This class wraps another shared map in an attribute map. + *

+ * The mutex can be used to serialize concurrent access to the shared map's + * contents by multiple threads. + * + * @author Keith Donald + */ +public class LocalSharedAttributeMap extends LocalAttributeMap implements SharedAttributeMap { + + /** + * Creates a new shared attribute map. + * @param sharedMap the shared map + */ + public LocalSharedAttributeMap(SharedMap sharedMap) { + super(sharedMap); + } + + public Object getMutex() { + return getSharedMap().getMutex(); + } + + /** + * Returns the wrapped shared map. + */ + protected SharedMap getSharedMap() { + return (SharedMap)getMapInternal(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/collection/MutableAttributeMap.java b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/MutableAttributeMap.java new file mode 100644 index 00000000..5c02a99c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/MutableAttributeMap.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core.collection; + +/** + * An interface for accessing and modifying attributes in a backing map with + * string keys. + *

+ * Implementations can optionally support {@link AttributeMapBindingListener listeners} + * that will be notified when they're bound in or unbound from the map. + * + * @author Keith Donald + */ +public interface MutableAttributeMap extends AttributeMap { + + /** + * Put the attribute into this map. + *

+ * If the attribute value is an {@link AttributeMapBindingListener} this map + * will publish {@link AttributeMapBindingEvent binding events} such as on + * "bind" and "unbind" if supported. + *

+ * Note: not all MutableAttributeMap implementations support this. + * @param attributeName the attribute name + * @param attributeValue the attribute value + * @return the previous value of the attribute, or null of there + * was no previous value + */ + public Object put(String attributeName, Object attributeValue); + + /** + * Put all the attributes into this map. + * @param attributes the attributes to put into this map + * @return this, to support call chaining + */ + public MutableAttributeMap putAll(AttributeMap attributes); + + /** + * Remove an attribute from this map. + * @param attributeName the name of the attribute to remove + * @return previous value associated with specified attribute name, or + * null if there was no mapping for the name + */ + public Object remove(String attributeName); + + /** + * Remove all attributes in this map. + * @return this, to support call chaining + */ + public MutableAttributeMap clear(); + + /** + * Replace the contents of this attribute map with the contents of the + * provided collection. + * @param attributes the attribute collection + * @return this, to support call chaining + */ + public MutableAttributeMap replaceWith(AttributeMap attributes) throws UnsupportedOperationException; + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/collection/ParameterMap.java b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/ParameterMap.java new file mode 100644 index 00000000..bbddd14a --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/ParameterMap.java @@ -0,0 +1,283 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core.collection; + +import org.springframework.binding.collection.MapAdaptable; +import org.springframework.binding.convert.ConversionException; +import org.springframework.web.multipart.MultipartFile; + +/** + * An interface for accessing parameters in a backing map. Parameters are + * immutable and have string keys and string values. + * + * @author Keith Donald + */ +public interface ParameterMap extends MapAdaptable { + + /** + * Is this parameter map empty, with a size of 0? + * @return true if empty, false if not + */ + public boolean isEmpty(); + + /** + * Returns the number of parameters in this map. + * @return the parameter count + */ + public int size(); + + /** + * Does the parameter with the provided name exist in this map? + * @param parameterName the parameter name + * @return true if so, false otherwise + */ + public boolean contains(String parameterName); + + /** + * Get a parameter value, returning null if no value is + * found. + * @param parameterName the parameter name + * @return the parameter value + */ + public String get(String parameterName); + + /** + * Get a parameter value, returning the defaultValue if no value is found. + * @param parameterName the parameter name + * @param defaultValue the default + * @return the parameter value + */ + public String get(String parameterName, String defaultValue); + + /** + * Get a multi-valued parameter value, returning null if no + * value is found. If the parameter is single valued an array with a single + * element is returned. + * @param parameterName the parameter name + * @return the parameter value array + */ + public String[] getArray(String parameterName); + + /** + * Get a multi-valued parameter value, converting each value to the target + * type or returning null if no value is found. + * @param parameterName the parameter name + * @param targetElementType the target type of the array's elements + * @return the converterd parameter value array + * @throws ConversionException when the value could not be converted + */ + public Object[] getArray(String parameterName, Class targetElementType) throws ConversionException; + + /** + * Get a parameter value, converting it from String to the + * target type. + * @param parameterName the name of the parameter + * @param targetType the target type of the parameter value + * @return the converted parameter value, or null if not found + * @throws ConversionException when the value could not be converted + */ + public Object get(String parameterName, Class targetType) throws ConversionException; + + /** + * Get a parameter value, converting it from String to the + * target type or returning the defaultValue if not found. + * @param parameterName name of the parameter to get + * @param targetType the target type of the parameter value + * @param defaultValue the default value + * @return the converted parameter value, or the default if not found + * @throws ConversionException when a value could not be converted + */ + public Object get(String parameterName, Class targetType, Object defaultValue) throws ConversionException; + + /** + * Get the value of a required parameter. + * @param parameterName the name of the parameter + * @return the parameter value + * @throws IllegalArgumentException when the parameter is not found + */ + public String getRequired(String parameterName) throws IllegalArgumentException; + + /** + * Get a required multi-valued parameter value. + * @param parameterName the name of the parameter + * @return the parameter value + * @throws IllegalArgumentException when the parameter is not found + */ + public String[] getRequiredArray(String parameterName) throws IllegalArgumentException; + + /** + * Get a required multi-valued parameter value, converting each value to the + * target type. + * @param parameterName the name of the parameter + * @return the parameter value + * @throws IllegalArgumentException when the parameter is not found + * @throws ConversionException when a value could not be converted + */ + public Object[] getRequiredArray(String parameterName, Class targetElementType) throws IllegalArgumentException, + ConversionException; + + /** + * Get the value of a required parameter and convert it to the target type. + * @param parameterName the name of the parameter + * @param targetType the target type of the parameter value + * @return the converted parameter value + * @throws IllegalArgumentException when the parameter is not found + * @throws ConversionException when the value could not be converted + */ + public Object getRequired(String parameterName, Class targetType) throws IllegalArgumentException, + ConversionException; + + /** + * Returns a number parameter value in the map that is of the specified + * type, returning null if no value was found. + * @param parameterName the parameter name + * @param targetType the target number type + * @return the number parameter value + * @throws ConversionException when the value could not be converted + */ + public Number getNumber(String parameterName, Class targetType) throws ConversionException; + + /** + * Returns a number parameter value in the map of the specified type, + * returning the defaultValue if no value was found. + * @param parameterName the parameter name + * @param defaultValue the default + * @return the number parameter value + * @throws ConversionException when the value could not be converted + */ + public Number getNumber(String parameterName, Class targetType, Number defaultValue) throws ConversionException; + + /** + * Returns a number parameter value in the map, throwing an exception if the + * parameter is not present or could not be converted. + * @param parameterName the parameter name + * @return the number parameter value + * @throws IllegalArgumentException if the parameter is not present + * @throws ConversionException when the value could not be converted + */ + public Number getRequiredNumber(String parameterName, Class targetType) throws IllegalArgumentException, + ConversionException; + + /** + * Returns an integer parameter value in the map, returning + * null if no value was found. + * @param parameterName the parameter name + * @return the integer parameter value + * @throws ConversionException when the value could not be converted + */ + public Integer getInteger(String parameterName) throws ConversionException; + + /** + * Returns an integer parameter value in the map, returning the defaultValue + * if no value was found. + * @param parameterName the parameter name + * @param defaultValue the default + * @return the integer parameter value + * @throws ConversionException when the value could not be converted + */ + public Integer getInteger(String parameterName, Integer defaultValue) throws ConversionException; + + /** + * Returns an integer parameter value in the map, throwing an exception if + * the parameter is not present or could not be converted. + * @param parameterName the parameter name + * @return the integer parameter value + * @throws IllegalArgumentException if the parameter is not present + * @throws ConversionException when the value could not be converted + */ + public Integer getRequiredInteger(String parameterName) throws IllegalArgumentException, ConversionException; + + /** + * Returns a long parameter value in the map, returning null + * if no value was found. + * @param parameterName the parameter name + * @return the long parameter value + * @throws ConversionException when the value could not be converted + */ + public Long getLong(String parameterName) throws ConversionException; + + /** + * Returns a long parameter value in the map, returning the defaultValue if + * no value was found. + * @param parameterName the parameter name + * @param defaultValue the default + * @return the long parameter value + * @throws ConversionException when the value could not be converted + */ + public Long getLong(String parameterName, Long defaultValue) throws ConversionException; + + /** + * Returns a long parameter value in the map, throwing an exception if the + * parameter is not present or could not be converted. + * @param parameterName the parameter name + * @return the long parameter value + * @throws IllegalArgumentException if the parameter is not present + * @throws ConversionException when the value could not be converted + */ + public Long getRequiredLong(String parameterName) throws IllegalArgumentException, ConversionException; + + /** + * Returns a boolean parameter value in the map, returning null + * if no value was found. + * @param parameterName the parameter name + * @return the long parameter value + * @throws ConversionException when the value could not be converted + */ + public Boolean getBoolean(String parameterName) throws ConversionException; + + /** + * Returns a boolean parameter value in the map, returning the defaultValue + * if no value was found. + * @param parameterName the parameter name + * @param defaultValue the default + * @return the boolean parameter value + * @throws ConversionException when the value could not be converted + */ + public Boolean getBoolean(String parameterName, Boolean defaultValue) throws ConversionException; + + /** + * Returns a boolean parameter value in the map, throwing an exception if + * the parameter is not present or could not be converted. + * @param parameterName the parameter name + * @return the boolean parameter value + * @throws IllegalArgumentException if the parameter is not present + * @throws ConversionException when the value could not be converted + */ + public Boolean getRequiredBoolean(String parameterName) throws IllegalArgumentException, ConversionException; + + /** + * Get a multi-part file parameter value, returning null if + * no value is found. + * @param parameterName the parameter name + * @return the multipart file + */ + public MultipartFile getMultipartFile(String parameterName); + + /** + * Get the value of a required multipart file parameter. + * @param parameterName the name of the parameter + * @return the parameter value + * @throws IllegalArgumentException when the parameter is not found + */ + public MultipartFile getRequiredMultipartFile(String parameterName); + + /** + * Adapts this parameter map to an {@link AttributeMap}. + * @return the underlying map as a unmodifiable attribute map + */ + public AttributeMap asAttributeMap(); + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/collection/SharedAttributeMap.java b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/SharedAttributeMap.java new file mode 100644 index 00000000..d3070cbe --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/SharedAttributeMap.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core.collection; + +/** + * An interface to be implemented by mutable attribute maps accessed by + * multiple threads that need to be synchronized. + * + * @author Keith Donald + */ +public interface SharedAttributeMap extends MutableAttributeMap { + + /** + * Returns the shared map's mutex, which may be synchronized on to block + * access to the map by other threads. + */ + public Object getMutex(); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/collection/package.html b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/package.html new file mode 100644 index 00000000..51479014 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/collection/package.html @@ -0,0 +1,17 @@ + + +

+Core element collection types used within Spring Web Flow. +

+

+This packages defines two primary collection flavors: +

    +
  1. AttributeMap - for accessing 'attributes' that have string keys and object values. +
  2. ParameterMap - for accessing 'parameters' that have string keys and string values. +
+

+

+Each map is java.util.Map adaptable. +

+ + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/core/package.html b/spring-webflow/src/main/java/org/springframework/webflow/core/package.html new file mode 100644 index 00000000..dfee8ba6 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/core/package.html @@ -0,0 +1,7 @@ + + +

+Foundational, generic types usable by all other packages. +

+ + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/Annotated.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/Annotated.java new file mode 100644 index 00000000..6a674d8e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/Annotated.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition; + +import org.springframework.webflow.core.collection.AttributeMap; + +/** + * An interface to be implemented by objects that are annotated with attributes + * they wish to expose to clients. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface Annotated { + + /** + * Returns a short summary of this object, suitable for display as + * an icon caption or tool tip. + * @return the caption + */ + public String getCaption(); + + /** + * Returns a longer, more detailed description of this object. + * @return the description + */ + public String getDescription(); + + /** + * Returns an immutable attribute map containing the attributes annotating + * this object. These attributes provide descriptive characteristics or + * properties that may affect object behavior. + * @return the attribute map + */ + public AttributeMap getAttributes(); + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/FlowDefinition.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/FlowDefinition.java new file mode 100644 index 00000000..c7d0bab4 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/FlowDefinition.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition; + +/** + * The definition of a flow, a program that when executed carries out the + * orchestration of a task on behalf of a single client. + *

+ * A flow definition is a reusable, self-contained controller module that + * defines a blue print for an executable user task. Flows typically orchestrate + * controlled navigations or dialogs within web applications to guide users + * through fulfillment of a business process/goal that takes place over a series + * of steps, modeled as states. + *

+ * Structurally a flow definition is composed of a set of states. A + * {@link StateDefinition state} is a point in a flow where a behavior is + * executed; for example, showing a view, executing an action, spawning a + * subflow, or terminating the flow. Different types of states execute different + * behaviors in a polymorphic fashion. Most states are + * {@link TransitionableStateDefinition transitionable states}, meaning they + * can respond to events by taking the flow from one state to another. + *

+ * Each flow has exactly one {@link #getStartState() start state} which defines + * the starting point of the program. + *

+ * This interface exposes the flow's identifier, states, and other definitional + * attributes. It is suitable for introspection by tools as well as user-code at + * flow execution time. + *

+ * Flow definitions may be annotated with attributes. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface FlowDefinition extends Annotated { + + /** + * Returns the unique id of this flow. + * @return the flow id + */ + public String getId(); + + /** + * Return this flow's starting point. + * @return the start state + */ + public StateDefinition getStartState(); + + /** + * Returns the state definition with the specified id. + * @param id the state id + * @return the state definition + * @throws IllegalArgumentException if a state with this id does not exist + */ + public StateDefinition getState(String id) throws IllegalArgumentException; +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/StateDefinition.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/StateDefinition.java new file mode 100644 index 00000000..64dfffb1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/StateDefinition.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition; + +/** + * A step within a {@link FlowDefinition flow definition} where behavior is + * executed. + *

+ * States have identifiers that are local to their containing flow definitions. + * They may also be annotated with attributes. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface StateDefinition extends Annotated { + + /** + * Returns the flow definition this state belongs to. + * @return the owning flow definition + */ + public FlowDefinition getOwner(); + + /** + * Returns this state's identifier, locally unique to is containing flow + * definition. + * @return the state identifier + */ + public String getId(); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/TransitionDefinition.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/TransitionDefinition.java new file mode 100644 index 00000000..0f1e71de --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/TransitionDefinition.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition; + +/** + * A transition takes a flow from one state to another. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface TransitionDefinition extends Annotated { + + /** + * The identifier of this transition. This id value should be unique among + * all other transitions in a set. + * @return the transition identifier + */ + public String getId(); + + /** + * Returns an identification of the target state of this transition. + * This could be an actual static state id or something more dynamic, + * like a string representation of an expression evaluating the target + * state id at flow execution time. + * @return the target state identifier + */ + public String getTargetStateId(); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/TransitionableStateDefinition.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/TransitionableStateDefinition.java new file mode 100644 index 00000000..4622401d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/TransitionableStateDefinition.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition; + +/** + * A state that can transition to another state. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface TransitionableStateDefinition extends StateDefinition { + + /** + * Returns the available transitions out of this state. + * @return the available state transitions + */ + public TransitionDefinition[] getTransitions(); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/package.html b/spring-webflow/src/main/java/org/springframework/webflow/definition/package.html new file mode 100644 index 00000000..5c98c1fd --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/package.html @@ -0,0 +1,41 @@ + + +

+Core, stable abstractions for representing flow definitions. +

+

+Each flow has an indentifier and is composed of one or more states, one of which is the start state. +States may be transitionable, and if so define one or more transitions that lead to other states. +

+

+With these types a client can introspect a flow definition to reason on its attributes and traverse +its structure, perhaps to display a visual diagram. Note that the types defined in this package +do not capture the behavioral characteristics of a flow. +

+

+The following code shows the beginnings of a basic flow definition traversal algorithm: +

+    FlowDefinition flow = ...
+
+    // lookup start state
+    StateDefinition state = flow.getStartState();
+
+    // traverse to state transitions
+    traverse(state);
+
+    public void traverse(StateDefinition state) {
+        logger.info("State: " + state.getId());
+        while (state instanceof TransitionableStateDefinition) {
+            TransitionableStateDefinition transitionable = (TransitionableStateDefinition)state;
+            TransitionDefinition[] transitions = transitionable.getTransitions();
+            for (int i = 0; i < transitions.length; i++) {
+                Transition t = transitions[i];
+                logger.info("Transition " + t.getId());
+                traverse(state.getOwner().getState(t.getTargetStateId()); 
+            }
+        }    
+    }
+
+

+ + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/AbstractFlowDefinitionRegistryFactoryBean.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/AbstractFlowDefinitionRegistryFactoryBean.java new file mode 100644 index 00000000..b04deeb4 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/AbstractFlowDefinitionRegistryFactoryBean.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition.registry; + +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; + +/** + * A base class for factory beans that create populated flow definition registries. + * Subclasses should override the {@link #doPopulate(FlowDefinitionRegistry)} method + * to perform the registry population logic, typically delegating to a + * {@link FlowDefinitionRegistrar} strategy to perform the population. + * + * @author Keith Donald + */ +public abstract class AbstractFlowDefinitionRegistryFactoryBean implements FactoryBean, InitializingBean { + + /** + * The registry to register flow definitions in. + */ + private FlowDefinitionRegistry registry = createFlowDefinitionRegistry(); + + /** + * Sets the parent registry of the registry constructed by this factory + * bean. + *

+ * A child registry will delegate to its parent if it cannot fulfill a + * request to locate a flow definition itself. + * @param parent the parent flow definition registry + */ + public void setParent(FlowDefinitionRegistry parent) { + registry.setParent(parent); + } + + // implementing InitializingBean + + public final void afterPropertiesSet() throws Exception { + init(); + doPopulate(registry); + } + + // implementing FactoryBean + + public Class getObjectType() { + return FlowDefinitionRegistry.class; + } + + public boolean isSingleton() { + return true; + } + + public Object getObject() throws Exception { + // the registry is populated by the time this is called + return getRegistry(); + } + + /** + * Returns the flow definition registry constructed by the factory bean. + */ + public FlowDefinitionRegistry getRegistry() { + return registry; + } + + // subclassing hooks + + /** + * Create the flow definition registry to be populated in + * {@link #doPopulate(FlowDefinitionRegistry)}. Subclasses can override + * this method if they want to use a custom flow definition registry + * implementation. + */ + protected FlowDefinitionRegistry createFlowDefinitionRegistry() { + return new FlowDefinitionRegistryImpl(); + } + + /** + * Template method subclasses may override to perform factory bean initialization + * logic before registry population. Will be called before + * {@link #doPopulate(FlowDefinitionRegistry)}. The default implementation + * is empty. + */ + protected void init() { + } + + /** + * Template method subclasses must override to perform registry population. + * @param registry the flow definition registry to populate + */ + protected abstract void doPopulate(FlowDefinitionRegistry registry); + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/ExternalizedFlowDefinitionRegistrar.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/ExternalizedFlowDefinitionRegistrar.java new file mode 100644 index 00000000..22c01df1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/ExternalizedFlowDefinitionRegistrar.java @@ -0,0 +1,223 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition.registry; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +import org.springframework.core.io.Resource; +import org.springframework.core.style.ToStringCreator; + +/** + * A flow definition registrar that populates a flow definition registry from + * flow definitions defined within externalized resources. Encapsulates + * registration behaivior common to all externalized registrars and is not tied + * to a specific flow definition format (e.g. xml). + *

+ * Concrete subclasses are expected to derive from this class to provide + * knowledge about a particular kind of definition format by implementing the + * abstract template methods in this class. + *

+ * By default, when configuring the {@link #setLocations(Resource[]) locations} + * property, flow definitions at those locations will be assigned a registry + * identifier equal to the filename of the underlying definition resource, minus + * the filename extension. For example, a XML-based flow definition defined in + * the file "flow1.xml" will be identified as "flow1" when registered in a + * registry. + *

+ * For full control over the assignment of flow identifiers and flow properties, + * configure formal + * {@link org.springframework.webflow.definition.registry.FlowDefinitionResource} + * instances using the {@link #setResources(FlowDefinitionResource[] resources)} property. + * + * @see org.springframework.webflow.definition.registry.FlowDefinitionResource + * @see org.springframework.webflow.definition.registry.FlowDefinitionRegistry + * + * @author Keith Donald + */ +public abstract class ExternalizedFlowDefinitionRegistrar implements FlowDefinitionRegistrar { + + /** + * File locations of externalized flow definition resources to load. + * A set of {@link Resource}} objects. + */ + private Set locations = new HashSet(); + + /** + * A set of formal externalized flow definitions to load. + * A set of {@link FlowDefinitionResource} objects. + */ + private Set resources = new HashSet(); + + /** + * Sets the locations (file paths) pointing to externalized flow + * definitions. + *

+ * Flows registered from this set will be automatically assigned an id based + * on the filename of the flow resource. + * @param locations the resource locations + */ + public void setLocations(Resource[] locations) { + this.locations = new HashSet(Arrays.asList(locations)); + } + + /** + * Sets the formal set of externalized flow definitions this registrar will + * register. + *

+ * Use this method when you want full control over the assigned flow id and + * the set of properties applied to the externalized flow resources. + * @param resources the externalized flow definition specifications + */ + public void setResources(FlowDefinitionResource[] resources) { + this.resources = new HashSet(Arrays.asList(resources)); + } + + /** + * Adds a flow location pointing to an externalized flow resource. + *

+ * The flow registered from this location will automatically assigned an id + * based on the filename of the flow resource. + * @param location the definition location + */ + public boolean addLocation(Resource location) { + return locations.add(location); + } + + /** + * Adds the flow locations pointing to externalized flow resources. + *

+ * The flow registered from this location will automatically assigned an id + * based on the filename of the flow resource. + * @param locations the definition locations + */ + public boolean addLocations(Resource[] locations) { + if (locations == null) { + return false; + } + return this.locations.addAll(Arrays.asList(locations)); + } + + /** + * Adds an externalized flow definition specification pointing to an + * externalized flow resource. + *

+ * Use this method when you want full control over the assigned flow id and + * the set of properties applied to the externalized flow resource. + * @param resource the definition the definition resource + */ + public boolean addResource(FlowDefinitionResource resource) { + return resources.add(resource); + } + + /** + * Adds the externalized flow definitions pointing to externalized flow + * resources. + *

+ * Use this method when you want full control over the assigned flow id and + * the set of properties applied to the externalized flow resources. + * @param resources the definitions + */ + public boolean addResources(FlowDefinitionResource[] resources) { + if (resources == null) { + return false; + } + return this.resources.addAll(Arrays.asList(resources)); + } + + public void registerFlowDefinitions(FlowDefinitionRegistry registry) { + processLocations(registry); + processResources(registry); + } + + // internal helpers + + /** + * Register the flow definitions at the configured file locations. + * @param registry the registry + */ + private void processLocations(FlowDefinitionRegistry registry) { + Iterator it = locations.iterator(); + while (it.hasNext()) { + Resource location = (Resource)it.next(); + if (isFlowDefinitionResource(location)) { + FlowDefinitionResource resource = createFlowDefinitionResource(location); + register(resource, registry); + } + } + } + + /** + * Register the flow definitions at the configured file locations. + * @param registry the registry + */ + private void processResources(FlowDefinitionRegistry registry) { + Iterator it = resources.iterator(); + while (it.hasNext()) { + FlowDefinitionResource resource = (FlowDefinitionResource)it.next(); + register(resource, registry); + } + } + + /** + * Helper method to register the flow built from an externalized resource in + * the registry. + * @param resource representation of the externalized flow definition + * resource + * @param registry the flow registry to register the flow in + */ + protected final void register(FlowDefinitionResource resource, FlowDefinitionRegistry registry) { + registry.registerFlowDefinition(createFlowDefinitionHolder(resource)); + } + + // subclassing hooks + + /** + * Template method that calculates if the given file resource is actually a + * flow definition resource. Resources that aren't flow definitions will be + * ignored. Subclasses may override; this implementation simply returns + * true. + * @param resource the underlying resource + * @return true if yes, false otherwise + */ + protected boolean isFlowDefinitionResource(Resource resource) { + return true; + } + + /** + * Factory method that creates a flow definition from an externalized + * resource location. + * @param location the location of the resource + * @return the externalized flow definition pointer + */ + protected FlowDefinitionResource createFlowDefinitionResource(Resource location) { + return new FlowDefinitionResource(location); + } + + /** + * Template factory method subclasses must override to return the holder for + * the flow definition to be registered loaded from the specified resource. + * @param resource the externalized resource + * @return the flow definition holder + */ + protected abstract FlowDefinitionHolder createFlowDefinitionHolder(FlowDefinitionResource resource); + + public String toString() { + return new ToStringCreator(this).append("locations", locations).append("resources", resources).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionConstructionException.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionConstructionException.java new file mode 100644 index 00000000..f408f732 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionConstructionException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition.registry; + +import org.springframework.webflow.core.FlowException; + +/** + * Thrown when a flow definition was found during a lookup operation + * but could not be constructed. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public abstract class FlowDefinitionConstructionException extends FlowException { + + /** + * The id of the flow that could not be constructed. + */ + private String flowId; + + /** + * Creates an exception indicating a flow definition could not be constructed. + * @param flowId the flow id + * @param cause underlying cause of the exception + */ + public FlowDefinitionConstructionException(String flowId, Throwable cause) { + super("An exception occured constructing the flow with id '" + flowId + "'", cause); + } + + /** + * Returns the id of the flow definition that could not be constructed. + * @return the flow id + */ + public String getFlowId() { + return flowId; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionHolder.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionHolder.java new file mode 100644 index 00000000..727eb6bb --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionHolder.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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. + * + * @see FlowDefinitionRegistry#registerFlowDefinition(FlowDefinitionHolder) + * + * @author Keith Donald + */ +public interface FlowDefinitionHolder { + + /** + * Returns the id of the flow definition held by this holder. + * This is a lightweight 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 + */ + 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 + */ + public void refresh() throws FlowDefinitionConstructionException; +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionLocator.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionLocator.java new file mode 100644 index 00000000..664b5b47 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionLocator.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition.registry; + +import org.springframework.webflow.definition.FlowDefinition; + +/** + * A runtime service locator interface for retrieving flow definitions by + * id. + *

+ * 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 + */ +public interface FlowDefinitionLocator { + + /** + * Lookup the flow definition with the specified id. + * @param id the flow definition id + * @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 + */ + public FlowDefinition getFlowDefinition(String id) + throws NoSuchFlowDefinitionException, FlowDefinitionConstructionException; +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistrar.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistrar.java new file mode 100644 index 00000000..ec225100 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistrar.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition.registry; + +/** + * A strategy to use to populate a flow definition registry with one or more flow + * definitions. + *

+ * Flow definition registrars encapsulate the knowledge about the source of a set of flow + * definition resources and the behavior necessary to add those resources to a + * flow definition registry. + *

+ * The typical usage pattern is as follows: + *

    + *
  1. Create a new (initially empty) flow definition registry. + *
  2. Use any number of flow definition registrars to populate the registry by calling + * {@link #registerFlowDefinitions(FlowDefinitionRegistry)}. + *
+ *

+ * This design where various registrars populate a generic registry was + * inspired by Spring's GenericApplicationContext, which can use any number of + * BeanDefinitionReaders to drive context population. + * + * @see FlowDefinitionRegistry + * + * @author Keith Donald + */ +public interface FlowDefinitionRegistrar { + + /** + * Register flow definition resources managed by this registrar in the + * registry provided. + * @param registry the registry to register flow definitions in + */ + public void registerFlowDefinitions(FlowDefinitionRegistry registry); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistry.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistry.java new file mode 100644 index 00000000..e04ee11d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistry.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition.registry; + +import org.springframework.webflow.definition.FlowDefinition; + +/** + * A container of flow definitions. Extends the {@link FlowDefinitionRegistryMBean} + * management interface exposing registry monitoring and management operations. + * Also extends {@link FlowDefinitionLocator} for accessing registered Flow + * definitions for execution at runtime. + *

+ * Flow definition registries can be configured with a "parent" registry to provide a hook + * into a larger flow definition registry hierarchy. + * + * @author Keith Donald + */ +public interface FlowDefinitionRegistry extends FlowDefinitionLocator, FlowDefinitionRegistryMBean { + + /** + * Sets this registry's parent registry. When asked by a client to locate a + * flow definition this registry will query it's parent if it cannot + * fullfill the lookup request itself. + * @param parent the parent flow definition registry, may be null + */ + public void setParent(FlowDefinitionRegistry parent); + + /** + * Return all flow definitions registered in this registry. Note that this + * will trigger flow assemply 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. + * @param flowHolder a holder holding the flow definition to register + */ + public void registerFlowDefinition(FlowDefinitionHolder flowHolder); + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistryImpl.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistryImpl.java new file mode 100644 index 00000000..33d88456 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistryImpl.java @@ -0,0 +1,233 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition.registry; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.Map; +import java.util.TreeMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.definition.FlowDefinition; + +/** + * A generic registry implementation for housing one or more flow definitions. + *

+ * This registry may be refreshed at runtime to "hot reload" refreshable flow + * definitions. + * + * @author Keith Donald + */ +public class FlowDefinitionRegistryImpl implements FlowDefinitionRegistry { + + private static final Log logger = LogFactory.getLog(FlowDefinitionRegistryImpl.class); + + /** + * The map of loaded Flow definitions maintained in this registry. + */ + private Map flowDefinitions = new TreeMap(); + + /** + * An optional parent flow definition registry. + */ + private FlowDefinitionRegistry parent; + + // 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 + "'"); + } + 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)); + } + } + it = needsReindexing.iterator(); + while (it.hasNext()) { + Indexed indexed = (Indexed)it.next(); + reindex(indexed.holder, indexed.key); + } + } + finally { + Thread.currentThread().setContextClassLoader(loader); + } + } + + public void refresh(String flowId) + throws NoSuchFlowDefinitionException, FlowDefinitionConstructionException { + if (logger.isDebugEnabled()) { + logger.debug("Refreshing flow with id '" + flowId + "'"); + } + 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); + } + } + + // 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 + "'"); + } + try { + return getFlowDefinitionHolder(id).getFlowDefinition(); + } + catch (NoSuchFlowDefinitionException e) { + if (parent != null) { + // try parent + return parent.getFlowDefinition(id); + } + throw e; + } + } + + // implementing FlowDefinitionRegistry + + public void setParent(FlowDefinitionRegistry parent) { + if (logger.isDebugEnabled()) { + logger.debug("Setting parent flow definition registry to '" + parent + "'"); + } + 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++; + } + 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() + "'"); + } + 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 + "'"); + } + 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()); + } + 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 { + + private String key; + + private FlowDefinitionHolder holder; + + public Indexed(String key, FlowDefinitionHolder holder) { + this.key = key; + this.holder = holder; + } + } + + public String toString() { + return new ToStringCreator(this).append("flowDefinitions", flowDefinitions).append("parent", parent).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistryMBean.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistryMBean.java new file mode 100644 index 00000000..e23e9619 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistryMBean.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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. + *

+ * 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 + * MBeanExporter to export a flow registry to an MBeanServer: + *

+ *     <!-- Creates the registry of flow definitions for this application -->
+ *     <bean name="flowRegistry" class="org.springframework.webflow...XmlFlowRegistryFactoryBean">
+ *         <property name="locations" value="/WEB-INF/flow1.xml"/>
+ *     </bean>
+ *  
+ *     <!-- Automatically exports the created flowRegistry as an MBean -->
+ *     <bean id="mbeanExporter" class="org.springframework.jmx.export.MBeanExporter">
+ *         <property name="beans">
+ *             <map>
+ *                 <entry key="spring-webflow:name=flowRegistry" value-ref="flowRegistry"/>
+ *             </map>
+ *         </property>
+ *         <property name="assembler">
+ *             <bean class="org.springframework.jmx.export.assembler.InterfaceBasedMBeanInfoAssembler">
+ *                 <property name="managedInterfaces"
+ *                     value="org.springframework.webflow.definition.registry.FlowDefinitionRegistryMBean"/>
+ *             </bean>
+ *         </property>
+ *     </bean>
+ * 
+ * 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 + */ + public String[] getFlowDefinitionIds(); + + /** + * Return the number of flow definitions registered in this registry. + * @return the flow definition count + */ + 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 + */ + public boolean containsFlowDefinition(String id); + + /** + * 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 id + * 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 + */ + public void refresh(String flowDefinitionId) + throws NoSuchFlowDefinitionException, FlowDefinitionConstructionException; + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionResource.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionResource.java new file mode 100644 index 00000000..03b324fb --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/FlowDefinitionResource.java @@ -0,0 +1,151 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition.registry; + +import java.io.Serializable; + +import org.springframework.core.io.Resource; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.CollectionUtils; + +/** + * A pointer to an externalized flow definition resource. Adds assigned + * identification information about the resource including the flow id and + * attributes. + * + * @see ExternalizedFlowDefinitionRegistrar + * + * @author Keith Donald + */ +public class FlowDefinitionResource implements Serializable { + + /** + * The identifier to assign to the flow definition. + */ + private String id; + + /** + * Attributes that can be used to affect flow construction. + */ + private AttributeMap attributes; + + /** + * The externalized location of the flow definition resource. + */ + private Resource location; + + /** + * Creates a new externalized flow definition resource. The flow id assigned will be + * the same name as the externalized resource's filename, excluding the extension. + * @param location the flow resource location. + */ + public FlowDefinitionResource(Resource location) { + Assert.notNull(location, "The location of the externalized flow definition is required"); + init(conventionalFlowId(location), location, null); + } + + /** + * Creates a new externalized flow definition. + * @param id the flow id to be assigned + * @param location the flow resource location + */ + public FlowDefinitionResource(String id, Resource location) { + init(id, location, null); + } + + /** + * Creates a new externalized flow definition. + * @param id the flow id to be assigned + * @param location the flow resource location + * @param attributes flow definition attributes to be assigned + */ + public FlowDefinitionResource(String id, Resource location, AttributeMap attributes) { + init(id, location, attributes); + } + + /** + * Returns the identifier to assign to the flow definition. + */ + public String getId() { + return id; + } + + /** + * Returns the externalized flow definition resource location. + */ + public Resource getLocation() { + return location; + } + + /** + * Returns arbitrary flow definition attributes. + */ + public AttributeMap getAttributes() { + return attributes; + } + + public boolean equals(Object o) { + if (!(o instanceof FlowDefinitionResource)) { + return false; + } + FlowDefinitionResource other = (FlowDefinitionResource)o; + return id.equals(other.id) && location.equals(other.location); + } + + public int hashCode() { + return id.hashCode() + location.hashCode(); + } + + // internal helpers + + /** + * Initialize this object. + */ + private void init(String id, Resource location, AttributeMap attributes) { + Assert.hasText(id, "The id of the externalized flow definition is required"); + Assert.notNull(location, "The location of the externalized flow definition is required"); + this.id = id; + this.location = location; + if (attributes != null) { + this.attributes = attributes; + } + else { + this.attributes = CollectionUtils.EMPTY_ATTRIBUTE_MAP; + } + } + + /** + * Returns the flow id assigned to the flow definition contained in given resource. + * By convention this will be the filename of the resource, excluding extension. + */ + private String conventionalFlowId(Resource location) { + String fileName = location.getFilename(); + int extensionIndex = fileName.lastIndexOf('.'); + if (extensionIndex != -1) { + return fileName.substring(0, extensionIndex); + } + else { + return fileName; + } + } + + public String toString() { + return new ToStringCreator(this).append("id", id).append("location", location).append("attributes", attributes) + .toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/NoSuchFlowDefinitionException.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/NoSuchFlowDefinitionException.java new file mode 100644 index 00000000..0707fe8e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/NoSuchFlowDefinitionException.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition.registry; + +import org.springframework.core.style.StylerUtils; +import org.springframework.webflow.core.FlowException; + +/** + * Thrown when no flow definition was found during a lookup operation by a flow + * locator. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class NoSuchFlowDefinitionException extends FlowException { + + /** + * The id of the flow definition that could not be located. + */ + private String flowId; + + /** + * Creates an exception indicating a flow definition could not be found. + * @param flowId the flow id + * @param availableFlowIds all flow ids available to the locator generating + * this exception + */ + public NoSuchFlowDefinitionException(String flowId, String[] availableFlowIds) { + super("No such flow definition with id '" + flowId + "' found; the flows available are: " + + StylerUtils.style(availableFlowIds)); + this.flowId = flowId; + } + + /** + * Returns the id of the flow definition that could not be found. + */ + public String getFlowId() { + return flowId; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/StaticFlowDefinitionHolder.java b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/StaticFlowDefinitionHolder.java new file mode 100644 index 00000000..9e81292e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/StaticFlowDefinitionHolder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +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. + * + * @author Keith Donald + */ +public final class StaticFlowDefinitionHolder implements FlowDefinitionHolder { + + /** + * The held flow definition. + */ + private final FlowDefinition flowDefinition; + + /** + * Creates the static flow definition holder. + * @param flowDefinition the flow to hold + */ + public StaticFlowDefinitionHolder(FlowDefinition flowDefinition) { + this.flowDefinition = flowDefinition; + } + + public String getFlowDefinitionId() { + return flowDefinition.getId(); + } + + public FlowDefinition getFlowDefinition() { + return flowDefinition; + } + + public void refresh() { + // nothing to do + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/package.html b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/package.html new file mode 100644 index 00000000..3a0d224c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/definition/registry/package.html @@ -0,0 +1,25 @@ + + +

+The flow definition registry subsystem for managing containers of flow definitions. +

+

+You can construct a generic, initially empty FlowDefinitionRegistry, populate it +with flow definitions using a FlowDefinitionRegistrar, then lookup flow definitions by id. +For example: +

+    // create registry
+    FlowDefinitionRegistry registry = new FlowDefinitionRegistryImpl();
+
+    // populate registry
+    FlowDefinitionRegistrar registrar = ..
+    registrar.addLocation(...);
+    registrar.addLocation(...);
+    registrar.registerFlowDefinitions(registry);
+
+    // use registry
+    FlowDefinition flow = registry.getFlow("myFlow");
+
+

+ + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/ActionExecutionException.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/ActionExecutionException.java new file mode 100644 index 00000000..58f48f33 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/ActionExecutionException.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.FlowExecutionException; + +/** + * Thrown if an unhandled exception occurs when an action is executed. Typically + * wraps another exception noting the root cause failure. The root cause may be + * checked or unchecked. + * + * @see org.springframework.webflow.execution.Action + * @see org.springframework.webflow.engine.ActionState + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class ActionExecutionException extends FlowExecutionException { + + /** + * Create a new action execution exception. + * @param flowId the current flow + * @param stateId the current state (may be null) + * @param action the action that generated an unrecoverable exception + * @param executionAttributes action execution properties that may have contributed to this failure + * @param cause the underlying cause + */ + public ActionExecutionException(String flowId, String stateId, Action action, + AttributeMap executionAttributes, Throwable cause) { + super(flowId, stateId, "Exception thrown executing " + action + " in state '" + stateId + "' of flow '" + + flowId + "' -- action execution attributes were '" + executionAttributes + "'", cause); + } + + /** + * Create a new action execution exception. + * @param flowId the current flow + * @param stateId the current state (may be null) + * @param action the action that generated an unrecoverable exception + * @param executionAttributes action execution properties that may have contributed to this failure + * @param message a descriptive message + * @param cause the underlying cause + */ + public ActionExecutionException(String flowId, String stateId, Action action, + AttributeMap executionAttributes, String message, Throwable cause) { + super(flowId, stateId, message, cause); + } + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/ActionExecutor.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/ActionExecutor.java new file mode 100644 index 00000000..32ba8016 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/ActionExecutor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +/** + * A simple static helper that performs action execution that encapsulates + * common logging and exception handling logic. This is an internal helper class + * that is not normally used by application code. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class ActionExecutor { + + private static final Log logger = LogFactory.getLog(ActionExecutor.class); + + /** + * Private constructor to avoid instantiation. + */ + private ActionExecutor() { + } + + /** + * Execute the given action. + * @param action the action to execute + * @param context the flow execution request context + * @return result of action execution + * @throws ActionExecutionException if the action threw an exception while + * executing, the orginal exception is available as the cause if this exception + */ + public static Event execute(Action action, RequestContext context) throws ActionExecutionException { + try { + if (logger.isDebugEnabled()) { + if (context.getCurrentState() == null) { + logger.debug("Executing start " + action + " for flow '" + context.getActiveFlow().getId() + "'"); + } + else { + logger.debug("Executing " + action + " in state '" + context.getCurrentState().getId() + + "' of flow '" + context.getActiveFlow().getId() + "'"); + } + } + return action.execute(context); + } + catch (ActionExecutionException e) { + throw e; + } + catch (Exception e) { + // wrap the action as an ActionExecutionException + throw new ActionExecutionException(context.getActiveFlow().getId(), + context.getCurrentState() != null ? context.getCurrentState().getId() : null, action, context + .getAttributes(), e); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/ActionList.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/ActionList.java new file mode 100644 index 00000000..f94aaf47 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/ActionList.java @@ -0,0 +1,167 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import org.springframework.core.style.StylerUtils; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.RequestContext; + +/** + * An ordered, typed list of actions, mainly for use internally by flow artifacts + * that can execute groups of actions. + * + * @see Flow#getStartActionList() + * @see Flow#getEndActionList() + * @see State#getEntryActionList() + * @see ActionState#getActionList() + * @see TransitionableState#getExitActionList() + * @see ViewState#getRenderActionList() + * + * @author Keith Donald + */ +public class ActionList { + + /** + * The lists of actions. + */ + private List actions = new LinkedList(); + + /** + * Add an action to this list. + * @param action the action to add + * @return true if this list's contents changed as a result of the add + * operation + */ + public boolean add(Action action) { + return actions.add(action); + } + + /** + * Add a collection of actions to this list. + * @param actions the actions to add + * @return true if this list's contents changed as a result of the add + * operation + */ + public boolean addAll(Action[] actions) { + if (actions == null) { + return false; + } + return this.actions.addAll(Arrays.asList(actions)); + } + + /** + * Tests if the action is in this list. + * @param action the action + * @return true if the action is contained in this list, false otherwise + */ + public boolean contains(Action action) { + return actions.contains(action); + } + + /** + * Remove the action instance from this list. + * @param action the action to add + * @return true if this list's contents changed as a result of the remove + * operation + */ + public boolean remove(Action action) { + return actions.remove(action); + } + + /** + * Returns the size of this action list. + * @return the action list size. + */ + public int size() { + return actions.size(); + } + + /** + * Returns the action in this list at the provided index. + * @param index the action index + * @return the action the action + */ + public Action get(int index) throws IndexOutOfBoundsException { + return (Action)actions.get(index); + } + + /** + * Returns the action in this list at the provided index, exposing it as an + * annotated action. This allows clients to access specific properties about + * a target action instance if they exist. + * @return the action, as an annotated action + */ + public AnnotatedAction getAnnotated(int index) throws IndexOutOfBoundsException { + Action action = get(index); + if (action instanceof AnnotatedAction) { + return (AnnotatedAction)action; + } + else { + // wrap the action; no annotations will be available + return new AnnotatedAction(action); + } + } + + /** + * Returns an iterator over this action list. + */ + public Iterator iterator() { + return actions.iterator(); + } + + /** + * Convert this list to a typed action array. + * @return the action list, as a typed array + */ + public Action[] toArray() { + return (Action[])actions.toArray(new Action[actions.size()]); + } + + /** + * Returns the list of actions in this list as a typed annotated action + * array. This is a convenience method allowing clients to access properties + * about an action if they exist. + * @return the annotated action list, as a typed array + */ + public AnnotatedAction[] toAnnotatedArray() { + AnnotatedAction[] annotatedActions = new AnnotatedAction[actions.size()]; + for (int i = 0; i < size(); i++) { + annotatedActions[i] = getAnnotated(i); + } + return annotatedActions; + } + + /** + * Executes the actions contained within this action list. Simply iterates + * over each action and calls execute. Action result events are ignored. + * @param context the action execution request context + */ + public void execute(RequestContext context) { + Iterator it = actions.iterator(); + while (it.hasNext()) { + ActionExecutor.execute((Action)it.next(), context); + } + } + + public String toString() { + return StylerUtils.style(actions); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/ActionState.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/ActionState.java new file mode 100644 index 00000000..528e9859 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/ActionState.java @@ -0,0 +1,252 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import java.util.Iterator; + +import org.springframework.core.style.StylerUtils; +import org.springframework.core.style.ToStringCreator; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; + +/** + * A transitionable state that executes one or more actions when entered. When + * the action(s) are executed this state responds to their result(s) to decide + * what state to transition to next. + *

+ * If more than one action is configured they are executed in an ordered chain + * until one returns a result event that matches a state transition out of + * this state. This is a form of the Chain of Responsibility (CoR) pattern. + *

+ * The result of an action's execution is typically the criteria for a + * transition out of this state. Additional information in the current + * {@link RequestContext} may also be tested as part of custom transitional + * criteria, allowing for sophisticated transition expressions that reason on + * contextual state. + *

+ * Each action executed by this action state may be provisioned with a set of + * arbitrary execution properties. These properties are made available to the + * action at execution time and may be used to influence action execution + * behavior. + *

+ * Common action execution properties include: + *

+ * + * + * + * + * + * + * + * + * + * + *
PropertyDescription
nameThe 'name' property is used as a qualifier for an action's result event, + * and is typically used to allow the flow to respond to a specific action's + * outcome within a larger action chain. For example, if an action named + * myAction returns a success result, a transition + * that matches on event myAction.success will be searched, and + * if found, executed. If this action is not assigned a name a transition for + * the base success event will be searched and if found, + * executed.
+ * This is useful in situations where you want to execute actions in an ordered + * chain as part of one action state, and wish to transition on the result of + * the last one in the chain. For example: + * + *
+ *     <action-state id="setupForm"> 
+ *         <action name="setup" bean="myAction" method="setupForm"/> 
+ *         <action name="referenceData" bean="myAction" method="setupReferenceData"/> 
+ *         <transition on="referenceData.success" to="displayForm"/> 
+ *     </action-state>
+ * 
+ * + * When the 'setupForm' state above is entered, the 'setup' action will execute, + * followed by the 'referenceData' action. After 'referenceData' execution, the + * flow will then respond to the 'referenceData.success' event by transitioning + * to the 'displayForm' state. The 'setup.success' event that was signaled by + * the 'setup' action will effectively be ignored.
methodThe 'method' property is the name of a target method on a + * {@link org.springframework.webflow.action.MultiAction} to + * execute. In the MultiAction scenario the named method must have the signature + * public Event ${method}(RequestContext) throws Exception. + * As an example of this scenario, a method property with value setupForm + * would bind to a method on a MultiAction instance with the signature: + * public Event setupForm(RequestContext context).
+ * As an alternative to a MultiAction method binding, this action state may + * excute a + * {@link org.springframework.webflow.action.AbstractBeanInvokingAction bean invoking action} + * that invokes a method on a POJO (Plain Old Java Object). If the method + * signature accepts arguments those arguments may be specified by using the + * format: + * + *
+ *      methodName(${arg1}, ${arg2}, ...)
+ * 
+ * + * Argument ${expressions} are evaluated against the current + * RequestContext, allowing for data stored in flow scope or + * request scope to be passed as arguments to the POJO. In addition, POJO return + * values may be exposed to the flow automatically. See the bean invoking action + * type hierarchy for more information.
+ * + * @see org.springframework.webflow.execution.Action + * @see org.springframework.webflow.action.MultiAction + * @see org.springframework.webflow.action.AbstractBeanInvokingAction + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class ActionState extends TransitionableState { + + /** + * The list of actions to be executed when this state is entered. + */ + private ActionList actionList = new ActionList(); + + /** + * Creates a new action state. + * @param flow the owning flow + * @param id the state identifier (must be unique to the flow) + * @throws IllegalArgumentException when this state cannot be added to given flow, + * e.g. beasue the id is not unique + * @see #getActionList() + */ + public ActionState(Flow flow, String id) throws IllegalArgumentException { + super(flow, id); + } + + /** + * Returns the list of actions executable by this action state. The + * returned list is mutable. + * @return the state action list + */ + public ActionList getActionList() { + return actionList; + } + + /* + * Overrides getRequiredTransition(RequestContext) to throw a local + * NoMatchingActionResultTransitionException if a transition on the + * occurence of an action result event cannot be matched. Used to facilitate + * an action invocation chain. + *

Note that we cannot catch NoMatchingTransitionException since that could lead to unwanted + * situations where we're catching an exception that's generated by another + * state, e.g. because of a configuration error! + */ + public Transition getRequiredTransition(RequestContext context) throws NoMatchingTransitionException { + Transition transition = getTransitionSet().getTransition(context); + if (transition == null) { + throw new NoMatchingActionResultTransitionException(this, context.getLastEvent()); + } + return transition; + } + + /** + * Specialization of State's doEnter template method that + * executes behaviour specific to this state type in polymorphic fashion. + *

+ * This implementation iterates over each configured Action + * instance and executes it. Execution continues until an + * Action returns a result event that matches a transition in + * this request context, or the set of all actions is exhausted. + * @param context the control context for the currently executing flow, used + * by this state to manipulate the flow execution + * @return a view selection signaling that control should be returned to the + * client and a view rendered + * @throws FlowExecutionException if an exception occurs in this state + */ + protected ViewSelection doEnter(RequestControlContext context) throws FlowExecutionException { + int executionCount = 0; + String[] eventIds = new String[actionList.size()]; + Iterator it = actionList.iterator(); + while (it.hasNext()) { + Action action = (Action)it.next(); + Event event = ActionExecutor.execute(action, context); + if (event != null) { + eventIds[executionCount] = event.getId(); + try { + // will check both local state transitions and global transitions + return context.signalEvent(event); + } + catch (NoMatchingActionResultTransitionException e) { + if (logger.isDebugEnabled()) { + logger.debug("Action execution [" + + (executionCount + 1) + + "] resulted in no matching transition on event '" + + event.getId() + + "'" + + (it.hasNext() ? ": proceeding to the next action in the list" + : ": action list exhausted")); + } + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Action execution [" + + (executionCount + 1) + + "] returned a [null] event" + + (it.hasNext() ? ": proceeding to the next action in the list" + : ": action list exhausted")); + } + eventIds[executionCount] = null; + } + executionCount++; + } + if (executionCount > 0) { + throw new NoMatchingTransitionException(getFlow().getId(), getId(), context.getLastEvent(), + "No transition was matched on the event(s) signaled by the [" + executionCount + + "] action(s) that executed in this action state '" + getId() + "' of flow '" + + getFlow().getId() + "'; transitions must be defined to handle action result outcomes -- " + + "possible flow configuration error? Note: the eventIds signaled were: '" + + StylerUtils.style(eventIds) + + "', while the supported set of transitional criteria for this action state is '" + + StylerUtils.style(getTransitionSet().getTransitionCriterias()) + "'"); + } + else { + throw new IllegalStateException( + "No actions were executed, thus I cannot execute any state transition " + + "-- programmer configuration error; make sure you add at least one action to this state's action list"); + } + } + + protected void appendToString(ToStringCreator creator) { + creator.append("actionList", actionList); + super.appendToString(creator); + } + + /** + * Local "no transition found" exception used to report that an action + * result could not be mapped to a state transition. + * + * @author Keith Donald + * @author Erwin Vervaet + */ + private static class NoMatchingActionResultTransitionException extends NoMatchingTransitionException { + + /** + * Creates a new exception. + * @param state the action state + * @param resultEvent the action result event + */ + public NoMatchingActionResultTransitionException(ActionState state, Event resultEvent) { + super(state.getFlow().getId(), state.getId(), resultEvent, + "Cannot find a transition matching an action result event; continuing with next action..."); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/AnnotatedAction.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/AnnotatedAction.java new file mode 100644 index 00000000..5f5ed02d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/AnnotatedAction.java @@ -0,0 +1,175 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +/** + * An action proxy/decorator that stores arbitrary properties about a target + * Action implementation for use within a specific Action + * execution context, for example an ActionState definition, a + * TransitionCriteria definition, or in a test environment. + *

+ * An annotated action is an action that wraps another action (referred to as + * the target action), setting up the target action's execution attributes + * before invoking {@link Action#execute}. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class AnnotatedAction extends AnnotatedObject implements Action { + + // well known attributes + + /** + * The action name attribute ("name"). + *

+ * The name attribute is often used as a qualifier for an action's result + * event, and is typically used to allow the flow to respond to a specific + * action's outcome within a larger action execution chain. + * @see ActionState + */ + public static final String NAME_ATTRIBUTE = "name"; + + /** + * The action execution method attribute ("method"). + *

+ * The method property is a hint about what method should be invoked; for + * example, the name of a specific target method on a + * {@link org.springframework.webflow.action.MultiAction multi action}. + * @see ActionState + */ + public static final String METHOD_ATTRIBUTE = "method"; + + /** + * The target action to execute. + */ + private Action targetAction; + + /** + * Creates a new annotated action object for the specified action. No + * contextual properties are provided. + * @param targetAction the action + */ + public AnnotatedAction(Action targetAction) { + setTargetAction(targetAction); + } + + /** + * Returns the wrapped target action. + * @return the action + */ + public Action getTargetAction() { + return targetAction; + } + + /** + * Set the target action wrapped by this decorator. + */ + public void setTargetAction(Action targetAction) { + Assert.notNull(targetAction, "The targetAction to annotate is required"); + this.targetAction = targetAction; + } + + /** + * Returns the name of a named action, or null if the action + * is unnamed. Used when mapping action result events to transitions. + * @see #isNamed() + * @see #postProcessResult(Event) + */ + public String getName() { + return getAttributeMap().getString(NAME_ATTRIBUTE); + } + + /** + * Sets the name of a named action. This is optional and can be + * null. + * @param name the action name + */ + public void setName(String name) { + getAttributeMap().put(NAME_ATTRIBUTE, name); + } + + /** + * Returns whether or not the wrapped target action is a named action. + * @see #getName() + * @see #setName(String) + */ + public boolean isNamed() { + return StringUtils.hasText(getName()); + } + + /** + * Returns the name of the action method to invoke when the target action is + * executed. + */ + public String getMethod() { + return getAttributeMap().getString(METHOD_ATTRIBUTE); + } + + /** + * Sets the name of the action method to invoke when the target action is + * executed. + * @param method the action method name + */ + public void setMethod(String method) { + getAttributeMap().put(METHOD_ATTRIBUTE, method); + } + + public Event execute(RequestContext context) throws Exception { + AttributeMap originalAttributes = getAttributeMap(); + try { + context.setAttributes(getAttributeMap()); + Event result = getTargetAction().execute(context); + return postProcessResult(result); + } + finally { + // restore original attributes + context.setAttributes(originalAttributes); + } + } + + /** + * Get the event id to be used as grounds for a transition in the containing + * state, based on given result returned from action execution. + *

+ * If the wrapped action is named, the name will be used as a qualifier for + * the event (e.g. "myAction.success"). + * @param resultEvent the action result event + */ + protected Event postProcessResult(Event resultEvent) { + if (resultEvent == null) { + return null; + } + if (isNamed()) { + // qualify result event id with action name for a named action + String qualifiedId = getName() + "." + resultEvent.getId(); + resultEvent = new Event(resultEvent.getSource(), qualifiedId, resultEvent.getAttributes()); + } + return resultEvent; + } + + public String toString() { + return new ToStringCreator(this).append("targetAction", getTargetAction()) + .append("attributes", getAttributeMap()).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/AnnotatedObject.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/AnnotatedObject.java new file mode 100644 index 00000000..2938e241 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/AnnotatedObject.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.Annotated; + +/** + * A base class for all objects in the web flow system that support annotation + * using arbitrary properties. Mainly used to ensure consistent configuration of + * properties for all annotated objects. + * + * @author Erwin Vervaet + * @author Keith Donald + */ +public abstract class AnnotatedObject implements Annotated { + + /** + * The caption property name ("caption"). A caption is also known as a + * "short description" and may be used in a GUI tooltip. + */ + public static final String CAPTION_PROPERTY = "caption"; + + /** + * The long description property name ("description"). A description + * provides additional, free-form detail about this object and might be + * shown in a GUI text area. + */ + public static final String DESCRIPTION_PROPERTY = "description"; + + /** + * Additional properties further describing this object. The properties set + * in this map may be arbitrary. + */ + private LocalAttributeMap attributes = new LocalAttributeMap(); + + // implementing Annotated + + public String getCaption() { + return attributes.getString(CAPTION_PROPERTY); + } + + public String getDescription() { + return attributes.getString(DESCRIPTION_PROPERTY); + } + + public AttributeMap getAttributes() { + return attributes; + } + + // mutators + + /** + * Sets the short description (suitable for display in a tooltip). + * @param caption the caption + */ + public void setCaption(String caption) { + attributes.put(CAPTION_PROPERTY, caption); + } + + /** + * Sets the long description. + * @param description the long description + */ + public void setDescription(String description) { + attributes.put(DESCRIPTION_PROPERTY, description); + } + + /** + * Returns the mutable attribute map for this annotated object. May be used + * to set attributes after construction. + */ + public MutableAttributeMap getAttributeMap() { + return attributes; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/DecisionState.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/DecisionState.java new file mode 100644 index 00000000..e87cf3b5 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/DecisionState.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; + +/** + * A simple transitionable state that when entered will execute the first + * transition whose matching criteria evaluates to true in the + * {@link RequestContext context} of the current request. + *

+ * A decision state is a convenient, simple way to encapsulate reusable state + * transition logic in one place. + * + * @author Keith Donald + */ +public class DecisionState extends TransitionableState { + + /** + * Creates a new decision state. + * @param flow the owning flow + * @param stateId the state identifier (must be unique to the flow) + * @throws IllegalArgumentException when this state cannot be added to given + * flow, e.g. because the id is not unique + */ + public DecisionState(Flow flow, String stateId) throws IllegalArgumentException { + super(flow, stateId); + } + + /** + * Specialization of State's doEnter template method that + * executes behaviour specific to this state type in polymorphic fashion. + *

+ * Simply looks up the first transition that matches the state of the + * context and executes it. + * @param context the control context for the currently executing flow, used + * by this state to manipulate the flow execution + * @return a view selection containing model and view information needed to + * render the results of the state execution + * @throws FlowExecutionException if an exception occurs in this state + */ + protected ViewSelection doEnter(RequestControlContext context) throws FlowExecutionException { + return getRequiredTransition(context).execute(this, context); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/EndState.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/EndState.java new file mode 100644 index 00000000..f75aa487 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/EndState.java @@ -0,0 +1,166 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.FlowSession; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; + +/** + * A state that ends a flow when entered. More specifically, this state ends the + * active flow session of the active flow execution associated with the current + * request context. + *

+ * If the ended session is the "root flow session" the entire flow execution + * ends, signaling the end of a logical conversation. + *

+ * If the terminated session was acting as a subflow the flow execution + * continues and control is returned to the parent flow session. In that case, + * this state returns an ending result event the resuming parent flow is + * expected to respond to. + *

+ * An end state may optionally be configured with the name of a view to render + * when entered. This view will be rendered if the end state terminates the + * entire flow execution as a kind of flow ending "confirmation page". + *

+ * Note: if no viewName property is specified and this + * end state terminates the entire flow execution it is expected that some + * action has already written the response (or else a blank response will + * result). On the other hand, if no viewName is specified and + * this end state relinquishes control back to a parent flow, view selection + * responsibility falls on the parent flow. + * + * @see org.springframework.webflow.engine.ViewSelector + * @see org.springframework.webflow.engine.SubflowState + * + * @author Keith Donald + * @author Colin Sampaleanu + * @author Erwin Vervaet + */ +public class EndState extends State { + + /** + * The optional view selector that will select a view to render if this end + * state terminates a root flow session. + */ + private ViewSelector viewSelector = NullViewSelector.INSTANCE; + + /** + * Attribute mapper for mapping output attributes exposed by this end state + * when it is entered. + */ + private AttributeMapper outputMapper; + + /** + * Create a new end state with no associated view. + * @param flow the owning flow + * @param id the state identifier (must be unique to the flow) + * @throws IllegalArgumentException when this state cannot be added to given + * flow, e.g. because the id is not unique + * @see State#State(Flow, String) + * @see #setViewSelector(ViewSelector) + * @see #setOutputMapper(AttributeMapper) + */ + public EndState(Flow flow, String id) throws IllegalArgumentException { + super(flow, id); + } + + /** + * Returns the strategy used to select the view to render in this end state + * if it terminates a root flow. + */ + public ViewSelector getViewSelector() { + return viewSelector; + } + + /** + * Sets the strategy used to select the view to render when this end state + * is entered and terminates a root flow. + */ + public void setViewSelector(ViewSelector viewSelector) { + Assert.notNull(viewSelector, "The view selector is required"); + this.viewSelector = viewSelector; + } + + /** + * Returns the configured attribute mapper for mapping output attributes + * exposed by this end state when it is entered. + */ + public AttributeMapper getOutputMapper() { + return outputMapper; + } + + /** + * Sets the attribute mapper to use for mapping output attributes exposed by + * this end state when it is entered. + */ + public void setOutputMapper(AttributeMapper outputMapper) { + this.outputMapper = outputMapper; + } + + /** + * Specialization of State's doEnter template method that + * executes behaviour specific to this state type in polymorphic fashion. + *

+ * This implementation pops the top (active) flow session off the execution + * stack, ending it, and resumes control in the parent flow (if neccessary). + * If the ended session is the root flow, a {@link ViewSelection} is + * returned. + * @param context the control context for the currently executing flow, used + * by this state to manipulate the flow execution + * @return a view selection signaling that control should be returned to the + * client and a view rendered + * @throws FlowExecutionException if an exception occurs in this state + */ + protected ViewSelection doEnter(RequestControlContext context) throws FlowExecutionException { + FlowSession activeSession = context.getFlowExecutionContext().getActiveSession(); + if (activeSession.isRoot()) { + // entire flow execution is ending, return ending view if applicable + ViewSelection selectedView = viewSelector.makeEntrySelection(context); + context.endActiveFlowSession(createSessionOutput(context)); + return selectedView; + } + else { + // there is a parent flow that will resume (this flow is a subflow) + LocalAttributeMap sessionOutput = createSessionOutput(context); + context.endActiveFlowSession(sessionOutput); + return context.signalEvent(new Event(this, getId(), sessionOutput)); + } + } + + /** + * Returns the subflow output map. This will invoke the output mapper (if any) + * to map data available in the flow execution request context into a newly + * creaed empty map. + */ + protected LocalAttributeMap createSessionOutput(RequestContext context) { + LocalAttributeMap outputMap = new LocalAttributeMap(); + if (outputMapper != null) { + outputMapper.map(context, outputMap, null); + } + return outputMap; + } + + protected void appendToString(ToStringCreator creator) { + creator.append("viewSelector", viewSelector).append("outputMapper", outputMapper); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/Flow.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/Flow.java new file mode 100644 index 00000000..c5865440 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/Flow.java @@ -0,0 +1,656 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import java.util.Iterator; +import java.util.Set; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.core.CollectionFactory; +import org.springframework.core.style.StylerUtils; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; + +/** + * A single flow definition. A Flow definition is a reusable, self-contained + * controller module that provides the blue print for a user dialog or + * conversation. Flows typically orchestrate controlled navigations within web + * applications to guide users through fulfillment of a business process/goal + * that takes place over a series of steps, modeled as states. + *

+ * A simple Flow definition could do nothing more than execute an action and + * display a view all in one request. A more elaborate Flow definition may be + * long-lived and execute across a series of requests, invoking many possible + * paths, actions, and subflows. + *

+ * Especially in Intranet applications there are often "controlled navigations" + * where the user is not free to do what he or she wants but must follow the + * guidelines provided by the system to complete a process that is transactional + * in nature (the quinessential example would be a 'checkout' flow of a shopping + * cart application). This is a typical use case appropriate to model as a flow. + *

+ * Structurally a Flow is composed of a set of states. A {@link State} is a + * point in a flow where a behavior is executed; for example, showing a view, + * executing an action, spawning a subflow, or terminating the flow. Different + * types of states execute different behaviors in a polymorphic fashion. + *

+ * Each {@link TransitionableState} type has one or more transitions that when + * executed move a flow to another state. These transitions define the supported + * paths through the flow. + *

+ * A state transition is triggered by the occurence of an event. An event is + * something that happens the flow should respond to, for example a user input + * event like ("submit") or an action execution result event like ("success"). + * When an event occurs in a state of a Flow that event drives a state + * transition that decides what to do next. + *

+ * Each Flow has exactly one start state. A start state is simply a marker + * noting the state executions of this Flow definition should start in. The + * first state added to the flow will become the start state by default. + *

+ * Flow definitions may have one or more flow exception handlers. A + * {@link FlowExecutionExceptionHandler} can execute custom behavior in response + * to a specific exception (or set of exceptions) that occur in a state of one + * of this flow's executions. + *

+ * Instances of this class are typically built by + * {@link org.springframework.webflow.engine.builder.FlowBuilder} + * implementations but may also be directly instantiated. + *

+ * This class and the rest of the Spring Web Flow (SWF) engine have been designed + * with minimal dependencies on other libraries. Spring Web Flow is usable in a + * standalone fashion (as well as in the context of other frameworks like Spring + * MVC, Struts, or JSF, for example). The engine system is fully usable outside an + * HTTP servlet environment, for example in portlets, tests, or standalone + * applications. One of the major architectural benefits of Spring Web Flow is + * the ability to design reusable, high-level controller modules that may be + * executed in any environment. + *

+ * Note: flows are singleton definition objects so they should be thread-safe. + * You can think a flow definition as analagous somewhat to a Java class, + * defining all the behavior of an application module. The core behaviors + * {@link #start(RequestControlContext, MutableAttributeMap) start}, + * {@link #onEvent(RequestControlContext) on event}, and + * {@link #end(RequestControlContext, MutableAttributeMap) end} each accept a + * {@link RequestContext request context} that allows for this flow to access + * execution state in a thread safe manner. A flow execution is what models a + * running instance of this flow definition, somewhat analgous to a java object + * that is an instance of a class. + * + * @see org.springframework.webflow.engine.State + * @see org.springframework.webflow.engine.TransitionableState + * @see org.springframework.webflow.engine.ActionState + * @see org.springframework.webflow.engine.ViewState + * @see org.springframework.webflow.engine.SubflowState + * @see org.springframework.webflow.engine.EndState + * @see org.springframework.webflow.engine.DecisionState + * @see org.springframework.webflow.engine.Transition + * @see org.springframework.webflow.engine.FlowExecutionExceptionHandler + * + * @author Keith Donald + * @author Erwin Vervaet + * @author Colin Sampaleanu + */ +public class Flow extends AnnotatedObject implements FlowDefinition { + + /** + * Logger, can be used in subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * An assigned flow identifier uniquely identifying this flow among all + * other flows. + */ + private String id; + + /** + * The set of state definitions for this flow. + */ + private Set states = CollectionFactory.createLinkedSetIfPossible(9); + + /** + * The default start state for this flow. + */ + private State startState; + + /** + * The set of flow variables created by this flow. + */ + private Set variables = CollectionFactory.createLinkedSetIfPossible(3); + + /** + * The mapper to map flow input attributes. + */ + private AttributeMapper inputMapper; + + /** + * The list of actions to execute when this flow starts. + *

+ * Start actions should execute with care as during startup a flow session + * has not yet fully initialized and some properties like its "currentState" + * have not yet been set. + */ + private ActionList startActionList = new ActionList(); + + /** + * The set of global transitions that are shared by all states of this flow. + */ + private TransitionSet globalTransitionSet = new TransitionSet(); + + /** + * The list of actions to execute when this flow ends. + */ + private ActionList endActionList = new ActionList(); + + /** + * The mapper to map flow output attributes. + */ + private AttributeMapper outputMapper; + + /** + * The set of exception handlers for this flow. + */ + private FlowExecutionExceptionHandlerSet exceptionHandlerSet = new FlowExecutionExceptionHandlerSet(); + + /** + * The set of inline flows contained by this flow. + */ + private Set inlineFlows = CollectionFactory.createLinkedSetIfPossible(3); + + /** + * Construct a new flow definition with the given id. The id should be + * unique among all flows. + * @param id the flow identifier + */ + public Flow(String id) { + setId(id); + } + + // implementing FlowDefinition + + public String getId() { + return id; + } + + public StateDefinition getStartState() { + if (startState == null) { + throw new IllegalStateException("No start state has been set for this flow ('" + getId() + + "') -- flow builder configuration error?"); + } + return startState; + } + + public StateDefinition getState(String stateId) { + return getStateInstance(stateId); + } + + /** + * Set the unique id of this flow. + */ + protected void setId(String id) { + Assert.hasText(id, "This flow must have a unique, non-blank identifier"); + this.id = id; + } + + /** + * Add given state definition to this flow definition. Marked protected, as + * this method is to be called by the (privileged) state definition classes + * themselves during state construction as part of a FlowBuilder invocation. + * @param state the state to add + * @throws IllegalArgumentException when the state cannot be added to the + * flow; for instance if another state shares the same id as the one + * provided or if given state already belongs to another flow + */ + protected void add(State state) throws IllegalArgumentException { + if (this != state.getFlow() && state.getFlow() != null) { + throw new IllegalArgumentException("State " + state + " cannot be added to this flow '" + getId() + + "' -- it already belongs to a different flow: '" + state.getFlow().getId() + "'"); + } + if (this.states.contains(state) || this.containsState(state.getId())) { + throw new IllegalArgumentException("This flow '" + getId() + "' already contains a state with id '" + + state.getId() + "' -- state ids must be locally unique to the flow definition; " + + "existing state-ids of this flow include: " + StylerUtils.style(getStateIds())); + } + boolean firstAdd = states.isEmpty(); + states.add(state); + if (firstAdd) { + setStartState(state); + } + } + + /** + * Returns the number of states defined in this flow. + * @return the state count + */ + public int getStateCount() { + return states.size(); + } + + /** + * Is a state with the provided id present in this flow? + * @param stateId the state id + * @return true if yes, false otherwise + */ + public boolean containsState(String stateId) { + Iterator it = states.iterator(); + while (it.hasNext()) { + State state = (State)it.next(); + if (state.getId().equals(stateId)) { + return true; + } + } + return false; + } + + /** + * Set the start state for this flow to the state with the provided + * stateId; a state must exist by the provided + * stateId. + * @param stateId the id of the new start state + * @throws IllegalArgumentException when no state exists with the id you + * provided + */ + public void setStartState(String stateId) throws IllegalArgumentException { + setStartState(getStateInstance(stateId)); + } + + /** + * Set the start state for this flow to the state provided; any state may be + * the start state. + * @param state the new start state + * @throws IllegalArgumentException given state has not been added to this + * flow + */ + public void setStartState(State state) throws IllegalArgumentException { + if (!states.contains(state)) { + throw new IllegalArgumentException("State '" + state + "' is not a state of flow '" + getId() + "'"); + } + startState = state; + } + + /** + * Return the TransitionableState with given stateId. + * @param stateId id of the state to look up + * @return the transitionable state + * @throws IllegalArgumentException if the identified state cannot be found + * @throws ClassCastException when the identified state is not + * transitionable + */ + public TransitionableState getTransitionableState(String stateId) + throws IllegalArgumentException, ClassCastException { + State state = getStateInstance(stateId); + if (state != null && !(state instanceof TransitionableState)) { + throw new ClassCastException("The state '" + stateId + "' of flow '" + getId() + "' must be transitionable"); + } + return (TransitionableState)state; + } + + /** + * Lookup the identified state instance of this flow. + * @param stateId the state id + * @return the state + * @throws IllegalArgumentException if the identified state cannot be found + */ + public State getStateInstance(String stateId) throws IllegalArgumentException { + if (!StringUtils.hasText(stateId)) { + throw new IllegalArgumentException("The specified stateId is invalid: state identifiers must be non-blank"); + } + Iterator it = states.iterator(); + while (it.hasNext()) { + State state = (State)it.next(); + if (state.getId().equals(stateId)) { + return state; + } + } + throw new IllegalArgumentException("Cannot find state with id '" + stateId + "' in flow '" + getId() + "' -- " + + "Known state ids are '" + StylerUtils.style(getStateIds()) + "'"); + } + + /** + * Convenience accessor that returns an ordered array of the String + * ids for the state definitions associated with this flow + * definition. + * @return the state ids + */ + public String[] getStateIds() { + String[] stateIds = new String[getStateCount()]; + int i = 0; + Iterator it = states.iterator(); + while (it.hasNext()) { + stateIds[i++] = ((State)it.next()).getId(); + } + return stateIds; + } + + /** + * Adds a flow variable. + * @param variable the variable + */ + public void addVariable(FlowVariable variable) { + variables.add(variable); + } + + /** + * Adds flow variables. + * @param variables the variables + */ + public void addVariables(FlowVariable[] variables) { + if (variables == null) { + return; + } + for (int i = 0; i < variables.length; i++) { + addVariable(variables[i]); + } + } + + /** + * Returns the flow variables. + */ + public FlowVariable[] getVariables() { + return (FlowVariable[])variables.toArray(new FlowVariable[variables.size()]); + } + + /** + * Returns the configured flow input mapper, or null if none. + * @return the input mapper + */ + public AttributeMapper getInputMapper() { + return inputMapper; + } + + /** + * Sets the mapper to map flow input attributes. + * @param inputMapper the input mapper + */ + public void setInputMapper(AttributeMapper inputMapper) { + this.inputMapper = inputMapper; + } + + /** + * Returns the list of actions executed by this flow when an execution of + * the flow starts. The returned list is mutable. + * @return the start action list + */ + public ActionList getStartActionList() { + return startActionList; + } + + /** + * Returns the list of actions executed by this flow when an execution of + * the flow ends. The returned list is mutable. + * @return the end action list + */ + public ActionList getEndActionList() { + return endActionList; + } + + /** + * Returns the configured flow output mapper, or null if none. + * @return the output mapper + */ + public AttributeMapper getOutputMapper() { + return outputMapper; + } + + /** + * Sets the mapper to map flow output attributes. + * @param outputMapper the output mapper + */ + public void setOutputMapper(AttributeMapper outputMapper) { + this.outputMapper = outputMapper; + } + + /** + * Returns the set of exception handlers, allowing manipulation of how + * exceptions are handled when thrown during flow execution. Exception + * handlers are invoked when an exception occurs at execution time + * and can execute custom exception handling logic as well as select an + * error view to display. Exception handlers attached at the flow + * level have an opportunity to handle exceptions that aren't handled at the + * state level. + * @return the exception handler set + */ + public FlowExecutionExceptionHandlerSet getExceptionHandlerSet() { + return exceptionHandlerSet; + } + + /** + * Adds an inline flow to this flow. + * @param flow the inline flow to add + */ + public void addInlineFlow(Flow flow) { + inlineFlows.add(flow); + } + + /** + * Returns the list of inline flow ids. + * @return a string array of inline flow identifiers + */ + public String[] getInlineFlowIds() { + String[] flowIds = new String[getInlineFlowCount()]; + int i = 0; + Iterator it = inlineFlows.iterator(); + while (it.hasNext()) { + flowIds[i++] = ((Flow)it.next()).getId(); + } + return flowIds; + } + + /** + * Returns the list of inline flows. + * @return the list of inline flows + */ + public Flow[] getInlineFlows() { + return (Flow[])inlineFlows.toArray(new Flow[inlineFlows.size()]); + } + + /** + * Returns the count of registered inline flows. + * @return the count + */ + public int getInlineFlowCount() { + return inlineFlows.size(); + } + + /** + * Tests if this flow contains an in-line flow with the specified id. + * @param id the inline flow id + * @return true if this flow contains a inline flow with that id, false + * otherwise + */ + public boolean containsInlineFlow(String id) { + return getInlineFlow(id) != null; + } + + /** + * Returns the inline flow with the provided id, or null if + * no such inline flow exists. + * @param id the inline flow id + * @return the inline flow + * @throws IllegalArgumentException when an invalid flow id is provided + */ + public Flow getInlineFlow(String id) throws IllegalArgumentException { + if (!StringUtils.hasText(id)) { + throw new IllegalArgumentException( + "The specified inline flowId is invalid: flow identifiers must be non-blank"); + } + Iterator it = inlineFlows.iterator(); + while (it.hasNext()) { + Flow flow = (Flow)it.next(); + if (flow.getId().equals(id)) { + return flow; + } + } + return null; + } + + /** + * Returns the set of transitions eligible for execution by this flow if no + * state-level transition is matched. The returned set is mutable. + * @return the global transition set + */ + public TransitionSet getGlobalTransitionSet() { + return globalTransitionSet; + } + + // id based equality + + public boolean equals(Object o) { + if (!(o instanceof Flow)) { + return false; + } + Flow other = (Flow)o; + return id.equals(other.id); + } + + public int hashCode() { + return id.hashCode(); + } + + // behavioral code, could be overridden in subclasses + + /** + * Start a new session for this flow in its start state. This boils down to + * the following: + *

    + *
  1. Create (setup) all registered flow variables ({@link #addVariable(FlowVariable)}) + * in flow scope.
  2. + *
  3. Map provided input data into the flow execution control context. + * Typically data will be mapped into flow scope using the registered input + * mapper ({@link #setInputMapper(AttributeMapper)}).
  4. + *
  5. Execute all registered start actions ({@link #getStartActionList()}).
  6. + *
  7. Enter the configured start state ({@link #setStartState(State)})
  8. + *
+ * @param context the flow execution control context + * @param input eligible input into the session + * @throws FlowExecutionException when an exception occurs starting the flow + */ + public ViewSelection start(RequestControlContext context, MutableAttributeMap input) throws FlowExecutionException { + createVariables(context); + if (inputMapper != null) { + inputMapper.map(input, context, null); + } + startActionList.execute(context); + return startState.enter(context); + } + + /** + * Inform this flow definition that an event was signaled in the current + * state of an active flow execution. The signaled event is the last event + * available in given request context ({@link RequestContext#getLastEvent()}). + * @param context the flow execution control context + * @return the selected view + * @throws FlowExecutionException when an exception occurs processing the + * event + */ + public ViewSelection onEvent(RequestControlContext context) throws FlowExecutionException { + TransitionableState currentState = getCurrentTransitionableState(context); + try { + return currentState.onEvent(context); + } + catch (NoMatchingTransitionException e) { + // try the flow level transition set for a match + Transition transition = globalTransitionSet.getTransition(context); + if (transition != null) { + return transition.execute(currentState, context); + } + else { + // no matching global transition => let the original exception + // propagate + throw e; + } + } + } + + /** + * Inform this flow definition that an execution session of itself has + * ended. As a result, the flow will do the following: + *
    + *
  1. Execute all registered end actions ({@link #getEndActionList()}).
  2. + *
  3. Map data available in the flow execution control context into + * provided output map using a registered output mapper + * ({@link #setOutputMapper(AttributeMapper)}).
  4. + *
+ * @param context the flow execution control context + * @param output initial output produced by the session that is eligible for + * modification by this method + * @throws FlowExecutionException when an exception occurs ending this flow + */ + public void end(RequestControlContext context, MutableAttributeMap output) throws FlowExecutionException { + endActionList.execute(context); + if (outputMapper != null) { + outputMapper.map(context, output, null); + } + } + + /** + * Handle an exception that occured during an execution of this flow. + * @param exception the exception that occured + * @param context the flow execution control context + * @return the selected error view, or null if no handler + * matched or returned a non-null view selection + */ + public ViewSelection handleException(FlowExecutionException exception, RequestControlContext context) + throws FlowExecutionException { + return getExceptionHandlerSet().handleException(exception, context); + } + + // internal helpers + + /** + * Create (setup) all known flow variables in flow scope. + */ + private void createVariables(RequestContext context) { + Iterator it = variables.iterator(); + while (it.hasNext()) { + FlowVariable variable = (FlowVariable)it.next(); + if (logger.isDebugEnabled()) { + logger.debug("Creating " + variable); + } + variable.create(context); + } + } + + /** + * Returns the current state and makes sure it is transitionable. + */ + private TransitionableState getCurrentTransitionableState(RequestControlContext context) { + State currentState = (State)context.getCurrentState(); + if (!(currentState instanceof TransitionableState)) { + throw new IllegalStateException("You can only signal events in transitionable states, and state " + + context.getCurrentState() + " is not transitionable - programmer error"); + } + return (TransitionableState)currentState; + } + + public String toString() { + return new ToStringCreator(this).append("id", id).append("states", states).append("startState", startState) + .append("variables", variables).append("inputMapper", inputMapper).append("startActionList", + startActionList).append("exceptionHandlerSet", exceptionHandlerSet).append( + "globalTransitionSet", globalTransitionSet).append("endActionList", endActionList).append( + "outputMapper", outputMapper).append("inlineFlows", inlineFlows).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/FlowAttributeMapper.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/FlowAttributeMapper.java new file mode 100644 index 00000000..b5a805b5 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/FlowAttributeMapper.java @@ -0,0 +1,117 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.execution.RequestContext; + +/** + * A service interface that maps attributes between two flows. Used by the + * subflow state to map attributes between a parent flow and its sub flow. + *

+ * An attribute mapper may map attributes of a parent flow down to a child flow + * as input when the child is spawned as a subflow. In addition, a + * mapper may map output attributes of a subflow into a resuming parent flow as + * output when the child session ends and control is returned to the + * parent flow. + *

+ * For example, say you have the following parent flow session: + *

+ * + *

+ *     Parent Flow Session
+ *     -------------------
+ *     -> flow = myFlow
+ *     -> flowScope = [map-> attribute1=value1, attribute2=value2, attribute3=value3]
+ * 
+ * + *

+ * For the "Parent Flow Session" above, there are 3 attributes in flow scope + * ("attribute1", "attribute2" and "attribute3", respectively). Any of these + * three attributes may be mapped as input down to child subflows when those + * subflows are spawned. An implementation of this interface performs the actual + * mapping, encapsulating knowledge of which attributes should be + * mapped, and how they will be mapped (for example, will the same + * attribute names be used between flows or not?). + *

+ * For example: + *

+ * + *

+ *     Flow Attribute Mapper Configuration
+ *     -----------------------------------
+ *     -> inputMappings  = [map-> flowScope.attribute1->attribute1, flowScope.attribute3->attribute4]
+ *     -> outputMappings = [map-> attribute4->flowScope.attribute3]
+ * 
+ * + *

+ * The above example "Flow Attribute Mapper" specifies + * inputMappings that define which parent attributes to map as + * input to the child. In this case, two attributes in flow scope of the parent + * are mapped, "attribute1" and "attribute3". "attribute1" is mapped with the + * name "attribute1" (given the same name in both flows), while "attribute3" is + * mapped to "attribute4", given a different name that is local to the child + * flow. + *

+ * Likewise, when a child flow ends the outputMappings define + * which output attributes to map into the parent. In this case the subflow + * output attribute "attribute4" will be mapped up to the parent as "attribute3", + * updating the value of "attribute3" in the parent's flow scope. Note: only + * output attributes exposed by the end state of the ending subflow are eligible + * for mapping. + *

+ * A FlowAttributeMapper is typically implemented using 2 distinct + * {@link org.springframework.binding.mapping.AttributeMapper} implementations: + * one responsible for input mapping and one taking care of output mapping. + *

+ * Note: because FlowAttributeMappers are singletons, take care not to store + * and/or modify caller-specific state in a unsafe manner. The + * FlowAttributeMapper methods run in an independently executing thread on each + * invocation so make sure you deal only with local data or internal, + * thread-safe services. + * + * @see org.springframework.webflow.engine.SubflowState + * @see org.springframework.binding.mapping.AttributeMapper + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface FlowAttributeMapper { + + /** + * Create a map of attributes that should be passed as input to a + * spawning flow. + *

+ * Attributes set in the map returned by this method are availale + * as input to the subflow when its session is spawned. + * @param context the current request execution context, which gives access + * to the parent flow scope, the request scope, any event parameters, etcetera + * @return a map of attributes (name=value pairs) to pass as input to the + * spawning subflow + */ + public MutableAttributeMap createFlowInput(RequestContext context); + + /** + * Map output attributes of an ended flow to a resuming parent flow session. + * This maps the output of the child as new input to the resuming + * parent, typically adding data to flow scope. + * @param flowOutput the output attributes exposed by the ended subflow + * @param context the current request execution context, which gives access + * to the parent flow scope + */ + public void mapFlowOutput(AttributeMap flowOutput, RequestContext context); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/FlowExecutionExceptionHandler.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/FlowExecutionExceptionHandler.java new file mode 100644 index 00000000..717703c1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/FlowExecutionExceptionHandler.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.ViewSelection; + +/** + * A strategy for handling an exception that occurs at runtime during the + * execution of a flow definition. + * + * @author Keith Donald + */ +public interface FlowExecutionExceptionHandler { + + /** + * Can this handler handle the given exception? + * @param exception the exception that occured + * @return true if yes, false if no + */ + public boolean handles(FlowExecutionException exception); + + /** + * Handle the exception in the context of the current request, optionally + * making an error view selection that should be rendered. + * @param exception the exception that occured + * @param context the execution control context for this request + * @return the selected error view that should be displayed (may be null if + * the handler chooses not to select a view, in which case other exception + * handlers may be given a chance to handle the exception) + */ + public ViewSelection handle(FlowExecutionException exception, RequestControlContext context); +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/FlowExecutionExceptionHandlerSet.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/FlowExecutionExceptionHandlerSet.java new file mode 100644 index 00000000..a2dccd15 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/FlowExecutionExceptionHandlerSet.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import org.springframework.core.style.StylerUtils; +import org.springframework.webflow.core.collection.CollectionUtils; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.ViewSelection; + +/** + * A typed set of state exception handlers, mainly for use internally by + * artifacts that can apply state exception handling logic. + * + * @see FlowExecutionExceptionHandler + * @see Flow#getExceptionHandlerSet() + * @see State#getExceptionHandlerSet() + * + * @author Keith Donald + */ +public class FlowExecutionExceptionHandlerSet { + + /** + * The set of exception handlers. + */ + private List exceptionHandlers = new LinkedList(); + + /** + * Add a state exception handler to this set. + * @param exceptionHandler the exception handler to add + * @return true if this set's contents changed as a result of the add + * operation + */ + public boolean add(FlowExecutionExceptionHandler exceptionHandler) { + if (contains(exceptionHandler)) { + return false; + } + return exceptionHandlers.add(exceptionHandler); + } + + /** + * Add a collection of state exception handler instances to this set. + * @param exceptionHandlers the exception handlers to add + * @return true if this set's contents changed as a result of the add + * operation + */ + public boolean addAll(FlowExecutionExceptionHandler[] exceptionHandlers) { + return CollectionUtils.addAllNoDuplicates(this.exceptionHandlers, exceptionHandlers); + } + + /** + * Tests if this state exception handler is in this set. + * @param exceptionHandler the exception handler + * @return true if the state exception handler is contained in this set, + * false otherwise + */ + public boolean contains(FlowExecutionExceptionHandler exceptionHandler) { + return exceptionHandlers.contains(exceptionHandler); + } + + /** + * Remove the exception handler instance from this set. + * @param exceptionHandler the exception handler to add + * @return true if this set's contents changed as a result of the remove + * operation + */ + public boolean remove(FlowExecutionExceptionHandler exceptionHandler) { + return exceptionHandlers.remove(exceptionHandler); + } + + /** + * Returns the size of this state exception handler set. + * @return the exception handler set size + */ + public int size() { + return exceptionHandlers.size(); + } + + /** + * Convert this list to a typed state exception handler array. + * @return the exception handler list, as a typed array + */ + public FlowExecutionExceptionHandler[] toArray() { + return (FlowExecutionExceptionHandler[])exceptionHandlers.toArray(new FlowExecutionExceptionHandler[exceptionHandlers.size()]); + } + + /** + * Handle an exception that occured during the context of the current flow + * execution request. + *

+ * This implementation iterates over the ordered set of exception handler + * objects, delegating to each handler in the set until one handles the + * exception that occured and selects a non-null error view. + * @param exception the exception that occured + * @param context the flow execution control context + * @return the selected error view, or null if no handler + * matched or returned a non-null view selection + */ + public ViewSelection handleException(FlowExecutionException exception, RequestControlContext context) { + Iterator it = exceptionHandlers.iterator(); + while (it.hasNext()) { + FlowExecutionExceptionHandler handler = (FlowExecutionExceptionHandler)it.next(); + if (handler.handles(exception)) { + ViewSelection result = handler.handle(exception, context); + if (result != null) { + return result; + } + // else continue with next handler + } + } + return null; + } + + public String toString() { + return StylerUtils.style(exceptionHandlers); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/FlowVariable.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/FlowVariable.java new file mode 100644 index 00000000..9e439aa3 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/FlowVariable.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import java.io.Serializable; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ScopeType; + +/** + * A value object that defines a specification for a flow variable. Encapsulates + * information about the variable and the behavior necessary to create a new + * variable instance in a flow execution scope. + * + * @author Keith Donald + */ +public abstract class FlowVariable extends AnnotatedObject implements Serializable { + + /** + * The variable name. + */ + private String name; + + /** + * The variable scope. + */ + private ScopeType scope; + + /** + * Creates a new flow variable. + * @param name the variable name + * @param scope the variable scope type + */ + public FlowVariable(String name, ScopeType scope) { + Assert.hasText(name, "The variable name is required"); + Assert.notNull(scope, "The variable scope type is required"); + this.name = name; + this.scope = scope; + } + + /** + * Returns the name of this variable. + */ + public String getName() { + return name; + } + + /** + * Returns the scope of this variable. + */ + public ScopeType getScope() { + return scope; + } + + // name and scope based equality + + public boolean equals(Object o) { + if (!(o instanceof FlowVariable)) { + return false; + } + FlowVariable other = (FlowVariable)o; + return name.equals(other.name) && scope.equals(other.scope); + } + + public int hashCode() { + return name.hashCode() + scope.hashCode(); + } + + /** + * Creates a new instance of this flow variable in the configured scope. + * @param context the flow execution request context + */ + public final void create(RequestContext context) { + scope.getScope(context).put(name, createVariableValue(context)); + } + + /** + * Hook method that needs to be implemented by subclasses to calculate the + * value of this flow variable based on the information available in the + * request context. + * @param context the flow execution request context + * @return the flow variable value + */ + protected abstract Object createVariableValue(RequestContext context); + + public String toString() { + return new ToStringCreator(this).append("name", name).append("scope", scope).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/NoMatchingTransitionException.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/NoMatchingTransitionException.java new file mode 100644 index 00000000..6466e505 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/NoMatchingTransitionException.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecutionException; + +/** + * Thrown when no transition can be matched given the occurence of an event in + * the context of a flow execution request. + *

+ * Typically this happens because there is no "handler" transition for the last + * event that occured. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class NoMatchingTransitionException extends FlowExecutionException { + + /** + * The event that occured that could not be matched to a Transition. + */ + private Event event; + + /** + * Create a new no matching transition exception. + * @param flowId the current flow + * @param stateId the state that could not be transitioned out of + * @param event the event that occured that could not be matched to a + * transition + * @param message the message + */ + public NoMatchingTransitionException(String flowId, String stateId, Event event, String message) { + super(flowId, stateId, message); + this.event = event; + } + + /** + * Create a new no matching transition exception. + * @param flowId the current flow + * @param stateId the state that could not be transitioned out of + * @param event the event that occured that could not be matched to a + * transition + * @param message the message + * @param cause the underlying cause + */ + public NoMatchingTransitionException(String flowId, String stateId, Event event, String message, Throwable cause) { + super(flowId, stateId, message, cause); + this.event = event; + } + + /** + * Returns the event for the current request that did not trigger any + * supported transition. + */ + public Event getEvent() { + return event; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/NullViewSelector.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/NullViewSelector.java new file mode 100644 index 00000000..c3997717 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/NullViewSelector.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import java.io.ObjectStreamException; +import java.io.Serializable; + +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; + +/** + * Makes a null view selection, indicating no response should be issued. + * + * @see org.springframework.webflow.execution.ViewSelection#NULL_VIEW + * + * @author Keith Donald + */ +public final class NullViewSelector implements ViewSelector, Serializable { + + /* + * Implementation note: not located in webflow.execution.support package to + * avoid a cyclic dependency between webflow.execution and webflow.execution.support. + */ + + /** + * The shared singleton {@link NullViewSelector} instance. + */ + public static final ViewSelector INSTANCE = new NullViewSelector(); + + /** + * Private constructor since this is a singleton. + */ + private NullViewSelector() { + } + + public boolean isEntrySelectionRenderable(RequestContext context) { + return true; + } + + public ViewSelection makeEntrySelection(RequestContext context) { + return ViewSelection.NULL_VIEW; + } + + public ViewSelection makeRefreshSelection(RequestContext context) { + return makeEntrySelection(context); + } + + // resolve the singleton instance + private Object readResolve() throws ObjectStreamException { + return INSTANCE; + } + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/RequestControlContext.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/RequestControlContext.java new file mode 100644 index 00000000..c7b1b36f --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/RequestControlContext.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecutionContext; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.FlowSession; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; + +/** + * Mutable control interface used to manipulate an ongoing flow execution in the + * context of one client request. Primarily used internally by the various flow + * artifacts when they are invoked. + *

+ * This interface acts as a facade for core definition constructs such as the + * central Flow and State classes, abstracting + * away details about the runtime execution machine defined in the + * {@link org.springframework.webflow.engine.impl execution engine implementation} + * package. + *

+ * Note this type is not the same as the {@link FlowExecutionContext}. Objects + * of this type are request specific: they provide a control interface + * for manipulating exactly one flow execution locally from exactly one request. + * A FlowExecutionContext provides information about a single + * flow execution (conversation), and it's scope is not local to a specific + * request (or thread). + * + * @see org.springframework.webflow.engine.Flow + * @see org.springframework.webflow.engine.State + * @see org.springframework.webflow.execution.FlowExecution + * @see FlowExecutionContext + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface RequestControlContext extends RequestContext { + + /** + * Record the last event signaled in the executing flow. This method will be + * called as part of signaling an event in a flow to indicate the + * 'lastEvent' that was signaled. + * @param lastEvent the last event signaled + * @see Flow#onEvent(RequestControlContext) + */ + public void setLastEvent(Event lastEvent); + + /** + * Record the last transition that executed in the executing flow. This + * method will be called as part of executing a transition from one state to + * another. + * @param lastTransition the last transition that executed + * @see Transition#execute(State, RequestControlContext) + */ + public void setLastTransition(Transition lastTransition); + + /** + * Record the current state that has entered in the executing flow. This + * method will be called as part of entering a new state by the State type + * itself. + * @param state the current state + * @see State#enter(RequestControlContext) + */ + public void setCurrentState(State state); + + /** + * Spawn a new flow session and activate it in the currently executing flow. + * Also transitions the spawned flow to its start state. This method should + * be called by clients that wish to spawn new flows, such as subflow + * states. + *

+ * This will start a new flow session in the current flow execution, which + * is already active. + * @param flow the flow to start, its start() method will be + * called + * @param input initial contents of the newly created flow session (may be + * null, e.g. empty) + * @return the selected starting view, which returns control to the client + * and requests that a view be rendered with model data + * @throws FlowExecutionException if an exception was thrown within a state + * of the flow during execution of this start operation + * @see Flow#start(RequestControlContext, MutableAttributeMap) + */ + public ViewSelection start(Flow flow, MutableAttributeMap input) throws FlowExecutionException; + + /** + * Signals the occurence of an event in the current state of this flow + * execution request context. This method should be called by clients that + * report internal event occurences, such as action states. The + * onEvent() method of the flow involved in the flow + * execution will be called. + * @param event the event that occured + * @return the next selected view, which returns control to the client and + * requests that a view be rendered with model data + * @throws FlowExecutionException if an exception was thrown within a state + * of the flow during execution of this signalEvent operation + * @see Flow#onEvent(RequestControlContext) + */ + public ViewSelection signalEvent(Event event) throws FlowExecutionException; + + /** + * End the active flow session of the current flow execution. This method + * should be called by clients that terminate flows, such as end states. The + * end() method of the flow involved in the flow execution + * will be called. + * @param output output produced by the session that is eligible for mapping + * by a resuming parent flow + * @return the ended session + * @throws IllegalStateException when the flow execution is not active + * @see Flow#end(RequestControlContext, MutableAttributeMap) + */ + public FlowSession endActiveFlowSession(MutableAttributeMap output) throws IllegalStateException; + + /** + * Execute this transition out of the current source state. Allows for + * privileged execution of an arbitrary transition. + * @param transition the transition + * @return a new view selection + * @see Transition#execute(State, RequestControlContext) + */ + public ViewSelection execute(Transition transition); + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/State.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/State.java new file mode 100644 index 00000000..c0575cd0 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/State.java @@ -0,0 +1,242 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.ViewSelection; + +/** + * A point in a flow where something happens. What happens is determined by a + * state's type. Standard types of states include action states, view states, + * subflow states, and end states. + *

+ * Each state is associated with exactly one owning flow definition. + * Specializations of this class capture all the configuration information + * needed for a specific kind of state. + *

+ * Subclasses should implement the doEnter method to execute the + * processing that should occur when this state is entered, acting on its + * configuration information. The ability to plugin custom state types that + * execute different behaviour polymorphically is the classic GoF state pattern. + *

+ * Equality: Two states are equal if they have the same id and are part of the same flow. + * + * @see org.springframework.webflow.engine.TransitionableState + * @see org.springframework.webflow.engine.ActionState + * @see org.springframework.webflow.engine.ViewState + * @see org.springframework.webflow.engine.SubflowState + * @see org.springframework.webflow.engine.EndState + * @see org.springframework.webflow.engine.DecisionState + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public abstract class State extends AnnotatedObject implements StateDefinition { + + /** + * Logger, for use in subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * The state's owning flow. + */ + private Flow flow; + + /** + * The state identifier, unique to the owning flow. + */ + private String id; + + /** + * The list of actions to invoke when this state is entered. + */ + private ActionList entryActionList = new ActionList(); + + /** + * The set of exception handlers for this state. + */ + private FlowExecutionExceptionHandlerSet exceptionHandlerSet = new FlowExecutionExceptionHandlerSet(); + + /** + * Creates a state for the provided flow identified by the + * provided id. The id must be locally unique to the owning + * flow. The state will be automatically added to the flow. + * @param flow the owning flow + * @param id the state identifier (must be unique to the flow) + * @throws IllegalArgumentException if this state cannot be added to the + * flow, for instance when the provided id is not unique in the owning flow + * @see #getEntryActionList() + * @see #getExceptionHandlerSet() + */ + protected State(Flow flow, String id) throws IllegalArgumentException { + setId(id); + setFlow(flow); + } + + // implementing StateDefinition + + public FlowDefinition getOwner() { + return flow; + } + + public String getId() { + return id; + } + + // implementation specific + + /** + * Returns the owning flow. + */ + public Flow getFlow() { + return flow; + } + + /** + * Set the owning flow. + * @throws IllegalArgumentException if this state cannot be added to the + * flow + */ + private void setFlow(Flow flow) throws IllegalArgumentException { + Assert.hasText(getId(), "The id of the state should be set before adding the state to a flow"); + Assert.notNull(flow, "The owning flow is required"); + this.flow = flow; + flow.add(this); + } + + /** + * Set the state identifier, unique to the owning flow. + * @param id the state identifier + */ + private void setId(String id) { + Assert.hasText(id, "This state must have a valid identifier"); + this.id = id; + } + + /** + * Returns the list of actions executed by this state when it is entered. + * The returned list is mutable. + * @return the state entry action list + */ + public ActionList getEntryActionList() { + return entryActionList; + } + + /** + * Returns a mutable set of exception handlers, allowing manipulation of how + * exceptions are handled when thrown within this state. + *

+ * Exception handlers are invoked when an exception occurs when this state + * is entered, and can execute custom exception handling logic as well as + * select an error view to display. + * @return the state exception handler set + */ + public FlowExecutionExceptionHandlerSet getExceptionHandlerSet() { + return exceptionHandlerSet; + } + + /** + * Returns a flag indicating if this state is the start state of its owning + * flow. + * @return true if the flow is the start state, false otherwise + */ + public boolean isStartState() { + return flow.getStartState() == this; + } + + // id and flow based equality + + public boolean equals(Object o) { + if (!(o instanceof State)) { + return false; + } + State other = (State)o; + return id.equals(other.id) && flow.equals(other.flow); + } + + public int hashCode() { + return id.hashCode() + flow.hashCode(); + } + + // behavioral methods + + /** + * Enter this state in the provided flow control context. This + * implementation just calls the + * {@link #doEnter(RequestControlContext)} hook method, which should + * be implemented by subclasses, after executing the entry actions. + * @param context the control context for the currently executing flow, used + * by this state to manipulate the flow execution + * @return a view selection containing model and view information needed to + * render the results of the state processing + * @throws FlowExecutionException if an exception occurs in this state + */ + public final ViewSelection enter(RequestControlContext context) throws FlowExecutionException { + if (logger.isDebugEnabled()) { + logger.debug("Entering state '" + getId() + "' of flow '" + getFlow().getId() + "'"); + } + context.setCurrentState(this); + entryActionList.execute(context); + return doEnter(context); + } + + /** + * Hook method to execute custom behaviour as a result of entering this + * state. By implementing this method subclasses specialize the behaviour of + * the state. + * @param context the control context for the currently executing flow, used + * by this state to manipulate the flow execution + * @return a view selection containing model and view information needed to + * render the results of the state processing + * @throws FlowExecutionException if an exception occurs in this state + */ + protected abstract ViewSelection doEnter(RequestControlContext context) throws FlowExecutionException; + + /** + * Handle an exception that occured in this state during the context of the + * current flow execution request. + * @param exception the exception that occured + * @param context the flow execution control context + * @return the selected error view, or null if no handler + * matched or returned a non-null view selection + */ + public ViewSelection handleException(FlowExecutionException exception, RequestControlContext context) { + return getExceptionHandlerSet().handleException(exception, context); + } + + public String toString() { + String flowName = (flow == null ? "" : flow.getId()); + ToStringCreator creator = new ToStringCreator(this).append("id", getId()).append("flow", flowName).append( + "entryActionList", entryActionList).append("exceptionHandlerSet", exceptionHandlerSet); + appendToString(creator); + return creator.toString(); + } + + /** + * Subclasses may override this hook method to stringify their internal + * state. This default implementation does nothing. + * @param creator the toString creator, to stringify properties + */ + protected void appendToString(ToStringCreator creator) { + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/SubflowState.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/SubflowState.java new file mode 100644 index 00000000..f3f08246 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/SubflowState.java @@ -0,0 +1,178 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; + +/** + * A transitionable state that spawns a subflow when executed. When the subflow + * this state spawns ends, the ending result is used as grounds for a state + * transition out of this state. + *

+ * A subflow state may be configured to map input data from its flow -- acting + * as the parent flow -- down to the subflow when the subflow is spawned. In + * addition, output data produced by the subflow may be mapped up to the parent + * flow when the subflow ends and the parent flow resumes. See the + * {@link FlowAttributeMapper} interface definition for more information on how + * to do this. The logic for ending a subflow is located in the {@link EndState} + * implementation. + * + * @see org.springframework.webflow.engine.FlowAttributeMapper + * @see org.springframework.webflow.engine.EndState + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class SubflowState extends TransitionableState { + + /** + * The subflow that should be spawned when this subflow state is entered. + */ + private Flow subflow; + + /** + * The attribute mapper that should map attributes from the parent flow down + * to the spawned subflow and visa versa. + */ + private FlowAttributeMapper attributeMapper; + + /** + * Create a new subflow state. + * @param flow the owning flow + * @param id the state identifier (must be unique to the flow) + * @param subflow the subflow to spawn + * @throws IllegalArgumentException when this state cannot be added to given + * flow, e.g. because the id is not unique + * @see #setAttributeMapper(FlowAttributeMapper) + */ + public SubflowState(Flow flow, String id, Flow subflow) throws IllegalArgumentException { + super(flow, id); + setSubflow(subflow); + } + + /** + * Returns the subflow spawned by this state. + */ + public Flow getSubflow() { + return subflow; + } + + /** + * Set the subflow that will be spawned by this state. + * @param subflow the subflow to spawn + */ + private void setSubflow(Flow subflow) { + Assert.notNull(subflow, "A subflow state must have a subflow; the subflow is required"); + this.subflow = subflow; + } + + /** + * Returns the attribute mapper used to map data between the parent and child + * flow, or null if no mapping is needed. + */ + public FlowAttributeMapper getAttributeMapper() { + return attributeMapper; + } + + /** + * Set the attribute mapper used to map model data between the parent and + * child flow. Can be null if no mapping is needed. + */ + public void setAttributeMapper(FlowAttributeMapper attributeMapper) { + this.attributeMapper = attributeMapper; + } + + /** + * Specialization of State's doEnter template method that + * executes behaviour specific to this state type in polymorphic fashion. + *

+ * Entering this state, creates the subflow input map and spawns the subflow + * in the current flow execution. + * @param context the control context for the currently executing flow, used + * by this state to manipulate the flow execution + * @return a view selection containing model and view information needed to + * render the results of the state execution + * @throws FlowExecutionException if an exception occurs in this state + */ + protected ViewSelection doEnter(RequestControlContext context) throws FlowExecutionException { + if (logger.isDebugEnabled()) { + logger.debug("Spawning subflow '" + getSubflow().getId() + "' within flow '" + getFlow().getId() + "'"); + } + return context.start(getSubflow(), createSubflowInput(context)); + } + + /** + * Create the input data map for the spawned subflow session. The returned + * map will be passed to {@link Flow#start(RequestControlContext, MutableAttributeMap)}. + */ + protected MutableAttributeMap createSubflowInput(RequestContext context) { + if (getAttributeMapper() != null) { + if (logger.isDebugEnabled()) { + logger.debug("Messaging the configured attribute mapper to map attributes " + + "down to the spawned subflow for access within the subflow"); + } + return getAttributeMapper().createFlowInput(context); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("No attribute mapper configured for this subflow state '" + getId() + + "' -- As a result, no attributes will be passed to the spawned subflow '" + + subflow.getId() + "'"); + } + return null; + } + } + + /** + * Called on completion of the subflow to handle the subflow result event as + * determined by the end state reached by the subflow. + */ + public ViewSelection onEvent(RequestControlContext context) { + mapSubflowOutput(context.getLastEvent().getAttributes(), context); + return super.onEvent(context); + } + + /** + * Map the output data produced by the subflow back into the request context + * (typically flow scope). + */ + private void mapSubflowOutput(AttributeMap subflowOutput, RequestContext context) { + if (getAttributeMapper() != null) { + if (logger.isDebugEnabled()) { + logger.debug("Messaging the configured attribute mapper to map subflow result attributes to the " + + "resuming parent flow -- It will have access to attributes passed up by the completed subflow"); + } + attributeMapper.mapFlowOutput(subflowOutput, context); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("No attribute mapper is configured for the resuming subflow state '" + getId() + + "' -- As a result, no attributes of the ending flow will be passed to the resuming parent flow"); + } + } + } + + protected void appendToString(ToStringCreator creator) { + creator.append("subflow", subflow.getId()).append("attributeMapper", attributeMapper); + super.appendToString(creator); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/TargetStateResolver.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/TargetStateResolver.java new file mode 100644 index 00000000..8be27802 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/TargetStateResolver.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.webflow.execution.RequestContext; + +/** + * A strategy for calculating the target state of a transition. This facilitates + * dynamic transition target state resolution that takes into account runtime + * contextual information. + * + * @author Keith Donald + */ +public interface TargetStateResolver { + + /** + * Resolve the target state of the transition from the source state in the + * current request context. Should never return null. + * @param transition the transition + * @param sourceState the source state of the transition, could be null + * @param context the current request context + * @return the transition's target state + */ + public State resolveTargetState(Transition transition, State sourceState, RequestContext context); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/Transition.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/Transition.java new file mode 100644 index 00000000..275caabc --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/Transition.java @@ -0,0 +1,259 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.definition.TransitionDefinition; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; + +/** + * A path from one {@link TransitionableState state} to another + * {@link State state}. + *

+ * When executed a transition takes a flow execution from its current state, + * called the source state, to another state, called the target + * state. A transition may become eligible for execution on the occurence + * of an {@link Event} from within a transitionable source state. + *

+ * When an event occurs within this transition's source + * TransitionableState the determination of the eligibility of + * this transition is made by a TransitionCriteria object called + * the matching criteria. If the matching criteria returns + * true this transition is marked eligible for execution for that + * event. + *

+ * Determination as to whether an eligible transition should be allowed to + * execute is made by a TransitionCriteria object called the + * execution criteria. If the execution criteria test fails this + * transition will roll back and reenter its source state. If the + * execution criteria test succeeds this transition will execute and take the + * flow to the transition's target state. + *

+ * The target state of this transition is typically specified at configuration + * time in a static manner. If the target state of this transition needs to be + * calculated in a dynamic fashion at runtime configure a {@link TargetStateResolver} + * that supports such calculations. + * + * @see TransitionableState + * @see TransitionCriteria + * @see TargetStateResolver + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class Transition extends AnnotatedObject implements TransitionDefinition { + + /** + * Logger, for use in subclasses. + */ + protected final Log logger = LogFactory.getLog(Transition.class); + + /** + * The criteria that determine whether or not this transition matches as + * eligible for execution when an event occurs in the source state. + */ + private TransitionCriteria matchingCriteria; + + /** + * The criteria that determine whether or not this transition, once matched, + * should complete execution or should roll back. + */ + private TransitionCriteria executionCriteria = WildcardTransitionCriteria.INSTANCE; + + /** + * The resolver responsible for calculating the target state of this + * transition. + */ + private TargetStateResolver targetStateResolver; + + /** + * Create a new transition that always matches and always executes, + * transitioning to the target state calculated by the provided + * targetStateResolver. + * @param targetStateResolver the resolver of the target state of this + * transition + * @see #setMatchingCriteria(TransitionCriteria) + * @see #setExecutionCriteria(TransitionCriteria) + */ + public Transition(TargetStateResolver targetStateResolver) { + this(WildcardTransitionCriteria.INSTANCE, targetStateResolver); + } + + /** + * Create a new transition that matches on the specified criteria, + * transitioning to the target state calculated by the provided + * targetStateResolver. + * @param matchingCriteria the criteria for matching this transition + * @param targetStateResolver the resolver of the target state of this + * transition + * @see #setExecutionCriteria(TransitionCriteria) + */ + public Transition(TransitionCriteria matchingCriteria, TargetStateResolver targetStateResolver) { + setMatchingCriteria(matchingCriteria); + setTargetStateResolver(targetStateResolver); + } + + // implementing transition definition + + public String getId() { + return matchingCriteria.toString(); + } + + public String getTargetStateId() { + return targetStateResolver.toString(); + } + + /** + * Returns the criteria that determine whether or not this transition + * matches as eligible for execution. + * @return the transition matching criteria + */ + public TransitionCriteria getMatchingCriteria() { + return matchingCriteria; + } + + /** + * Set the criteria that determine whether or not this transition matches as + * eligible for execution. + * @param matchingCriteria the transition matching criteria + */ + public void setMatchingCriteria(TransitionCriteria matchingCriteria) { + Assert.notNull(matchingCriteria, "The matching criteria is required"); + this.matchingCriteria = matchingCriteria; + } + + /** + * Returns the criteria that determine whether or not this transition, once + * matched, should complete execution or should roll back. + * @return the transition execution criteria + */ + public TransitionCriteria getExecutionCriteria() { + return executionCriteria; + } + + /** + * Set the criteria that determine whether or not this transition, once + * matched, should complete execution or should roll back. + * @param executionCriteria the transition execution criteria + */ + public void setExecutionCriteria(TransitionCriteria executionCriteria) { + Assert.notNull(executionCriteria, "The execution criteria is required"); + this.executionCriteria = executionCriteria; + } + + /** + * Returns this transition's target state resolver. + */ + public TargetStateResolver getTargetStateResolver() { + return targetStateResolver; + } + + /** + * Set this transition's target state resolver, to calculate what state to + * transition to when this transition is executed. + * @param targetStateResolver the target state resolver + */ + public void setTargetStateResolver(TargetStateResolver targetStateResolver) { + Assert.notNull(targetStateResolver, "The target state resolver is required"); + this.targetStateResolver = targetStateResolver; + } + + /** + * Checks if this transition is elligible for execution given the state of + * the provided flow execution request context. + * @param context the flow execution request context + * @return true if this transition should execute, false otherwise + */ + public boolean matches(RequestContext context) { + return matchingCriteria.test(context); + } + + /** + * Checks if this transition can complete its execution or should be rolled + * back, given the state of the flow execution request context. + * @param context the flow execution request context + * @return true if this transition can complete execution, false if it + * should roll back + */ + public boolean canExecute(RequestContext context) { + return executionCriteria.test(context); + } + + /** + * Execute this state transition. Will only be called if the + * {@link #matches(RequestContext)} method returns true for given context. + * @param context the flow execution control context + * @return a view selection containing model and view information needed to + * render the results of the transition execution + * @throws FlowExecutionException when transition execution fails + */ + public ViewSelection execute(State sourceState, RequestControlContext context) throws FlowExecutionException { + ViewSelection selectedView; + if (canExecute(context)) { + if (sourceState != null) { + if (logger.isDebugEnabled()) { + logger.debug("Executing " + this + " out of state '" + sourceState.getId() + "'"); + } + if (sourceState instanceof TransitionableState) { + // make exit call back on transitionable state + ((TransitionableState)sourceState).exit(context); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Executing " + this); + } + } + State targetState = targetStateResolver.resolveTargetState(this, sourceState, context); + context.setLastTransition(this); + // enter the target state (note: any exceptions are propagated) + selectedView = targetState.enter(context); + } + else { + if (sourceState != null && sourceState instanceof TransitionableState) { + // 'roll back' and re-enter the transitionable source state + selectedView = ((TransitionableState)sourceState).reenter(context); + } + else { + throw new IllegalStateException( + "Execution of '" + this + "' was blocked by '" + getExecutionCriteria() + + "', " + "; however, no source state is set at runtime. " + + "This is an illegal situation: check your flow definition."); + } + } + if (logger.isDebugEnabled()) { + if (context.getFlowExecutionContext().isActive()) { + logger.debug("Completed execution of " + this + ", as a result the new state is '" + + context.getCurrentState().getId() + "' in flow '" + context.getActiveFlow().getId() + "'"); + } + else { + logger.debug("Completed execution of " + this + ", as a result the flow execution has ended"); + } + } + return selectedView; + } + + public String toString() { + return new ToStringCreator(this).append("on", getMatchingCriteria()).append("to", getTargetStateResolver()) + .toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/TransitionCriteria.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/TransitionCriteria.java new file mode 100644 index 00000000..e6d809e1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/TransitionCriteria.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.webflow.execution.RequestContext; + +/** + * Strategy interface encapsulating criteria that determine whether + * or not a transition should execute given a flow execution request context. + * + * @see org.springframework.webflow.engine.Transition + * @see org.springframework.webflow.execution.RequestContext + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface TransitionCriteria { + + /** + * Check if the transition should fire based on the given flow execution + * request context. + * @param context the flow execution request context + * @return true if the transition should fire, false otherwise + */ + public boolean test(RequestContext context); + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/TransitionSet.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/TransitionSet.java new file mode 100644 index 00000000..af7270c5 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/TransitionSet.java @@ -0,0 +1,144 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import org.springframework.core.style.StylerUtils; +import org.springframework.webflow.core.collection.CollectionUtils; +import org.springframework.webflow.execution.RequestContext; + +/** + * A typed set of transitions for use internally by artifacts that can + * apply transition execution logic. + * + * @see TransitionableState#getTransitionSet() + * @see Flow#getGlobalTransitionSet() + * + * @author Keith Donald + */ +public class TransitionSet { + + /** + * The set of transitions. + */ + private List transitions = new LinkedList(); + + /** + * Add a transition to this set. + * @param transition the transition to add + * @return true if this set's contents changed as a result of the add + * operation + */ + public boolean add(Transition transition) { + if (contains(transition)) { + return false; + } + return transitions.add(transition); + } + + /** + * Add a collection of transition instances to this set. + * @param transitions the transitions to add + * @return true if this set's contents changed as a result of the add + * operation + */ + public boolean addAll(Transition[] transitions) { + return CollectionUtils.addAllNoDuplicates(this.transitions, transitions); + } + + /** + * Tests if this transition is in this set. + * @param transition the transition + * @return true if the transition is contained in this set, false otherwise + */ + public boolean contains(Transition transition) { + return transitions.contains(transition); + } + + /** + * Remove the transition instance from this set. + * @param transition the transition to remove + * @return true if this list's contents changed as a result of the remove + * operation + */ + public boolean remove(Transition transition) { + return transitions.remove(transition); + } + + /** + * Returns the size of this transition set. + * @return the exception handler set size + */ + public int size() { + return transitions.size(); + } + + /** + * Convert this set to a typed transition array. + * @return the transition set as a typed array + */ + public Transition[] toArray() { + return (Transition[])transitions.toArray(new Transition[transitions.size()]); + } + + /** + * Returns a list of the supported transitional criteria used to match + * transitions in this state. + * @return the list of transitional criteria + */ + public TransitionCriteria[] getTransitionCriterias() { + TransitionCriteria[] criterias = new TransitionCriteria[transitions.size()]; + int i = 0; + Iterator it = transitions.iterator(); + while (it.hasNext()) { + criterias[i++] = ((Transition)it.next()).getMatchingCriteria(); + } + return criterias; + } + + /** + * Gets a transition for given flow execution request context. The first + * matching transition will be returned. + * @param context a flow execution context + * @return the transition, or null if no transition matches + */ + public Transition getTransition(RequestContext context) { + Iterator it = transitions.iterator(); + while (it.hasNext()) { + Transition transition = (Transition)it.next(); + if (transition.matches(context)) { + return transition; + } + } + return null; + } + + /** + * Returns whether or not this list has a transition that will fire for + * given flow execution request context. + * @param context a flow execution context + */ + public boolean hasMatchingTransition(RequestContext context) { + return getTransition(context) != null; + } + + public String toString() { + return StylerUtils.style(transitions); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/TransitionableState.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/TransitionableState.java new file mode 100644 index 00000000..485094bd --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/TransitionableState.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.core.style.StylerUtils; +import org.springframework.core.style.ToStringCreator; +import org.springframework.webflow.definition.TransitionDefinition; +import org.springframework.webflow.definition.TransitionableStateDefinition; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; + +/** + * Abstract superclass for states that can execute a transition in response to + * an event. + * + * @see org.springframework.webflow.engine.Transition + * @see org.springframework.webflow.engine.TransitionCriteria + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public abstract class TransitionableState extends State implements TransitionableStateDefinition { + + /** + * The set of possible transitions out of this state. + */ + private TransitionSet transitions = new TransitionSet(); + + /** + * An actions to execute when exiting this state. + */ + private ActionList exitActionList = new ActionList(); + + /** + * Create a new transitionable state. + * @param flow the owning flow + * @param id the state identifier (must be unique to the flow) + * @throws IllegalArgumentException when this state cannot be added to given + * flow, for instance when the id is not unique + * @see State#State(Flow, String) + * @see #getTransitionSet() + */ + protected TransitionableState(Flow flow, String id) throws IllegalArgumentException { + super(flow, id); + } + + // implementing TranstionableStateDefinition + + public TransitionDefinition[] getTransitions() { + return getTransitionSet().toArray(); + } + + /** + * Returns the set of transitions. The returned set is mutable. + */ + public TransitionSet getTransitionSet() { + return transitions; + } + + /** + * Get a transition in this state for given flow execution request context. + * Throws and exception when there is no corresponding transition. + * @throws NoMatchingTransitionException when a matching transition cannot + * be found + */ + public Transition getRequiredTransition(RequestContext context) throws NoMatchingTransitionException { + Transition transition = getTransitionSet().getTransition(context); + if (transition == null) { + throw new NoMatchingTransitionException(getFlow().getId(), getId(), context.getLastEvent(), + "No transition found on occurence of event '" + context.getLastEvent() + "' in state '" + getId() + + "' of flow '" + getFlow().getId() + "' -- valid transitional criteria are " + + StylerUtils.style(getTransitionSet().getTransitionCriterias()) + + " -- likely programmer error, check the set of TransitionCriteria for this state"); + } + return transition; + } + + /** + * Returns the list of actions executed by this state when it is exited. + * The returned list is mutable. + * @return the state exit action list + */ + public ActionList getExitActionList() { + return exitActionList; + } + + // behavioral methods + + /** + * Inform this state definition that an event was signaled in it. The + * signaled event is the last event available in given request context + * ({@link RequestContext#getLastEvent()}). + * @param context the flow execution control context + * @return the selected view + * @throws NoMatchingTransitionException when a matching transition cannot + * be found + */ + public ViewSelection onEvent(RequestControlContext context) throws NoMatchingTransitionException { + return getRequiredTransition(context).execute(this, context); + } + + /** + * Re-enter this state. This is typically called when a transition out of + * this state is selected, but transition execution rolls back and as a + * result the flow reenters the source state. + *

+ * By default, this just calls enter(). + * @param context the flow control context in an executing flow (a client + * instance of a flow) + * @return a view selection containing model and view information needed to + * render the results of the state processing + */ + public ViewSelection reenter(RequestControlContext context) { + return enter(context); + } + + /** + * Exit this state. This is typically called when a transition takes the + * flow out of this state into another state. By default just executes any + * registered exit actions. + * @param context the flow control context + */ + public void exit(RequestControlContext context) { + exitActionList.execute(context); + } + + protected void appendToString(ToStringCreator creator) { + creator.append("transitions", transitions).append("exitActionList", exitActionList); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewSelector.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewSelector.java new file mode 100644 index 00000000..3be66bd1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewSelector.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; + +/** + * Factory that produces a new, configured {@link ViewSelection} object on each + * invocation, taking into account the information in the provided flow + * execution request context. + *

+ * Note: this class is a runtime factory. Instances are used at flow execution + * time by objects like the {@link ViewState} to produce new + * {@link ViewSelection view selections}. + *

+ * This class allows for easy insertion of dynamic view selection logic, for + * instance, letting you determine the view to render or the available model + * data for rendering based on contextual information. + * + * @see org.springframework.webflow.execution.ViewSelection + * @see org.springframework.webflow.engine.ViewState + * @see org.springframework.webflow.engine.EndState + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface ViewSelector { + + /** + * Will the primary selection returned by 'makeEntrySelection' for the given + * request context be renderable in this request? + *

+ * "Renderable" view selections typically can have 'render-actions' execute + * before they are created. An example would be an ApplicationView that + * forwards to a view template like a JSP. "Non-renderable" view selections + * are things like a flow execution redirect--no render actually occurs, but + * only a redirect--rendering happens on the new redirect request. + * @param context the current request context of the executing flow + * @return true if yes, false otherwise + */ + public boolean isEntrySelectionRenderable(RequestContext context); + + /** + * Make a new "entry" view selection for the given request context. Called + * when a view-state, end-state, or other interactive state type is entered. + * @param context the current request context of the executing flow + * @return the entry view selection + */ + public ViewSelection makeEntrySelection(RequestContext context); + + /** + * Reconstitute a renderable view selection for the given request context to + * support a ViewState 'refresh' operation. + * @param context the current request context of the executing flow + * @return the view selection + */ + public ViewSelection makeRefreshSelection(RequestContext context); + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewState.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewState.java new file mode 100644 index 00000000..b3ad81cb --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/ViewState.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; + +/** + * A view state is a state that issues a response to the user, for + * example, for soliciting form input. + *

+ * To accomplish this, a ViewState makes a {@link ViewSelection}, + * which contains the necessary information to issue a suitable response. + * + * @see org.springframework.webflow.engine.ViewSelector + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class ViewState extends TransitionableState { + + /** + * The list of actions to be executed when this state is entered. + */ + private ActionList renderActionList = new ActionList(); + + /** + * The factory for the view selection to return when this state is entered. + */ + private ViewSelector viewSelector = NullViewSelector.INSTANCE; + + /** + * Create a new view state. + * @param flow the owning flow + * @param id the state identifier (must be unique to the flow) + * @throws IllegalArgumentException when this state cannot be added to given + * flow, e.g. because the id is not unique + */ + public ViewState(Flow flow, String id) throws IllegalArgumentException { + super(flow, id); + } + + /** + * Returns the strategy used to select the view to render in this view + * state. + */ + public ViewSelector getViewSelector() { + return viewSelector; + } + + /** + * Sets the strategy used to select the view to render in this view state. + */ + public void setViewSelector(ViewSelector viewSelector) { + Assert.notNull(viewSelector, "The view selector to make view selections is required"); + this.viewSelector = viewSelector; + } + + /** + * Returns the list of actions executable by this view state on entry and on + * refresh. The returned list is mutable. + * @return the state action list + */ + public ActionList getRenderActionList() { + return renderActionList; + } + + /** + * Specialization of State's doEnter template method that + * executes behavior specific to this state type in polymorphic fashion. + *

+ * Returns a view selection indicating a response to issue. The view + * selection typically contains all the data necessary to issue the + * response. + * @param context the control context for the currently executing flow, used + * by this state to manipulate the flow execution + * @return a view selection serving as a response instruction + * @throws FlowExecutionException if an exception occurs in this state + */ + protected ViewSelection doEnter(RequestControlContext context) throws FlowExecutionException { + if (viewSelector.isEntrySelectionRenderable(context)) { + // the entry selection will be rendered! + renderActionList.execute(context); + } + return viewSelector.makeEntrySelection(context); + } + + /** + * Request that the current view selection be reconstituted to support + * reissuing the response. This is an idempotent operation that may be + * safely called any number of times on a paused execution, used primarily + * to support a flow execution redirect. + * @param context the request context + * @return the view selection + * @throws FlowExecutionException if an exception occurs in this state + */ + public ViewSelection refresh(RequestContext context) throws FlowExecutionException { + renderActionList.execute(context); + return viewSelector.makeRefreshSelection(context); + } + + protected void appendToString(ToStringCreator creator) { + creator.append("viewSelector", viewSelector); + super.appendToString(creator); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/WildcardTransitionCriteria.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/WildcardTransitionCriteria.java new file mode 100644 index 00000000..6d72921c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/WildcardTransitionCriteria.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import java.io.ObjectStreamException; +import java.io.Serializable; + +import org.springframework.webflow.execution.RequestContext; + +/** + * Transition criteria that always returns true. + * + * @author Keith Donald + */ +public class WildcardTransitionCriteria implements TransitionCriteria, Serializable { + + /* + * Implementation note: not located in webflow.execution.support package to + * avoid a cyclic dependency between webflow.execution and webflow.execution.support. + */ + + /** + * Event id value ("*") that will cause the transition to match on any + * event. + */ + public static final String WILDCARD_EVENT_ID = "*"; + + /** + * Shared instance of a TransitionCriteria that always returns true. + */ + public static final WildcardTransitionCriteria INSTANCE = new WildcardTransitionCriteria(); + + /** + * Private constructor because this is a singleton. + */ + private WildcardTransitionCriteria() { + } + + public boolean test(RequestContext context) { + return true; + } + + // resolve the singleton instance + private Object readResolve() throws ObjectStreamException { + return INSTANCE; + } + + public String toString() { + return WILDCARD_EVENT_ID; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/AbstractFlowBuilder.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/AbstractFlowBuilder.java new file mode 100644 index 00000000..66636805 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/AbstractFlowBuilder.java @@ -0,0 +1,877 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.binding.mapping.Mapping; +import org.springframework.binding.mapping.MappingBuilder; +import org.springframework.binding.method.MethodSignature; +import org.springframework.core.style.ToStringCreator; +import org.springframework.webflow.action.AbstractBeanInvokingAction; +import org.springframework.webflow.action.ActionResultExposer; +import org.springframework.webflow.action.BeanInvokingActionFactory; +import org.springframework.webflow.action.EvaluateAction; +import org.springframework.webflow.action.MultiAction; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.CollectionUtils; +import org.springframework.webflow.engine.AnnotatedAction; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.FlowAttributeMapper; +import org.springframework.webflow.engine.FlowExecutionExceptionHandler; +import org.springframework.webflow.engine.State; +import org.springframework.webflow.engine.TargetStateResolver; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.engine.ViewSelector; +import org.springframework.webflow.engine.support.ActionTransitionCriteria; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.execution.support.EventFactorySupport; + +/** + * Base class for flow builders that programmatically build flows in Java + * configuration code. + *

+ * To give you an example of what a simple Java-based web flow builder + * definition might look like, the following example defines the 'dynamic' web + * flow roughly equivalent to the work flow statically implemented in Spring + * MVC's simple form controller: + * + *

+ * public class CustomerDetailFlowBuilder extends AbstractFlowBuilder {
+ * 	public void buildStates() {
+ * 		// get customer information
+ * 		addActionState("getDetails", action("customerAction"), transition(on(success()), to("displayDetails")));
+ * 
+ * 		// view customer information               
+ * 		addViewState("displayDetails", "customerDetails", transition(on(submit()), to("bindAndValidate")));
+ * 
+ * 		// bind and validate customer information updates 
+ * 		addActionState("bindAndValidate", action("customerAction"), new Transition[] {
+ * 				transition(on(error()), to("displayDetails")), transition(on(success()), to("finish")) });
+ * 
+ * 		// finish
+ * 		addEndState("finish");
+ * 	}
+ * }
+ * 
+ * + * What this Java-based FlowBuilder implementation does is add four states to a + * flow. These include a "get" ActionState (the start state), a + * ViewState state, a "bind and validate" + * ActionState, and an end marker state (EndState). + *

+ * The first state, an action state, will be assigned the indentifier + * getDetails. This action state will automatically be + * configured with the following defaults: + *

    + *
  1. The action instance with id customerAction. This is the + * Action implementation that will execute when this state is + * entered. In this example, that Action will go out to the DB, + * load the Customer, and put it in the Flow's request context. + *
  2. A success transition to a default view state, called + * displayDetails. This means when the Action + * returns a success result event (aka outcome), the + * displayDetails state will be entered. + *
  3. It will act as the start state for this flow (by default, the first + * state added to a flow during the build process is treated as the start + * state). + *
+ *

+ * The second state, a view state, will be identified as + * displayDetails. This view state will automatically be + * configured with the following defaults: + *

    + *
  1. A view name called customerDetails. This is the logical + * name of a view resource. This logical view name gets mapped to a physical + * view resource (jsp, etc.) by the calling front controller (via a Spring view + * resolver, or a Struts action forward, for example). + *
  2. A submit transition to a bind and validate action state, + * indentified by the default id bindAndValidate. This means + * when a submit event is signaled by the view (for example, on a + * submit button click), the bindAndValidate action state will be entered and + * the bindAndValidate method of the + * customerAction Action implementation will be + * executed. + *
+ *

+ * The third state, an action state, will be indentified as + * bindAndValidate. This action state will automatically be + * configured with the following defaults: + *

    + *
  1. An action bean named customerAction -- this is the name + * of the Action implementation exported in the application + * context that will execute when this state is entered. In this example, the + * Action has a "bindAndValidate" method that will bind form + * input in the HTTP request to a backing Customer form object, validate it, and + * update the DB. + *
  2. A success transition to a default end state, called + * finish. This means if the Action returns a + * success result, the finish end state will be + * transitioned to and the flow will terminate. + *
  3. An error transition back to the form view. This means if + * the Action returns an error event, the + * displayDetails view state will be transitioned back to. + *
+ *

+ * The fourth and last state, an end state, will be indentified with the default + * end state id finish. This end state is a marker that signals + * the end of the flow. When entered, the flow session terminates, and if this + * flow is acting as a root flow in the current flow execution, any + * flow-allocated resources will be cleaned up. An end state can optionally be + * configured with a logical view name to forward to when entered. It will also + * trigger a state transition in a resuming parent flow if this flow was + * participating as a spawned 'subflow' within a suspended parent flow. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public abstract class AbstractFlowBuilder extends BaseFlowBuilder { + + /** + * A helper for creating commonly used event identifiers that drive + * transitions created by this builder. + */ + private EventFactorySupport eventFactorySupport = new EventFactorySupport(); + + /** + * Default constructor for subclassing. + */ + protected AbstractFlowBuilder() { + super(); + } + + /** + * Create an instance of an abstract flow builder, using the specified + * locator to obtain needed flow services at build time. + * @param flowServiceLocator the locator for services needed by this builder + * to build its Flow + */ + protected AbstractFlowBuilder(FlowServiceLocator flowServiceLocator) { + super(flowServiceLocator); + } + + /** + * Returns the configured event factory support helper for creating commonly + * used event identifiers that drive transitions created by this builder. + */ + public EventFactorySupport getEventFactorySupport() { + return eventFactorySupport; + } + + /** + * Sets the event factory support helper to use to create commonly used + * event identifiers that drive transitions created by this builder. + */ + public void setEventFactorySupport(EventFactorySupport eventFactorySupport) { + this.eventFactorySupport = eventFactorySupport; + } + + public void init(String flowId, AttributeMap attributes) throws FlowBuilderException { + setFlow(getFlowArtifactFactory().createFlow(flowId, flowAttributes().union(attributes))); + } + + /** + * Hook subclasses may override to provide additional properties for the + * flow built by this builder. Returns a empty collection by default. + * @return additional properties describing the flow being built, should not + * return null + */ + protected AttributeMap flowAttributes() { + return CollectionUtils.EMPTY_ATTRIBUTE_MAP; + } + + // view state + + /** + * Adds a view state to the flow built by this builder. + * @param stateId the state identifier + * @param viewName the string-encoded view selector + * @param transition the sole transition (path) out of this state + * @return the fully constructed view state instance + */ + protected State addViewState(String stateId, String viewName, Transition transition) { + return getFlowArtifactFactory().createViewState(stateId, getFlow(), null, viewSelector(viewName), null, + new Transition[] { transition }, null, null, null); + } + + /** + * Adds a view state to the flow built by this builder. + * @param stateId the state identifier + * @param viewName the string-encoded view selector + * @param transitions the transitions (paths) out of this state + * @return the fully constructed view state instance + */ + protected State addViewState(String stateId, String viewName, Transition[] transitions) { + return getFlowArtifactFactory().createViewState(stateId, getFlow(), null, viewSelector(viewName), null, + transitions, null, null, null); + } + + /** + * Adds a view state to the flow built by this builder. + * @param stateId the state identifier + * @param viewName the string-encoded view selector + * @param renderAction the action to execute on state entry and refresh; may + * be null + * @param transition the sole transition (path) out of this state + * @return the fully constructed view state instance + */ + protected State addViewState(String stateId, String viewName, Action renderAction, Transition transition) { + return getFlowArtifactFactory().createViewState(stateId, getFlow(), null, viewSelector(viewName), + new Action[] { renderAction }, new Transition[] { transition }, null, null, null); + } + + /** + * Adds a view state to the flow built by this builder. + * @param stateId the state identifier + * @param viewName the string-encoded view selector + * @param renderAction the action to execute on state entry and refresh; may + * be null + * @param transitions the transitions (paths) out of this state + * @return the fully constructed view state instance + */ + protected State addViewState(String stateId, String viewName, Action renderAction, Transition[] transitions) { + return getFlowArtifactFactory().createViewState(stateId, getFlow(), null, viewSelector(viewName), + new Action[] { renderAction }, transitions, null, null, null); + } + + /** + * Adds a view state to the flow built by this builder. + * @param stateId the state identifier + * @param entryActions the actions to execute when the state is entered + * @param viewSelector the view selector that will make the view selection + * when the state is entered + * @param renderActions any 'render actions' to execute on state entry and + * refresh; may be null + * @param transitions the transitions (path) out of this state + * @param exceptionHandlers any exception handlers to attach to the state + * @param exitActions the actions to execute when the state exits + * @param attributes attributes to assign to the state that may be used to + * affect state construction and execution + * @return the fully constructed view state instance + */ + protected State addViewState(String stateId, Action[] entryActions, ViewSelector viewSelector, + Action[] renderActions, Transition[] transitions, FlowExecutionExceptionHandler[] exceptionHandlers, + Action[] exitActions, AttributeMap attributes) { + return getFlowArtifactFactory().createViewState(stateId, getFlow(), entryActions, viewSelector, renderActions, + transitions, exceptionHandlers, exitActions, attributes); + } + + // action state + + /** + * Adds an action state to the flow built by this builder. + * @param stateId the state identifier + * @param action the single action to execute when the state is entered + * @param transition the single transition (path) out of this state + * @return the fully constructed action state instance + */ + protected State addActionState(String stateId, Action action, Transition transition) { + return getFlowArtifactFactory().createActionState(stateId, getFlow(), null, new Action[] { action }, + new Transition[] { transition }, null, null, null); + } + + /** + * Adds an action state to the flow built by this builder. + * @param stateId the state identifier + * @param action the single action to execute when the state is entered + * @param transitions the transitions (paths) out of this state + * @return the fully constructed action state instance + */ + protected State addActionState(String stateId, Action action, Transition[] transitions) { + return getFlowArtifactFactory().createActionState(stateId, getFlow(), null, new Action[] { action }, + transitions, null, null, null); + } + + /** + * Adds an action state to the flow built by this builder. + * @param stateId the state identifier + * @param action the single action to execute when the state is entered + * @param transition the single transition (path) out of this state + * @param exceptionHandler the exception handler to handle exceptions thrown + * by the action + * @return the fully constructed action state instance + */ + protected State addActionState(String stateId, Action action, Transition transition, + FlowExecutionExceptionHandler exceptionHandler) { + return getFlowArtifactFactory().createActionState(stateId, getFlow(), null, new Action[] { action }, + new Transition[] { transition }, new FlowExecutionExceptionHandler[] { exceptionHandler }, null, null); + } + + /** + * Adds an action state to the flow built by this builder. + * @param stateId the state identifier + * @param entryActions any generic entry actions to add to the state + * @param actions the actions to execute in a chain when the state is + * entered + * @param transitions the transitions (paths) out of this state + * @param exceptionHandlers the exception handlers to handle exceptions + * thrown by the actions + * @param exitActions the exit actions to execute when the state exits + * @param attributes attributes to assign to the state that may be used to + * affect state construction and execution + * @return the fully constructed action state instance + */ + protected State addActionState(String stateId, Action[] entryActions, Action[] actions, Transition[] transitions, + FlowExecutionExceptionHandler[] exceptionHandlers, Action[] exitActions, AttributeMap attributes) { + return getFlowArtifactFactory().createActionState(stateId, getFlow(), entryActions, actions, transitions, + exceptionHandlers, exitActions, attributes); + } + + // decision state + + /** + * Adds a decision state to the flow built by this builder. + * @param stateId the state identifier + * @param transitions the transitions (paths) out of this state + * @return the fully constructed decision state instance + */ + protected State addDecisionState(String stateId, Transition[] transitions) { + return getFlowArtifactFactory().createDecisionState(stateId, getFlow(), null, transitions, null, null, null); + } + + /** + * Adds a decision state to the flow built by this builder. + * @param stateId the state identifier + * @param decisionCriteria the criteria that defines the decision + * @param trueStateId the target state on a "true" decision + * @param falseStateId the target state on a "false" decision + * @return the fully constructed decision state instance + */ + protected State addDecisionState(String stateId, TransitionCriteria decisionCriteria, String trueStateId, + String falseStateId) { + Transition thenTransition = getFlowArtifactFactory() + .createTransition(to(trueStateId), decisionCriteria, null, null); + Transition elseTransition = getFlowArtifactFactory().createTransition(to(falseStateId), null, null, null); + return getFlowArtifactFactory().createDecisionState(stateId, getFlow(), null, + new Transition[] { thenTransition, elseTransition }, null, null, null); + } + + /** + * Adds a decision state to the flow built by this builder. + * @param stateId the state identifier + * @param entryActions the entry actions to execute when the state enters + * @param transitions the transitions (paths) out of this state + * @param exceptionHandlers the exception handlers to handle exceptions + * thrown by the state + * @param exitActions the exit actions to execute when the state exits + * @param attributes attributes to assign to the state that may be used to + * affect state construction and execution + * @return the fully constructed decision state instance + */ + protected State addDecisionState(String stateId, Action[] entryActions, Transition[] transitions, + FlowExecutionExceptionHandler[] exceptionHandlers, Action[] exitActions, AttributeMap attributes) { + return getFlowArtifactFactory().createDecisionState(stateId, getFlow(), entryActions, transitions, + exceptionHandlers, exitActions, attributes); + } + + // subflow state + + /** + * Adds a subflow state to the flow built by this builder. + * @param stateId the state identifier + * @param subflow the flow that will act as the subflow + * @param attributeMapper the mapper to map subflow input and output + * attributes + * @param transition the single transition (path) out of the state + * @return the fully constructed subflow state instance + */ + protected State addSubflowState(String stateId, Flow subflow, FlowAttributeMapper attributeMapper, + Transition transition) { + return getFlowArtifactFactory().createSubflowState(stateId, getFlow(), null, subflow, attributeMapper, + new Transition[] { transition }, null, null, null); + } + + /** + * Adds a subflow state to the flow built by this builder. + * @param stateId the state identifier + * @param subflow the flow that will act as the subflow + * @param attributeMapper the mapper to map subflow input and output + * attributes + * @param transitions the transitions (paths) out of the state + * @return the fully constructed subflow state instance + */ + protected State addSubflowState(String stateId, Flow subflow, FlowAttributeMapper attributeMapper, + Transition[] transitions) { + return getFlowArtifactFactory().createSubflowState(stateId, getFlow(), null, subflow, attributeMapper, + transitions, null, null, null); + } + + /** + * Adds a subflow state to the flow built by this builder. + * @param stateId the state identifier + * @param entryActions the entry actions to execute when the state enters + * @param subflow the flow that will act as the subflow + * @param attributeMapper the mapper to map subflow input and output + * attributes + * @param transitions the transitions (paths) out of this state + * @param exceptionHandlers the exception handlers to handle exceptions + * thrown by the state + * @param exitActions the exit actions to execute when the state exits + * @param attributes attributes to assign to the state that may be used to + * affect state construction and execution + * @return the fully constructed subflow state instance + */ + protected State addSubflowState(String stateId, Action[] entryActions, Flow subflow, + FlowAttributeMapper attributeMapper, Transition[] transitions, + FlowExecutionExceptionHandler[] exceptionHandlers, Action[] exitActions, AttributeMap attributes) { + return getFlowArtifactFactory().createSubflowState(stateId, getFlow(), entryActions, subflow, attributeMapper, + transitions, exceptionHandlers, exitActions, attributes); + } + + // end state + + /** + * Adds an end state to the flow built by this builder. + * @param stateId the state identifier + * @return the fully constructed end state instance + */ + protected State addEndState(String stateId) { + return getFlowArtifactFactory().createEndState(stateId, getFlow(), null, null, null, null, null); + } + + /** + * Adds an end state to the flow built by this builder. + * @param stateId the state identifier + * @param viewName the string-encoded view selector + * @return the fully constructed end state instance + */ + protected State addEndState(String stateId, String viewName) { + return getFlowArtifactFactory().createEndState(stateId, getFlow(), null, viewSelector(viewName), null, + null, null); + } + + /** + * Adds an end state to the flow built by this builder. + * @param stateId the state identifier + * @param viewName the string-encoded view selector + * @param outputMapper the output mapper to map output attributes for the + * end state (a flow outcome) + * @return the fully constructed end state instance + */ + protected State addEndState(String stateId, String viewName, AttributeMapper outputMapper) { + return getFlowArtifactFactory().createEndState(stateId, getFlow(), null, viewSelector(viewName), + outputMapper, null, null); + } + + /** + * Adds an end state to the flow built by this builder. + * @param stateId the state identifier + * @param entryActions the actions to execute when the state is entered + * @param viewSelector the view selector that will make the view selection + * when the state is entered + * @param outputMapper the output mapper to map output attributes for the + * end state (a flow outcome) + * @param exceptionHandlers any exception handlers to attach to the state + * @param attributes attributes to assign to the state that may be used to + * affect state construction and execution + * @return the fully constructed end state instance + */ + protected State addEndState(String stateId, Action[] entryActions, ViewSelector viewSelector, + AttributeMapper outputMapper, FlowExecutionExceptionHandler[] exceptionHandlers, AttributeMap attributes) { + return getFlowArtifactFactory().createEndState(stateId, getFlow(), entryActions, viewSelector, outputMapper, + exceptionHandlers, attributes); + } + + // helpers to create misc. flow artifacts + + /** + * Factory method that creates a view selector from an encoded + * view name. See {@link TextToViewSelector} for information on the + * conversion rules. + * @param viewName the encoded view selector + * @return the view selector + */ + public ViewSelector viewSelector(String viewName) { + return (ViewSelector)fromStringTo(ViewSelector.class).execute(viewName); + } + + /** + * Resolves the action with the specified id. Simply looks the action up by + * id and returns it. + * @param id the action id + * @return the action + * @throws FlowArtifactLookupException the action could not be resolved + */ + protected Action action(String id) throws FlowArtifactLookupException { + return getFlowServiceLocator().getAction(id); + } + + /** + * Creates a bean invoking action that invokes the method identified by the + * signature on the bean associated with the action identifier. + * @param beanId the id identifying an arbitrary + * java.lang.Object to be used as an action + * @param methodSignature the signature of the method to invoke on the POJO + * @return the adapted bean invoking action + * @throws FlowArtifactLookupException the action could not be resolved + */ + protected Action action(String beanId, MethodSignature methodSignature) throws FlowArtifactLookupException { + return getBeanInvokingActionFactory().createBeanInvokingAction(beanId, + getFlowServiceLocator().getBeanFactory(), methodSignature, null, + getFlowServiceLocator().getConversionService(), null); + } + + /** + * Creates a bean invoking action that invokes the method identified by the + * signature on the bean associated with the action identifier. + * @param beanId the id identifying an arbitrary + * java.lang.Object to be used as an action + * @param methodSignature the signature of the method to invoke on the POJO + * @return the adapted bean invoking action + * @throws FlowArtifactLookupException the action could not be resolved + */ + protected Action action(String beanId, MethodSignature methodSignature, ActionResultExposer resultExposer) + throws FlowArtifactLookupException { + return getBeanInvokingActionFactory().createBeanInvokingAction(beanId, + getFlowServiceLocator().getBeanFactory(), methodSignature, resultExposer, + getFlowServiceLocator().getConversionService(), null); + } + + /** + * Creates an evaluate action that evaluates the expression when executed. + * @param expression the expression to evaluate + */ + protected Action action(Expression expression) { + return action(expression, null); + } + + /** + * Creates an evaluate action that evaluates the expression when executed. + * @param expression the expression to evaluate + * @param resultExposer the evaluation result exposer + */ + protected Action action(Expression expression, ActionResultExposer resultExposer) { + return new EvaluateAction(expression, resultExposer); + } + + /** + * Parses the expression string into a evaluatable {@link Expression} + * object. + * @param expressionString the expression string, e.g. flowScope.order.number + * @return the evaluatable expression + */ + protected Expression expression(String expressionString) { + return getFlowServiceLocator().getExpressionParser().parseExpression(expressionString); + } + + /** + * Convert the encoded method signature string to a {@link MethodSignature} + * object. Method signatures are used to match methods on POJO services to + * invoke on a {@link AbstractBeanInvokingAction bean invoking action}. + *

+ * Encoded method signature format: + * + * Method without arguments: + *

+	 *       ${methodName}
+	 * 
+ * + * Method with arguments: + *
+	 *       ${methodName}(${arg1}, ${arg2}, ${arg n})
+	 * 
+ * + * @param method the encoded method signature + * @return the method signature + * @see #action(String, MethodSignature, ActionResultExposer) + */ + protected MethodSignature method(String method) { + return (MethodSignature)fromStringTo(MethodSignature.class).execute(method); + } + + /** + * Factory method for a {@link ActionResultExposer result exposer}. A + * result exposer is used to expose an action result such as a method return + * value or expression evaluation result to the calling flow. + * @param resultName the result name + * @return the result exposer + * @see #action(String, MethodSignature, ActionResultExposer) + */ + protected ActionResultExposer result(String resultName) { + return result(resultName, ScopeType.REQUEST); + } + + /** + * Factory method for a {@link ActionResultExposer result exposer}. A + * result exposer is used to expose an action result such as a method return + * value or expression evaluation result to the calling flow. + * @param resultName the result name + * @param resultScope the scope of the result + * @return the result exposer + * @see #action(String, MethodSignature, ActionResultExposer) + */ + protected ActionResultExposer result(String resultName, ScopeType resultScope) { + return new ActionResultExposer(resultName, resultScope); + } + + /** + * Creates an annotated action decorator that instructs the specified method + * be invoked on the multi action when it is executed. Use this when working + * with MultiActions to specify the method on the MultiAction to invoke for + * a particular usage scenario. Use the {@link #method(String)} factory + * method when working with + * {@link AbstractBeanInvokingAction bean invoking actions}. + * @param methodName the name of the method on the multi action instance + * @param multiAction the multi action + * @return the annotated action that when invoked sets up a context property + * used by the multi action to instruct it with what method to invoke + */ + protected AnnotatedAction invoke(String methodName, MultiAction multiAction) throws FlowArtifactLookupException { + AnnotatedAction action = new AnnotatedAction(multiAction); + action.setMethod(methodName); + return action; + } + + /** + * Request that the attribute mapper with the specified name be used to map + * attributes between a parent flow and a spawning subflow when the subflow + * state being constructed is entered. + * @param id the id of the attribute mapper that will map attributes between + * the flow built by this builder and the subflow + * @return the attribute mapper + * @throws FlowArtifactLookupException no FlowAttributeMapper implementation + * was exported with the specified id + */ + protected FlowAttributeMapper attributeMapper(String id) throws FlowArtifactLookupException { + return getFlowServiceLocator().getAttributeMapper(id); + } + + /** + * Request that the Flow with the specified flowId be spawned + * as a subflow when the subflow state being built is entered. Simply + * resolves the subflow definition by id and returns it; throwing a + * fail-fast exception if it does not exist. + * @param id the flow definition id + * @return the flow to be used as a subflow, this should be passed to a + * addSubflowState call + * @throws FlowArtifactLookupException when the flow cannot be resolved + */ + protected Flow flow(String id) throws FlowArtifactLookupException { + return getFlowServiceLocator().getSubflow(id); + } + + /** + * Creates a transition criteria that is used to match a Transition. The + * criteria is based on the provided expression string. + * @param transitionCriteriaExpression the transition criteria expression, + * typically simply a static event identifier (e.g. "submit") + * @return the transition criteria + */ + protected TransitionCriteria on(String transitionCriteriaExpression) { + return (TransitionCriteria)fromStringTo(TransitionCriteria.class).execute(transitionCriteriaExpression); + } + + /** + * Creates a target state resolver for the given state id expression. + * @param targetStateIdExpression the target state id expression + * @return the target state resolver + */ + protected TargetStateResolver to(String targetStateIdExpression) { + return (TargetStateResolver)fromStringTo(TargetStateResolver.class).execute(targetStateIdExpression); + } + + /** + * Creates a new transition. + * @param matchingCriteria the criteria that determines when the transition + * matches + * @param targetStateResolver the resolver of the transition's target state + * @return the transition + */ + protected Transition transition(TransitionCriteria matchingCriteria, TargetStateResolver targetStateResolver) { + return getFlowArtifactFactory().createTransition(targetStateResolver, matchingCriteria, null, null); + } + + /** + * Creates a new transition. + * @param matchingCriteria the criteria that determines when the transition + * matches + * @param targetStateResolver the resolver of the transition's target state + * @param executionCriteria the criteria that determines if a matched + * transition is allowed to execute + * @return the transition + */ + protected Transition transition(TransitionCriteria matchingCriteria, TargetStateResolver targetStateResolver, + TransitionCriteria executionCriteria) { + return getFlowArtifactFactory().createTransition(targetStateResolver, matchingCriteria, executionCriteria, null); + } + + /** + * Creates a new transition. + * @param matchingCriteria the criteria that determines when the transition + * matches + * @param targetStateResolver the resolver of the transition's target state + * @param executionCriteria the criteria that determines if a matched + * transition is allowed to execute + * @param attributes transition attributes + * @return the transition + */ + protected Transition transition(TransitionCriteria matchingCriteria, TargetStateResolver targetStateResolver, + TransitionCriteria executionCriteria, AttributeMap attributes) { + return getFlowArtifactFactory() + .createTransition(targetStateResolver, matchingCriteria, executionCriteria, attributes); + } + + /** + * Creates a TransitionCriteria that will execute the + * specified action when the Transition is executed but before the + * transition's target state is entered. + *

+ * This criteria will only allow the Transition to complete execution if the + * Action completes successfully. + * @param action the action to execute after a transition is matched but + * before it transitions to its target state + * @return the transition execution criteria + */ + protected TransitionCriteria ifReturnedSuccess(Action action) { + return new ActionTransitionCriteria(action); + } + + /** + * Creates the success event id. "Success" indicates that an + * action completed successfuly. + * @return the event id + */ + protected String success() { + return eventFactorySupport.getSuccessEventId(); + } + + /** + * Creates the error event id. "Error" indicates that an + * action completed with an error status. + * @return the event id + */ + protected String error() { + return eventFactorySupport.getErrorEventId(); + } + + /** + * Creates the submit event id. "Submit" indicates the user + * submitted a request (form) for processing. + * @return the event id + */ + protected String submit() { + return "submit"; + } + + /** + * Creates the back event id. "Back" indicates the user wants + * to go to the previous step in the flow. + * @return the event id + */ + protected String back() { + return "back"; + } + + /** + * Creates the cancel event id. "Cancel" indicates the flow + * was aborted because the user changed their mind. + * @return the event id + */ + protected String cancel() { + return "cancel"; + } + + /** + * Creates the finish event id. "Finish" indicates the flow + * has finished processing. + * @return the event id + */ + protected String finish() { + return "finish"; + } + + /** + * Creates the select event id. "Select" indicates an object + * was selected for processing or display. + * @return the event id + */ + protected String select() { + return "select"; + } + + /** + * Creates the edit event id. "Edit" indicates an object was + * selected for creation or updating. + * @return the event id + */ + protected String edit() { + return "edit"; + } + + /** + * Creates the add event id. "Add" indicates a child object + * is being added to a parent collection. + * @return the event id + */ + protected String add() { + return "add"; + } + + /** + * Creates the delete event id. "Delete" indicates a object + * is being removed. + * @return the event id + */ + protected String delete() { + return "delete"; + } + + /** + * Creates the yes event id. "Yes" indicates a true result + * was returned. + * @return the event id + */ + protected String yes() { + return eventFactorySupport.getYesEventId(); + } + + /** + * Creates the no event id. "False" indicates a false result + * was returned. + * @return the event id + */ + protected String no() { + return eventFactorySupport.getNoEventId(); + } + + /** + * Factory method that returns a new, fully configured mapping builder to + * assist with building {@link Mapping} objects used by a + * {@link FlowAttributeMapper} to map attributes. + * @return the mapping builder + */ + protected MappingBuilder mapping() { + MappingBuilder mapping = new MappingBuilder(getFlowServiceLocator().getExpressionParser()); + mapping.setConversionService(getFlowServiceLocator().getConversionService()); + return mapping; + } + + public String toString() { + return new ToStringCreator(this).toString(); + } + + // internal helpers + + private FlowArtifactFactory getFlowArtifactFactory() { + return getFlowServiceLocator().getFlowArtifactFactory(); + } + + private BeanInvokingActionFactory getBeanInvokingActionFactory() { + return getFlowServiceLocator().getBeanInvokingActionFactory(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/AbstractFlowBuildingFlowRegistryFactoryBean.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/AbstractFlowBuildingFlowRegistryFactoryBean.java new file mode 100644 index 00000000..a50c42b7 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/AbstractFlowBuildingFlowRegistryFactoryBean.java @@ -0,0 +1,234 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.context.ResourceLoaderAware; +import org.springframework.core.io.ResourceLoader; +import org.springframework.webflow.action.BeanInvokingActionFactory; +import org.springframework.webflow.definition.registry.AbstractFlowDefinitionRegistryFactoryBean; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.State; +import org.springframework.webflow.execution.Action; + +/** + * A base class for factory beans that create populated registries of flow + * definitions built using a {@link FlowBuilder}, typically a {@link BaseFlowBuilder} + * subclass. This base class will setup a {@link FlowServiceLocator} for + * use by the flow builder. + *

+ * Subclasses should override the {@link #doPopulate(FlowDefinitionRegistry)} + * template method to perform the registry population logic, typically delegating to a + * {@link org.springframework.webflow.definition.registry.FlowDefinitionRegistrar} + * strategy. + * + * @see org.springframework.webflow.definition.registry.FlowDefinitionRegistry + * @see org.springframework.webflow.definition.registry.FlowDefinitionRegistrar + * + * @author Keith Donald + */ +public abstract class AbstractFlowBuildingFlowRegistryFactoryBean extends AbstractFlowDefinitionRegistryFactoryBean + implements BeanFactoryAware, ResourceLoaderAware { + + /** + * The locator of services needed by the flows built for inclusion in the + * registry. + */ + private FlowServiceLocator flowServiceLocator; + + /** + * The factory encapsulating the creation of central Flow artifacts such as + * {@link Flow flows} and {@link State states}. + */ + private FlowArtifactFactory flowArtifactFactory; + + /** + * The factory encapsulating the creation of bean invoking actions, actions + * that adapt methods on objects to the {@link Action} interface. + */ + private BeanInvokingActionFactory beanInvokingActionFactory; + + /** + * The parser for parsing expression strings into evaluatable expression + * objects. + */ + private ExpressionParser expressionParser; + + /** + * A conversion service that can convert between types. + */ + private ConversionService conversionService; + + /** + * A resource loader that can load resources. + */ + private ResourceLoader resourceLoader; + + /** + * The Spring bean factory that manages configured flow artifacts. + */ + private BeanFactory beanFactory; + + /** + * Returns the factory encapsulating the creation of central Flow artifacts + * such as {@link Flow flows} and {@link State states}. + */ + protected FlowArtifactFactory getFlowArtifactFactory() { + return flowArtifactFactory; + } + + /** + * Sets the factory encapsulating the creation of central Flow artifacts + * such as {@link Flow flows} and {@link State states}. + */ + public void setFlowArtifactFactory(FlowArtifactFactory flowArtifactFactory) { + this.flowArtifactFactory = flowArtifactFactory; + } + + /** + * Returns the factory for creating bean invoking actions, actions that adapt + * methods on objects to the {@link Action} interface. + */ + protected BeanInvokingActionFactory getBeanInvokingActionFactory() { + return beanInvokingActionFactory; + } + + /** + * Sets the factory for creating bean invoking actions, actions that adapt + * methods on objects to the {@link Action} interface. + */ + public void setBeanInvokingActionFactory(BeanInvokingActionFactory beanInvokingActionFactory) { + this.beanInvokingActionFactory = beanInvokingActionFactory; + } + + /** + * Returns the expression parser responsible for parsing expression strings into + * evaluatable expression objects. + */ + protected ExpressionParser getExpressionParser() { + return expressionParser; + } + + /** + * Set the expression parser responsible for parsing expression strings into + * evaluatable expression objects. + */ + public void setExpressionParser(ExpressionParser expressionParser) { + this.expressionParser = expressionParser; + } + + /** + * Returns the conversion service to use to convert between types; typically + * from string to a rich object type. + */ + protected ConversionService getConversionService() { + return conversionService; + } + + /** + * Set the conversion service to use to convert between types; typically + * from string to a rich object type. + */ + public void setConversionService(ConversionService conversionService) { + this.conversionService = conversionService; + } + + // implementing ResourceLoaderAware + + /** + * Returns the injected resource loader. + */ + protected ResourceLoader getResourceLoader() { + return resourceLoader; + } + + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + // implementing BeanFactoryAware + + /** + * Returns the bean factory managing this bean. + */ + protected BeanFactory getBeanFactory() { + return beanFactory; + } + + public void setBeanFactory(BeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + protected final void init() { + flowServiceLocator = createFlowServiceLocator(); + init(flowServiceLocator); + } + + // subclassing hooks + + /** + * Factory method for creating the service locator used to locate webflow + * services during flow assembly. Subclasses may override to customize the + * instantiation and configuration of the locator returned. + * @return the service locator + */ + protected FlowServiceLocator createFlowServiceLocator() { + DefaultFlowServiceLocator serviceLocator = new DefaultFlowServiceLocator(getRegistry(), beanFactory); + if (flowArtifactFactory != null) { + serviceLocator.setFlowArtifactFactory(flowArtifactFactory); + } + if (beanInvokingActionFactory != null) { + serviceLocator.setBeanInvokingActionFactory(beanInvokingActionFactory); + } + if (expressionParser != null) { + serviceLocator.setExpressionParser(expressionParser); + } + if (conversionService != null) { + serviceLocator.setConversionService(conversionService); + } + if (resourceLoader != null) { + serviceLocator.setResourceLoader(resourceLoader); + } + return serviceLocator; + } + + /** + * Called after properties have been set on the service locator, but before + * registry population. Subclasses may override to perform custom initialization + * of the flow service locator. + * @param flowServiceLocator the flow service locator to use to locate externally managed + * services needed during flow building and assembly, typically used by a + * {@link org.springframework.webflow.definition.registry.FlowDefinitionRegistrar} + */ + protected void init(FlowServiceLocator flowServiceLocator) { + } + + /** + * Returns the strategy for locating dependent artifacts when a flow is + * being built. May be called by subclasses during + * {@link #doPopulate(FlowDefinitionRegistry) registry population} to wire + * in the service locator needed for flow assembly. + */ + protected FlowServiceLocator getFlowServiceLocator() { + return flowServiceLocator; + } + + protected abstract void doPopulate(FlowDefinitionRegistry registry); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/BaseFlowBuilder.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/BaseFlowBuilder.java new file mode 100644 index 00000000..b2802a72 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/BaseFlowBuilder.java @@ -0,0 +1,154 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.binding.convert.ConversionException; +import org.springframework.binding.convert.ConversionExecutor; +import org.springframework.util.Assert; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.engine.Flow; + +/** + * Abstract base implementation of a flow builder defining common functionality + * needed by most concrete flow builder implementations. This class implements + * all optional parts of the FlowBuilder process as no-op methods. Subclasses + * are only required to implement {@link #init(String, AttributeMap)} and + * {@link #buildStates()}. + *

+ * This class also provides a {@link FlowServiceLocator} for use by + * subclasses in the flow construction process. + * + * @see org.springframework.webflow.engine.builder.FlowServiceLocator + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public abstract class BaseFlowBuilder implements FlowBuilder { + + /** + * The Flow built by this builder. + */ + private Flow flow; + + /** + * Locates actions, attribute mappers, and other artifacts needed by the + * flow built by this builder. + */ + private FlowServiceLocator flowServiceLocator; + + /** + * Default constructor for subclassing. Sets up use of a {@link BaseFlowServiceLocator}. + * @see #setFlowServiceLocator(FlowServiceLocator) + */ + protected BaseFlowBuilder() { + setFlowServiceLocator(new BaseFlowServiceLocator()); + } + + /** + * Creates a flow builder using the given locator to link in artifacts. + * @param flowServiceLocator the locator for services needed by this builder to build its Flow + */ + protected BaseFlowBuilder(FlowServiceLocator flowServiceLocator) { + setFlowServiceLocator(flowServiceLocator); + } + + /** + * Returns the configured flow service locator. + */ + public FlowServiceLocator getFlowServiceLocator() { + return flowServiceLocator; + } + + /** + * Sets the flow service locator to use. Defaults to {@link BaseFlowServiceLocator}. + */ + public void setFlowServiceLocator(FlowServiceLocator flowServiceLocator) { + Assert.notNull(flowServiceLocator, "The flow service locator is required"); + this.flowServiceLocator = flowServiceLocator; + } + + /** + * Set the flow being built by this builder. Typically called during + * initialization to set the initial flow reference returned by + * {@link #getFlow()} after building. + */ + protected void setFlow(Flow flow) { + this.flow = flow; + } + + public abstract void init(String flowId, AttributeMap attributes) throws FlowBuilderException; + + public void buildVariables() throws FlowBuilderException { + } + + public void buildInputMapper() throws FlowBuilderException { + } + + public void buildStartActions() throws FlowBuilderException { + } + + public void buildInlineFlows() throws FlowBuilderException { + } + + public abstract void buildStates() throws FlowBuilderException; + + public void buildGlobalTransitions() throws FlowBuilderException { + } + + public void buildEndActions() throws FlowBuilderException { + } + + public void buildOutputMapper() throws FlowBuilderException { + } + + public void buildExceptionHandlers() throws FlowBuilderException { + } + + /** + * Get the flow (result) built by this builder. + */ + public Flow getFlow() { + return flow; + } + + public void dispose() { + setFlow(null); + } + + // helpers for use in subclasses + + /** + * Returns a conversion executor capable of converting string objects to the + * target class aliased by the provided alias. + * @param targetAlias the target class alias, e.g. "long" or "float" + * @return the conversion executor, or null if no suitable + * converter exists for given alias + */ + protected ConversionExecutor fromStringTo(String targetAlias) { + return getFlowServiceLocator().getConversionService().getConversionExecutorByTargetAlias(String.class, targetAlias); + } + + /** + * Returns a converter capable of converting a string value to the given + * type. + * @param targetType the type you wish to convert to (from a string) + * @return the converter + * @throws ConversionException when the converter cannot be found + */ + protected ConversionExecutor fromStringTo(Class targetType) throws ConversionException { + return getFlowServiceLocator().getConversionService().getConversionExecutor(String.class, targetType); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/BaseFlowServiceLocator.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/BaseFlowServiceLocator.java new file mode 100644 index 00000000..6e63ece9 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/BaseFlowServiceLocator.java @@ -0,0 +1,249 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.convert.support.CompositeConversionService; +import org.springframework.binding.convert.support.DefaultConversionService; +import org.springframework.binding.convert.support.GenericConversionService; +import org.springframework.binding.convert.support.TextToExpression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.binding.method.TextToMethodSignature; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.Assert; +import org.springframework.webflow.action.BeanInvokingActionFactory; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.FlowAttributeMapper; +import org.springframework.webflow.engine.FlowExecutionExceptionHandler; +import org.springframework.webflow.engine.State; +import org.springframework.webflow.engine.TargetStateResolver; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.engine.ViewSelector; +import org.springframework.webflow.execution.Action; + +/** + * Base implementation that implements a minimal set of the + * FlowServiceLocator interface, throwing unsupported operation + * exceptions for some operations. + *

+ * May be subclassed to offer additional factory/lookup support. + * + * @author Keith Donald + */ +public class BaseFlowServiceLocator implements FlowServiceLocator { + + /** + * The factory encapsulating the creation of central Flow artifacts such as + * {@link Flow flows} and {@link State states}. + */ + private FlowArtifactFactory flowArtifactFactory = new FlowArtifactFactory(); + + /** + * The factory encapsulating the creation of bean invoking actions, actions + * that adapt methods on objects to the {@link Action} interface. + */ + private BeanInvokingActionFactory beanInvokingActionFactory = new BeanInvokingActionFactory(); + + /** + * The parser for parsing expression strings into evaluatable expression + * objects. + */ + private ExpressionParser expressionParser = DefaultExpressionParserFactory.getExpressionParser(); + + /** + * A conversion service that can convert between types. + */ + private ConversionService conversionService = createConversionService(null); + + /** + * A resource loader that can load resources. + */ + private ResourceLoader resourceLoader = new DefaultResourceLoader(); + + /** + * Sets the factory encapsulating the creation of central Flow artifacts + * such as {@link Flow flows} and {@link State states}. + */ + public void setFlowArtifactFactory(FlowArtifactFactory flowArtifactFactory) { + Assert.notNull(flowArtifactFactory, "The flow artifact factory is required"); + this.flowArtifactFactory = flowArtifactFactory; + } + + /** + * Sets the factory for creating bean invoking actions, actions that adapt + * methods on objects to the {@link Action} interface. + */ + public void setBeanInvokingActionFactory(BeanInvokingActionFactory beanInvokingActionFactory) { + Assert.notNull(beanInvokingActionFactory, "The bean invoking action factory is required"); + this.beanInvokingActionFactory = beanInvokingActionFactory; + } + + /** + * Set the expression parser responsible for parsing expression strings into + * evaluatable expression objects. + */ + public void setExpressionParser(ExpressionParser expressionParser) { + Assert.notNull(expressionParser, "The expression parser is required"); + this.expressionParser = expressionParser; + } + + /** + * Set the conversion service to use to convert between types; typically + * from string to a rich object type. + */ + public void setConversionService(ConversionService conversionService) { + Assert.notNull(conversionService, "The conversion service is required"); + this.conversionService = createConversionService(conversionService); + } + + /** + * Set the resource loader to load file-based resources from string-encoded + * paths. This is optional. + */ + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + public Flow getSubflow(String id) throws FlowArtifactLookupException { + throw new FlowArtifactLookupException(id, Flow.class, + "Subflow lookup is not supported by this service locator"); + } + + public Action getAction(String id) throws FlowArtifactLookupException { + return (Action)getBean(id, Action.class); + } + + public FlowAttributeMapper getAttributeMapper(String id) throws FlowArtifactLookupException { + return (FlowAttributeMapper)getBean(id, FlowAttributeMapper.class); + } + + public TransitionCriteria getTransitionCriteria(String id) throws FlowArtifactLookupException { + return (TransitionCriteria)getBean(id, TransitionCriteria.class); + } + + public TargetStateResolver getTargetStateResolver(String id) throws FlowArtifactLookupException { + return (TargetStateResolver)getBean(id, TargetStateResolver.class); + } + + public ViewSelector getViewSelector(String id) throws FlowArtifactLookupException { + return (ViewSelector)getBean(id, ViewSelector.class); + } + + public FlowExecutionExceptionHandler getExceptionHandler(String id) throws FlowArtifactLookupException { + return (FlowExecutionExceptionHandler)getBean(id, FlowExecutionExceptionHandler.class); + } + + public FlowArtifactFactory getFlowArtifactFactory() { + return flowArtifactFactory; + } + + public BeanInvokingActionFactory getBeanInvokingActionFactory() { + return beanInvokingActionFactory; + } + + public BeanFactory getBeanFactory() throws UnsupportedOperationException { + throw new UnsupportedOperationException("Bean factory lookup is not supported by this service locator"); + } + + public ResourceLoader getResourceLoader() { + return resourceLoader; + } + + public ExpressionParser getExpressionParser() { + return expressionParser; + } + + public ConversionService getConversionService() { + return conversionService; + } + + // helpers for use by subclasses + + /** + * Helper method for determining if the configured bean factory contains the + * provided bean. + * @param id the id of the bean + * @return true if yes, false otherwise + */ + protected boolean containsBean(String id) { + return getBeanFactory().containsBean(id); + } + + /** + * Helper method to lookup the bean representing a flow artifact of the + * specified type. + * @param id the bean id + * @param artifactType the bean type + * @return the bean + * @throws FlowArtifactLookupException an exception occurred + */ + protected Object getBean(String id, Class artifactType) throws FlowArtifactLookupException { + try { + return getBeanFactory().getBean(id, artifactType); + } + catch (BeansException e) { + throw new FlowArtifactLookupException(id, artifactType, e); + } + } + + /** + * Helper method to lookup the type of the bean with the provided id. + * @param id the bean id + * @param artifactType the bean type + * @return the bean's type + * @throws FlowArtifactLookupException an exception occurred + */ + protected Class getBeanType(String id, Class artifactType) throws FlowArtifactLookupException { + try { + return getBeanFactory().getType(id); + } + catch (BeansException e) { + throw new FlowArtifactLookupException(id, artifactType, e); + } + } + + /** + * Setup a conversion service used by this flow service locator. + * @param userConversionService a user supplied conversion service + * @return the newly created conversion service + */ + protected ConversionService createConversionService(ConversionService userConversionService) { + DefaultConversionService defaultConversionService = new DefaultConversionService(); + addWebFlowConverters(defaultConversionService); + if (userConversionService != null) { + return new CompositeConversionService( + new ConversionService[] { userConversionService, defaultConversionService}); + } + else { + return defaultConversionService; + } + } + + /** + * Add all web flow specific converters to given conversion service. + */ + protected void addWebFlowConverters(GenericConversionService conversionService) { + conversionService.addConverter(new TextToTransitionCriteria(this)); + conversionService.addConverter(new TextToTargetStateResolver(this)); + conversionService.addConverter(new TextToViewSelector(this)); + conversionService.addConverter(new TextToExpression(getExpressionParser())); + conversionService.addConverter(new TextToMethodSignature(conversionService)); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/DefaultFlowServiceLocator.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/DefaultFlowServiceLocator.java new file mode 100644 index 00000000..28aab6a2 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/DefaultFlowServiceLocator.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.util.Assert; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.definition.registry.NoSuchFlowDefinitionException; +import org.springframework.webflow.engine.Flow; + +/** + * The default flow service locator implementation that obtains subflow + * definitions from a dedicated {@link FlowDefinitionRegistry} and obtains the + * remaining services from a generic Spring {@link BeanFactory}. + * + * @see FlowDefinitionRegistry + * @see FlowServiceLocator#getSubflow(String) + * @see BeanFactory + * + * @author Keith Donald + */ +public class DefaultFlowServiceLocator extends BaseFlowServiceLocator { + + /** + * The registry for locating subflows. + */ + private FlowDefinitionRegistry subflowRegistry; + + /** + * The Spring bean factory used. + */ + private BeanFactory beanFactory; + + /** + * Creates a flow service locator that retrieves subflows from the provided + * registry and additional artifacts from the provided bean factory. + * @param subflowRegistry The registry for loading subflows + * @param beanFactory The spring bean factory + */ + public DefaultFlowServiceLocator(FlowDefinitionRegistry subflowRegistry, BeanFactory beanFactory) { + Assert.notNull(subflowRegistry, "The subflow registry is required"); + Assert.notNull(beanFactory, "The beanFactory is required"); + this.subflowRegistry = subflowRegistry; + this.beanFactory = beanFactory; + } + + public Flow getSubflow(String id) throws FlowArtifactLookupException { + try { + return (Flow)subflowRegistry.getFlowDefinition(id); + } + catch (NoSuchFlowDefinitionException e) { + throw new FlowArtifactLookupException(id, Flow.class, + "Could not locate subflow definition with id '" + id + "'", e); + } + } + + public BeanFactory getBeanFactory() { + return beanFactory; + } + + /** + * Returns the flow definition registry used to lookup subflows. + * @return the flow definition registry + */ + protected FlowDefinitionRegistry getSubflowRegistry() { + return subflowRegistry; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowArtifactFactory.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowArtifactFactory.java new file mode 100644 index 00000000..bc783d6a --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowArtifactFactory.java @@ -0,0 +1,275 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.engine.ActionState; +import org.springframework.webflow.engine.DecisionState; +import org.springframework.webflow.engine.EndState; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.FlowAttributeMapper; +import org.springframework.webflow.engine.FlowExecutionExceptionHandler; +import org.springframework.webflow.engine.State; +import org.springframework.webflow.engine.SubflowState; +import org.springframework.webflow.engine.TargetStateResolver; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.engine.TransitionableState; +import org.springframework.webflow.engine.ViewSelector; +import org.springframework.webflow.engine.ViewState; +import org.springframework.webflow.execution.Action; + +/** + * A factory for core web flow elements such as {@link Flow flows}, + * {@link State states}, and {@link Transition transitions}. + *

+ * This factory encapsulates the construction of each Flow implementation as + * well as each core artifact type. Subclasses may customize how the core elements + * are created, useful for plugging in custom implementations. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class FlowArtifactFactory { + + /** + * Factory method that creates a new {@link Flow} definition object. + *

+ * Note this method does not return a fully configured Flow instance, it + * only encapsulates the selection of implementation. A + * {@link FlowAssembler} delegating to a calling {@link FlowBuilder} is + * expected to assemble the Flow fully before returning it to external + * clients. + * @param id the flow identifier, should be unique to all flows in an + * application (required) + * @param attributes attributes to assign to the Flow, which may also be + * used to affect flow construction; may be null + * @return the initial flow instance, ready for assembly by a FlowBuilder + * @throws FlowArtifactLookupException an exception occured creating the + * Flow instance + */ + public Flow createFlow(String id, AttributeMap attributes) throws FlowArtifactLookupException { + Flow flow = new Flow(id); + flow.getAttributeMap().putAll(attributes); + return flow; + } + + /** + * Factory method that creates a new view state, a state where a user is + * allowed to participate in the flow. This method is an atomic operation + * that returns a fully initialized state. It encapsulates the selection of + * the view state implementation as well as the state assembly. + * @param id the identifier to assign to the state, must be unique to its + * owning flow (required) + * @param flow the flow that will own (contain) this state (required) + * @param entryActions any state entry actions; may be null + * @param viewSelector the state view selector strategy; may be null + * @param renderActions any 'render actions' to execute on entry and refresh; + * may be null + * @param transitions any transitions (paths) out of this state; may be null + * @param exceptionHandlers any exception handlers; may be null + * @param exitActions any state exit actions; may be null + * @param attributes attributes to assign to the State, which may also be + * used to affect state construction; may be null + * @return the fully initialized view state instance + * @throws FlowArtifactLookupException an exception occured creating the + * state + */ + public State createViewState(String id, Flow flow, Action[] entryActions, ViewSelector viewSelector, + Action[] renderActions, Transition[] transitions, FlowExecutionExceptionHandler[] exceptionHandlers, + Action[] exitActions, AttributeMap attributes) throws FlowArtifactLookupException { + ViewState viewState = new ViewState(flow, id); + if (viewSelector != null) { + viewState.setViewSelector(viewSelector); + } + viewState.getRenderActionList().addAll(renderActions); + configureCommonProperties(viewState, entryActions, transitions, exceptionHandlers, exitActions, attributes); + return viewState; + } + + /** + * Factory method that creates a new action state, a state where a system + * action is executed. This method is an atomic operation that returns a + * fully initialized state. It encapsulates the selection of the action + * state implementation as well as the state assembly. + * @param id the identifier to assign to the state, must be unique to its + * owning flow (required) + * @param flow the flow that will own (contain) this state (required) + * @param entryActions any state entry actions; may be null + * @param actions the actions to execute when the state is entered + * (required) + * @param transitions any transitions (paths) out of this state; may be null + * @param exceptionHandlers any exception handlers; may be null + * @param exitActions any state exit actions; may be null + * @param attributes attributes to assign to the State, which may also be + * used to affect state construction; may be null + * @return the fully initialized action state instance + * @throws FlowArtifactLookupException an exception occured creating the + * state + */ + public State createActionState(String id, Flow flow, Action[] entryActions, Action[] actions, + Transition[] transitions, FlowExecutionExceptionHandler[] exceptionHandlers, Action[] exitActions, + AttributeMap attributes) throws FlowArtifactLookupException { + ActionState actionState = new ActionState(flow, id); + actionState.getActionList().addAll(actions); + configureCommonProperties(actionState, entryActions, transitions, exceptionHandlers, exitActions, attributes); + return actionState; + } + + /** + * Factory method that creates a new decision state, a state where a flow + * routing decision is made. This method is an atomic operation that returns + * a fully initialized state. It encapsulates the selection of the decision + * state implementation as well as the state assembly. + * @param id the identifier to assign to the state, must be unique to its + * owning flow (required) + * @param flow the flow that will own (contain) this state (required) + * @param entryActions any state entry actions; may be null + * @param transitions any transitions (paths) out of this state + * @param exceptionHandlers any exception handlers; may be null + * @param exitActions any state exit actions; may be null + * @param attributes attributes to assign to the State, which may also be + * used to affect state construction; may be null + * @return the fully initialized decision state instance + * @throws FlowArtifactLookupException an exception occured creating the + * state + */ + public State createDecisionState(String id, Flow flow, Action[] entryActions, Transition[] transitions, + FlowExecutionExceptionHandler[] exceptionHandlers, Action[] exitActions, AttributeMap attributes) + throws FlowArtifactLookupException { + DecisionState decisionState = new DecisionState(flow, id); + configureCommonProperties(decisionState, entryActions, transitions, exceptionHandlers, exitActions, attributes); + return decisionState; + } + + /** + * Factory method that creates a new subflow state, a state where a parent + * flow spawns another flow as a subflow. This method is an atomic operation + * that returns a fully initialized state. It encapsulates the selection of + * the subflow state implementation as well as the state assembly. + * @param id the identifier to assign to the state, must be unique to its + * owning flow (required) + * @param flow the flow that will own (contain) this state (required) + * @param entryActions any state entry actions; may be null + * @param subflow the subflow definition (required) + * @param attributeMapper the subflow input and output attribute mapper; may + * be null + * @param transitions any transitions (paths) out of this state + * @param exceptionHandlers any exception handlers; may be null + * @param exitActions any state exit actions; may be null + * @param attributes attributes to assign to the State, which may also be + * used to affect state construction; may be null + * @return the fully initialized subflow state instance + * @throws FlowArtifactLookupException an exception occured creating the + * state + */ + public State createSubflowState(String id, Flow flow, Action[] entryActions, Flow subflow, + FlowAttributeMapper attributeMapper, Transition[] transitions, + FlowExecutionExceptionHandler[] exceptionHandlers, Action[] exitActions, AttributeMap attributes) + throws FlowArtifactLookupException { + SubflowState subflowState = new SubflowState(flow, id, subflow); + if (attributeMapper != null) { + subflowState.setAttributeMapper(attributeMapper); + } + configureCommonProperties(subflowState, entryActions, transitions, exceptionHandlers, exitActions, attributes); + return subflowState; + } + + /** + * Factory method that creates a new end state, a state where an executing + * flow session terminates. This method is an atomic operation that returns + * a fully initialized state. It encapsulates the selection of the end state + * implementation as well as the state assembly. + * @param id the identifier to assign to the state, must be unique to its + * owning flow (required) + * @param flow the flow that will own (contain) this state (required) + * @param entryActions any state entry actions; may be null + * @param viewSelector the state confirmation view selector strategy; may be + * null + * @param outputMapper the state output mapper; may be null + * @param exceptionHandlers any exception handlers; may be null + * @param attributes attributes to assign to the State, which may also be + * used to affect state construction; may be null + * @return the fully initialized subflow state instance + * @throws FlowArtifactLookupException an exception occured creating the + * state + */ + public State createEndState(String id, Flow flow, Action[] entryActions, ViewSelector viewSelector, + AttributeMapper outputMapper, FlowExecutionExceptionHandler[] exceptionHandlers, AttributeMap attributes) + throws FlowArtifactLookupException { + EndState endState = new EndState(flow, id); + if (viewSelector != null) { + endState.setViewSelector(viewSelector); + } + if (outputMapper != null) { + endState.setOutputMapper(outputMapper); + } + configureCommonProperties(endState, entryActions, exceptionHandlers, attributes); + return endState; + } + + /** + * Factory method that creates a new transition, a path from one step in a + * flow to another. This method is an atomic operation that returns a fully + * initialized transition. It encapsulates the selection of the transition + * implementation as well as the transition assembly. + * @param targetStateResolver the resolver of the target state of the transition (required) + * @param matchingCriteria the criteria that matches the transition; may be + * null + * @param executionCriteria the criteria that governs execution of the + * transition after match; may be null + * @param attributes attributes to assign to the transition, which may also + * be used to affect transition construction; may be null + * @return the fully initialized transition instance + * @throws FlowArtifactLookupException an exception occured creating the + * transition + */ + public Transition createTransition(TargetStateResolver targetStateResolver, TransitionCriteria matchingCriteria, + TransitionCriteria executionCriteria, AttributeMap attributes) throws FlowArtifactLookupException { + Transition transition = new Transition(targetStateResolver); + if (matchingCriteria != null) { + transition.setMatchingCriteria(matchingCriteria); + } + if (executionCriteria != null) { + transition.setExecutionCriteria(executionCriteria); + } + transition.getAttributeMap().putAll(attributes); + return transition; + } + + // internal helpers + + /** + * Configure common properties for a transitionable state. + */ + private void configureCommonProperties(TransitionableState state, Action[] entryActions, Transition[] transitions, + FlowExecutionExceptionHandler[] exceptionHandlers, Action[] exitActions, AttributeMap attributes) { + configureCommonProperties(state, entryActions, exceptionHandlers, attributes); + state.getTransitionSet().addAll(transitions); + state.getExitActionList().addAll(exitActions); + } + + /** + * Configure common properties for a state. + */ + private void configureCommonProperties(State state, Action[] entryActions, + FlowExecutionExceptionHandler[] exceptionHandlers, AttributeMap attributes) { + state.getEntryActionList().addAll(entryActions); + state.getExceptionHandlerSet().addAll(exceptionHandlers); + state.getAttributeMap().putAll(attributes); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowArtifactLookupException.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowArtifactLookupException.java new file mode 100644 index 00000000..aac012ff --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowArtifactLookupException.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; +import org.springframework.webflow.core.FlowException; +import org.springframework.webflow.execution.FlowExecutionException; + +/** + * A flow artifact lookup exception is thrown when an artifact (such as a flow, state, + * action, etc.) required by the webflow system cannot be obtained. + *

+ * Flow artifact lookup exceptions indicate unrecoverable problems with the flow + * definition, e.g. a required action of a flow cannot be found. They're not used + * to signal problems related to execution of a client request. A + * {@link FlowExecutionException} is used for that. + * + * @see org.springframework.webflow.execution.FlowExecutionException + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class FlowArtifactLookupException extends FlowException { + + /** + * The id of the artifact that could not be retrieved. + */ + private String artifactId; + + /** + * The type of artifact that could not be retrieved. + */ + private Class artifactType; + + /** + * Create a new flow artifact lookup exception. + * @param artifactId the id of the artifact + * @param artifactType the expected artifact type + */ + public FlowArtifactLookupException(String artifactId, Class artifactType) { + this(artifactId, artifactType, null, null); + } + + /** + * Create a new flow artifact lookup exception. + * @param artifactId the id of the artifact + * @param artifactType the expected artifact type + * @param cause the underlying cause of this exception + */ + public FlowArtifactLookupException(String artifactId, Class artifactType, Throwable cause) { + this(artifactId, artifactType, null, cause); + } + + /** + * Create a new flow artifact lookup exception. + * @param artifactId the id of the artifact + * @param artifactType the expected artifact type + * @param message descriptive message + */ + public FlowArtifactLookupException(String artifactId, Class artifactType, String message) { + this(artifactId, artifactType, message, null); + } + + /** + * Create a new flow artifact lookup exception. + * @param artifactId the id of the artifact + * @param artifactType the expected artifact type + * @param message descriptive message + * @param cause the underlying cause of this exception + */ + public FlowArtifactLookupException(String artifactId, Class artifactType, String message, Throwable cause) { + super((StringUtils.hasText(message) ? message : "Unable to obtain a " + ClassUtils.getShortName(artifactType) + + " flow artifact with id '" + artifactId + "': make sure there is a valid [" + artifactType + + "] exported with this id"), cause); + this.artifactType = artifactType; + this.artifactId = artifactId; + } + + /** + * Returns the id of the artifact that cannot be found. + */ + public String getArtifactId() { + return artifactId; + } + + /** + * Returns the expected artifact type. + */ + public Class getArtifactType() { + return artifactType; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowAssembler.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowAssembler.java new file mode 100644 index 00000000..c05e10ef --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowAssembler.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.Assert; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.CollectionUtils; +import org.springframework.webflow.engine.Flow; + +/** + * A director for assembling flows, delegating to a {@link FlowBuilder} to + * construct a flow. This class encapsulates the algorithm for using a + * FlowBuilder to assemble a Flow properly. It acts as the director in the + * classic GoF builder pattern. + *

+ * Flow assemblers may be used in a standalone, programmatic fashion as follows: + * + *

+ *     FlowBuilder builder = ...;
+ *     Flow flow = new FlowAssembler("myFlow", builder).assembleFlow();
+ * 
+ * + * @see org.springframework.webflow.engine.builder.FlowBuilder + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class FlowAssembler { + + private static final Log logger = LogFactory.getLog(FlowAssembler.class); + + /** + * The identifier to assign to the flow. + */ + private String flowId; + + /** + * Attributes that can be used to affect flow construction. + */ + private AttributeMap flowAttributes; + + /** + * The flow builder strategy used to construct the flow from its component + * parts. + */ + private FlowBuilder flowBuilder; + + /** + * Create a new flow assembler that will direct Flow assembly using the + * specified builder strategy. + * @param flowId the flow id to assign + * @param flowBuilder the builder the factory will use to build flows + */ + public FlowAssembler(String flowId, FlowBuilder flowBuilder) { + this(flowId, null, flowBuilder); + } + + /** + * Create a new flow assembler that will direct Flow assembly using the + * specified builder strategy. + * @param flowId the flow id to assign + * @param flowAttributes externally assigned flow attributes that can affect + * flow construction + * @param flowBuilder the builder the factory will use to build flows + */ + public FlowAssembler(String flowId, AttributeMap flowAttributes, FlowBuilder flowBuilder) { + Assert.hasText(flowId, "The flow id is required"); + Assert.notNull(flowBuilder, "The flow builder is required"); + this.flowId = flowId; + this.flowAttributes = (flowAttributes != null ? flowAttributes : CollectionUtils.EMPTY_ATTRIBUTE_MAP); + this.flowBuilder = flowBuilder; + } + + /** + * Returns the identifier to assign to the flow. + */ + public String getFlowId() { + return flowId; + } + + /** + * Returns externally assigned attributes that can be used to affect flow + * construction. + */ + public AttributeMap getFlowAttributes() { + return flowAttributes; + } + + /** + * Returns the flow builder strategy used to construct the flow from its + * component parts. + */ + public FlowBuilder getFlowBuilder() { + return flowBuilder; + } + + /** + * Assembles the flow, directing the construction process by delegating to + * the configured FlowBuilder. Every call to this method will assemble + * the Flow instance. + *

+ * This will drive the flow construction process as described in the + * {@link FlowBuilder} JavaDoc, starting with builder initialisation using + * {@link FlowBuilder#init(String, AttributeMap)} and finishing by + * cleaning up the builder with a call to {@link FlowBuilder#dispose()}. + * @return the constructed flow + * @throws FlowBuilderException when flow assembly fails + */ + public Flow assembleFlow() throws FlowBuilderException { + if (logger.isDebugEnabled()) { + logger.debug("Assembling flow definition with id '" + flowId + "' using flow builder '" + + flowBuilder + "'; externally assigned flow attributes are '" + flowAttributes + "'"); + } + try { + flowBuilder.init(flowId, flowAttributes); + directAssembly(); + return flowBuilder.getFlow(); + } + finally { + flowBuilder.dispose(); + } + } + + /** + * Build all parts of the flow by directing flow assembly by the flow + * builder. + * @throws FlowBuilderException when flow assembly fails + */ + protected void directAssembly() throws FlowBuilderException { + flowBuilder.buildVariables(); + flowBuilder.buildInputMapper(); + flowBuilder.buildStartActions(); + flowBuilder.buildInlineFlows(); + flowBuilder.buildStates(); + flowBuilder.buildGlobalTransitions(); + flowBuilder.buildEndActions(); + flowBuilder.buildOutputMapper(); + flowBuilder.buildExceptionHandlers(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowBuilder.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowBuilder.java new file mode 100644 index 00000000..65749306 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowBuilder.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.engine.Flow; + +/** + * Builder interface used to build a flow definition. The process of building a + * flow consists of the following steps: + *

    + *
  1. Initialize this builder, creating the initial flow definition, by + * calling {@link #init(String, AttributeMap)}. + *
  2. Call {@link #buildVariables()} to create any variables of the flow and + * add them to the flow definition. + *
  3. Call {@link #buildInputMapper()} to create and set the input mapper for + * the flow. + *
  4. Call {@link #buildStartActions()} to create and add any start actions to + * the flow. + *
  5. Call {@link #buildInlineFlows()} to create any inline flows + * encapsulated by the flow and add them to the flow definition. + *
  6. Call {@link #buildStates()} to create the states of the flow and add + * them to the flow definition. + *
  7. Call {@link #buildGlobalTransitions()} to create the any transitions + * shared by all states of the flow and add them to the flow definition. + *
  8. Call {@link #buildEndActions()} to create and add any end actions to + * the flow. + *
  9. Call {@link #buildOutputMapper()} to create and set the output mapper + * for the flow. + *
  10. Call {@link #buildExceptionHandlers()} to create the exception + * handlers of the flow and add them to the flow definition. + *
  11. Call {@link #getFlow()} to return the fully-built {@link Flow} + * definition. + *
  12. Dispose this builder, releasing any resources allocated during the + * building process by calling {@link #dispose()}. + *
+ *

+ * Implementations should encapsulate flow construction logic, either for a + * specific kind of flow, for example, an OrderFlowBuilder built + * in Java code, or a generic flow builder strategy, like the + * XmlFlowBuilder, for building flows from an XML-definition. + *

+ * Flow builders are used by the + * {@link org.springframework.webflow.engine.builder.FlowAssembler}, which acts as an + * assembler (director). Flow Builders may be reused, however, exercise caution + * when doing this as these objects are not thread safe. Also, for each use be + * sure to call init, followed by the build* methods, getFlow, and dispose + * completely in that order. + *

+ * This is an example of the classic GoF builder pattern. + * + * @see Flow + * @see org.springframework.webflow.engine.builder.FlowAssembler + * @see org.springframework.webflow.engine.builder.AbstractFlowBuilder + * @see org.springframework.webflow.engine.builder.xml.XmlFlowBuilder + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface FlowBuilder { + + /** + * Initialize this builder. This could cause the builder to open a stream to + * an externalized resource representing the flow definition, for example. + * @param flowId the identifier to assign to the flow + * @param attributes custom attributes to assign to the flow + * @throws FlowBuilderException an exception occured building the flow + */ + public void init(String flowId, AttributeMap attributes) throws FlowBuilderException; + + /** + * Builds any variables initialized by the flow when it starts. + * @throws FlowBuilderException an exception occured building the flow + */ + public void buildVariables() throws FlowBuilderException; + + /** + * Builds the input mapper responsible for mapping flow input on start. + * @throws FlowBuilderException an exception occured building the flow + */ + public void buildInputMapper() throws FlowBuilderException; + + /** + * Builds any start actions to execute when the flow starts. + * @throws FlowBuilderException an exception occured building the flow + */ + public void buildStartActions() throws FlowBuilderException; + + /** + * Builds any "in-line" flows encapsulated by the flow. + * @throws FlowBuilderException an exception occured building the flow + */ + public void buildInlineFlows() throws FlowBuilderException; + + /** + * Builds the states of the flow. + * @throws FlowBuilderException an exception occured building the flow + */ + public void buildStates() throws FlowBuilderException; + + /** + * Builds any transitions shared by all states of the flow. + * @throws FlowBuilderException an exception occured building the flow + */ + public void buildGlobalTransitions() throws FlowBuilderException; + + /** + * Builds any end actions to execute when the flow ends. + * @throws FlowBuilderException an exception occured building the flow + */ + public void buildEndActions() throws FlowBuilderException; + + /** + * Builds the output mapper responsible for mapping flow output on end. + * @throws FlowBuilderException an exception occured building the flow + */ + public void buildOutputMapper() throws FlowBuilderException; + + /** + * Creates and adds all exception handlers to the flow built by this + * builder. + * @throws FlowBuilderException an exception occured building this flow + */ + public void buildExceptionHandlers() throws FlowBuilderException; + + /** + * Get the fully constructed and configured Flow object - called by the + * builder's assembler (director) after assembly. When this method is called + * by the assembler, it is expected flow construction has completed + * and the returned flow is ready for use. + */ + public Flow getFlow(); + + /** + * Shutdown the builder, releasing any resources it holds. A new flow + * construction process should start with another call to the + * {@link #init(String, AttributeMap)} method. + */ + public void dispose(); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowBuilderException.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowBuilderException.java new file mode 100644 index 00000000..1e2d8601 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowBuilderException.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.webflow.core.FlowException; + +/** + * Exception thrown to indicate a problem while building a flow. + * + * @see org.springframework.webflow.engine.builder.FlowBuilder + * + * @author Erwin Vervaet + */ +public class FlowBuilderException extends FlowException { + + /** + * Create a new flow builder exception. + * @param message descriptive message + */ + public FlowBuilderException(String message) { + super(message); + } + + /** + * Create a new flow builder exception. + * @param message descriptive message + * @param cause the underlying cause of this exception + */ + public FlowBuilderException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowServiceLocator.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowServiceLocator.java new file mode 100644 index 00000000..0f64c540 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/FlowServiceLocator.java @@ -0,0 +1,159 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.core.io.ResourceLoader; +import org.springframework.webflow.action.AbstractBeanInvokingAction; +import org.springframework.webflow.action.BeanInvokingActionFactory; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.FlowAttributeMapper; +import org.springframework.webflow.engine.FlowExecutionExceptionHandler; +import org.springframework.webflow.engine.State; +import org.springframework.webflow.engine.TargetStateResolver; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.engine.ViewSelector; +import org.springframework.webflow.execution.Action; + +/** + * A support interface used by flow builders at configuration time. Acts as a + * "service locator" responsible for: + *

    + *
  1. Retrieving dependent (but externally managed) flow services needed to + * configure flow and state definitions. Such services are usually hosted in a + * backing registry and may be shared by multiple flows. + *
  2. Providing access to abstract factories to create core flow definitional + * artifacts such as {@link Flow}, {@link State}, {@link Transition}, and + * {@link AbstractBeanInvokingAction bean invoking actions}. These artifacts + * are unique to each flow and are typically not shared. + *
+ *

+ * In general, implementations of this interface act as facades to accessing and + * creating flow artifacts during {@link FlowAssembler flow assembly}. + *

+ * Finally, this interface also exposes access to generic infrastructure + * services also needed by flow assemblers such as a {@link ConversionService} + * and {@link ExpressionParser}. + * + * @see org.springframework.webflow.engine.builder.FlowBuilder + * @see org.springframework.webflow.engine.builder.BaseFlowBuilder + * @see org.springframework.webflow.engine.builder.FlowAssembler + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface FlowServiceLocator { + + /** + * Returns the Flow to be used as a subflow with the provided id. + * @param id the flow id + * @return the flow to be used as a subflow + * @throws FlowArtifactLookupException when no such flow is found + */ + public Flow getSubflow(String id) throws FlowArtifactLookupException; + + /** + * Retrieve an action to be executed within a flow with the assigned id. + * @param id the id of the action + * @throws FlowArtifactLookupException when no such action is found + */ + public Action getAction(String id) throws FlowArtifactLookupException; + + /** + * Returns the flow attribute mapper with the provided id. Flow attribute + * mappers are used from subflow states to map input and output attributes. + * @param id the attribute mapper id + * @return the attribute mapper + * @throws FlowArtifactLookupException when no such mapper is found + */ + public FlowAttributeMapper getAttributeMapper(String id) throws FlowArtifactLookupException; + + /** + * Returns the transition criteria to drive state transitions with the + * provided id. + * @param id the transition criteria id + * @return the transition criteria + * @throws FlowArtifactLookupException when no such criteria is found + */ + public TransitionCriteria getTransitionCriteria(String id) throws FlowArtifactLookupException; + + /** + * Returns the transition target state resolver with the specified id. + * @param id the target state resolver id + * @return the target state resolver + * @throws FlowArtifactLookupException when no such resolver is found + */ + public TargetStateResolver getTargetStateResolver(String id) throws FlowArtifactLookupException; + + /** + * Returns the view selector to make view selections in view states with the + * provided id. + * @param id the view selector id + * @return the view selector + * @throws FlowArtifactLookupException when no such selector is found + */ + public ViewSelector getViewSelector(String id) throws FlowArtifactLookupException; + + /** + * Returns the exception handler to handle flow execution exceptions with + * the provided id. + * @param id the exception handler id + * @return the exception handler + * @throws FlowArtifactLookupException when no such handler is found + */ + public FlowExecutionExceptionHandler getExceptionHandler(String id) throws FlowArtifactLookupException; + + /** + * Returns the factory for core flow artifacts such as Flow and State. + * @return the flow artifact factory + */ + public FlowArtifactFactory getFlowArtifactFactory(); + + /** + * Returns the factory for bean invoking actions. + * @return the bean invoking action factory + */ + public BeanInvokingActionFactory getBeanInvokingActionFactory(); + + /** + * Returns a generic bean (service) registry for accessing arbitrary beans. + * @return the generic service registry + * @throws UnsupportedOperationException when not supported by this locator + */ + public BeanFactory getBeanFactory() throws UnsupportedOperationException; + + /** + * Returns a generic resource loader for accessing file-based resources. + * @return the generic resource loader + */ + public ResourceLoader getResourceLoader(); + + /** + * Returns the expression parser for parsing expression strings. + * @return the expression parser + */ + public ExpressionParser getExpressionParser(); + + /** + * Returns a generic type conversion service for converting between types, + * typically from string to a rich value object. + * @return the generic conversion service + */ + public ConversionService getConversionService(); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/RefreshableFlowDefinitionHolder.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/RefreshableFlowDefinitionHolder.java new file mode 100644 index 00000000..7bcbab9d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/RefreshableFlowDefinitionHolder.java @@ -0,0 +1,194 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import java.io.IOException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.io.Resource; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.registry.FlowDefinitionConstructionException; +import org.springframework.webflow.definition.registry.FlowDefinitionHolder; +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. + *

+ * This class is threadsafe. + *

+ * 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 abstract world of {@link FlowDefinition flow definitions} + * with the concrete world of {@link Flow flow implementations}. + * + * @see FlowDefinition + * @see Flow + * @see FlowAssembler + * + * @author Keith Donald + */ +public class RefreshableFlowDefinitionHolder implements FlowDefinitionHolder { + + private static final Log logger = LogFactory.getLog(RefreshableFlowDefinitionHolder.class); + + /** + * The flow definition assembled by this assembler. + */ + private FlowDefinition flowDefinition; + + /** + * The flow assembler. + */ + private FlowAssembler assembler; + + /** + * 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. + */ + 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. + * @param assembler the flow assembler to use + */ + public RefreshableFlowDefinitionHolder(FlowAssembler assembler) { + this.assembler = assembler; + } + + public String getFlowDefinitionId() { + return assembler.getFlowId(); + } + + public synchronized FlowDefinition getFlowDefinition() throws FlowDefinitionConstructionException { + if (assembling) { + // must return early assembly result + return getFlowBuilder().getFlow(); + } + if (!isAssembled()) { + lastModified = calculateLastModified(); + assembleFlow(); + } + else { + refreshIfChanged(); + } + return flowDefinition; + } + + public synchronized void refresh() throws FlowBuilderException { + assembleFlow(); + } + + // internal helpers + + /** + * Returns the flow builder that actually builds the Flow definition. + */ + protected FlowBuilder getFlowBuilder() { + return assembler.getFlowBuilder(); + } + + /** + * Reassemble the flow if its underlying resource has changed. + */ + protected void refreshIfChanged() { + if (this.lastModified == -1) { + // just ignore, tracking last modified date not supported + return; + } + long calculatedLastModified = calculateLastModified(); + if (this.lastModified < calculatedLastModified) { + if (logger.isDebugEnabled()) { + logger.debug("Resource modification detected, reloading flow definition with id '" + + assembler.getFlowId() + "'"); + } + assembleFlow(); + this.lastModified = calculatedLastModified; + } + } + + /** + * 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(); + if (logger.isDebugEnabled()) { + logger.debug( + "Calculating last modified timestamp for flow definition resource '" + resource + "'"); + } + try { + return resource.getFile().lastModified(); + } + catch (IOException e) { + // ignore, last modified checks not supported + } + } + return -1; + } + + /** + * Returns the last modifed date of the backed flow definition resource. + * @return the last modified date + */ + protected long getLastModified() { + return lastModified; + } + + /** + * Assemble the held flow definition, delegating to the configured + * FlowAssembler (director). + */ + protected void assembleFlow() throws FlowBuilderException { + if (logger.isDebugEnabled()) { + logger.debug("Assembling flow definition with id '" + assembler.getFlowId() + "'"); + } + try { + assembling = true; + flowDefinition = assembler.assembleFlow(); + } + finally { + assembling = false; + } + } + + /** + * Returns a flag indicating if this holder has performed and completed + * flow definition assembly. + */ + protected boolean isAssembled() { + return flowDefinition != null; + } + + /** + * Returns a flag indicating if this holder is performing assembly. + */ + protected boolean isAssembling() { + return assembling; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/TextToTargetStateResolver.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/TextToTargetStateResolver.java new file mode 100644 index 00000000..f45523ac --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/TextToTargetStateResolver.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.binding.convert.ConversionContext; +import org.springframework.binding.convert.support.AbstractConverter; +import org.springframework.binding.expression.Expression; +import org.springframework.webflow.engine.TargetStateResolver; +import org.springframework.webflow.engine.support.DefaultTargetStateResolver; + +/** + * Converter that takes an encoded string representation and produces a + * corresponding {@link TargetStateResolver} object. + *

+ * This converter supports the following encoded forms: + *

    + *
  • "stateId" - will result in a TargetStateResolver that always resolves + * the same state.
  • + *
  • "${stateIdExpression} - will result in a TargetStateResolver that + * resolves the target state by evaluating an expression against the request + * context.
  • + *
  • "bean:<id>" - will result in usage of a custom TargetStateResolver + * bean implementation configured in an external context.
  • + *
+ * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class TextToTargetStateResolver extends AbstractConverter { + + /** + * Prefix used when the user wants to use a custom TargetStateResolver + * implementation managed by a factory. + */ + private static final String BEAN_PREFIX = "bean:"; + + /** + * Locator to use for loading custom TargetStateResolver beans. + */ + private FlowServiceLocator flowServiceLocator; + + /** + * Create a new converter that converts strings to transition target state + * resolver objects. The given conversion service will be used to do all + * necessary internal conversion (e.g. parsing expression strings). + */ + public TextToTargetStateResolver(FlowServiceLocator flowServiceLocator) { + this.flowServiceLocator = flowServiceLocator; + } + + public Class[] getSourceClasses() { + return new Class[] { String.class }; + } + + public Class[] getTargetClasses() { + return new Class[] { TargetStateResolver.class }; + } + + protected Object doConvert(Object source, Class targetClass, ConversionContext context) throws Exception { + String targetStateId = (String)source; + if (flowServiceLocator.getExpressionParser().isDelimitedExpression(targetStateId)) { + Expression expression = flowServiceLocator.getExpressionParser().parseExpression(targetStateId); + return new DefaultTargetStateResolver(expression); + } + else if (targetStateId.startsWith(BEAN_PREFIX)) { + return flowServiceLocator.getTargetStateResolver(targetStateId.substring(BEAN_PREFIX.length())); + } + else { + return new DefaultTargetStateResolver(targetStateId); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/TextToTransitionCriteria.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/TextToTransitionCriteria.java new file mode 100644 index 00000000..7ac24c1e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/TextToTransitionCriteria.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.binding.convert.ConversionContext; +import org.springframework.binding.convert.ConversionException; +import org.springframework.binding.convert.support.AbstractConverter; +import org.springframework.binding.expression.Expression; +import org.springframework.util.StringUtils; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.engine.WildcardTransitionCriteria; +import org.springframework.webflow.engine.support.BooleanExpressionTransitionCriteria; +import org.springframework.webflow.engine.support.EventIdTransitionCriteria; + +/** + * Converter that takes an encoded string representation and produces a + * corresponding TransitionCriteria object. + *

+ * This converter supports the following encoded forms: + *

    + *
  • "*" - will result in a TransitionCriteria object that matches on + * everything ({@link org.springframework.webflow.engine.WildcardTransitionCriteria}) + *
  • + *
  • "eventId" - will result in a TransitionCriteria object that matches + * given event id ({@link org.springframework.webflow.engine.support.EventIdTransitionCriteria}) + *
  • + *
  • "${...}" - will result in a TransitionCriteria object that evaluates + * given condition, expressed as an expression + * ({@link org.springframework.webflow.engine.support.BooleanExpressionTransitionCriteria}) + *
  • + *
  • "bean:<id>" - will result in usage of a custom TransitionCriteria + * bean implementation.
  • + *
+ * + * @see org.springframework.webflow.engine.TransitionCriteria + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class TextToTransitionCriteria extends AbstractConverter { + + /** + * Prefix used when the user wants to use a custom TransitionCriteria + * implementation managed by a bean factory. + */ + private static final String BEAN_PREFIX = "bean:"; + + /** + * Locator to use for loading custom TransitionCriteria beans. + */ + private FlowServiceLocator flowServiceLocator; + + /** + * Create a new converter that converts strings to transition criteria + * objects. Custom transition criteria will be looked up using given + * service locator. + */ + public TextToTransitionCriteria(FlowServiceLocator flowServiceLocator) { + this.flowServiceLocator = flowServiceLocator; + } + + public Class[] getSourceClasses() { + return new Class[] { String.class }; + } + + public Class[] getTargetClasses() { + return new Class[] { TransitionCriteria.class }; + } + + protected Object doConvert(Object source, Class targetClass, ConversionContext context) throws Exception { + String encodedCriteria = (String)source; + if (!StringUtils.hasText(encodedCriteria) + || WildcardTransitionCriteria.WILDCARD_EVENT_ID.equals(encodedCriteria)) { + return WildcardTransitionCriteria.INSTANCE; + } + else if (flowServiceLocator.getExpressionParser().isDelimitedExpression(encodedCriteria)) { + Expression expression = flowServiceLocator.getExpressionParser().parseExpression(encodedCriteria); + return createBooleanExpressionTransitionCriteria(expression); + } + else if (encodedCriteria.startsWith(BEAN_PREFIX)) { + return flowServiceLocator.getTransitionCriteria(encodedCriteria.substring(BEAN_PREFIX.length())); + } + else { + return createEventIdTransitionCriteria(encodedCriteria); + } + } + + /** + * Hook method subclasses can override to return a specialized eventId + * matching transition criteria implementation. + * @param eventId the event id to match + * @return the transition criteria object + * @throws ConversionException when something goes wrong + */ + protected TransitionCriteria createEventIdTransitionCriteria(String eventId) throws ConversionException { + return new EventIdTransitionCriteria(eventId); + } + + /** + * Hook method subclasses can override to return a specialized expression + * evaluating transition criteria implementation. + * @param expression the expression to evaluate + * @return the transition criteria object + * @throws ConversionException when something goes wrong + */ + protected TransitionCriteria createBooleanExpressionTransitionCriteria(Expression expression) + throws ConversionException { + return new BooleanExpressionTransitionCriteria(expression); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/TextToViewSelector.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/TextToViewSelector.java new file mode 100644 index 00000000..6f36fbd1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/TextToViewSelector.java @@ -0,0 +1,149 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.binding.convert.ConversionContext; +import org.springframework.binding.convert.support.ConversionServiceAwareConverter; +import org.springframework.binding.expression.Expression; +import org.springframework.util.StringUtils; +import org.springframework.webflow.engine.NullViewSelector; +import org.springframework.webflow.engine.ViewSelector; +import org.springframework.webflow.engine.support.ApplicationViewSelector; +import org.springframework.webflow.engine.support.ExternalRedirectSelector; +import org.springframework.webflow.engine.support.FlowDefinitionRedirectSelector; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.execution.support.ExternalRedirect; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; +import org.springframework.webflow.execution.support.FlowExecutionRedirect; + +/** + * Converter that converts an encoded string representation of a view selector + * into a {@link ViewSelector} object that will make selections at runtime. + *

+ * This converter supports the following encoded forms: + *

    + *
  • empty - will result in a {@link NullViewSelector}.
  • + *
  • "viewName" - will result in an {@link ApplicationViewSelector} that + * returns an {@link ApplicationView} ViewSelection with the provided view name expression.
  • + *
  • "redirect:<viewName>" - will result in an + * {@link ApplicationViewSelector} that returns an {@link FlowExecutionRedirect} + * to a flow execution URL.
  • + *
  • "externalRedirect:<url>" - will result in an + * {@link ExternalRedirectSelector} that returns an {@link ExternalRedirect} to a + * URL.
  • + *
  • "flowRedirect:<flowId>" - will result in a + * {@link FlowDefinitionRedirectSelector} that returns a {@link FlowDefinitionRedirect} + * to a flow.
  • + *
  • "bean:<id>" - will result in usage of a custom + * ViewSelector bean implementation.
  • + *
+ * + * @see org.springframework.webflow.execution.ViewSelection + * @see org.springframework.webflow.engine.ViewSelector + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class TextToViewSelector extends ConversionServiceAwareConverter { + + /** + * Prefix used when the encoded view name wants to specify that a redirect + * is required. ("redirect:") + */ + public static final String REDIRECT_PREFIX = "redirect:"; + + /** + * Prefix used when the encoded view name wants to specify that a redirect + * to an external URL is required. ("externalRedirect:") + */ + public static final String EXTERNAL_REDIRECT_PREFIX = "externalRedirect:"; + + /** + * Prefix used when the encoded view name wants to specify that a redirect + * to a flow definition is requred. ("flowRedirect:") + */ + public static final String FLOW_DEFINITION_REDIRECT_PREFIX = "flowRedirect:"; + + /** + * Prefix used when the user wants to use a ViewSelector implementation + * managed by a bean factory. ("bean:") + */ + private static final String BEAN_PREFIX = "bean:"; + + /** + * Locator to use for loading custom ViewSelector beans. + */ + private FlowServiceLocator flowServiceLocator; + + /** + * Create a new text to ViewSelector converter. Custom ViewSelector implemenations + * will be looked up using given service locator. + */ + public TextToViewSelector(FlowServiceLocator flowServiceLocator) { + this.flowServiceLocator = flowServiceLocator; + setConversionService(flowServiceLocator.getConversionService()); + } + + public Class[] getSourceClasses() { + return new Class[] { String.class }; + } + + public Class[] getTargetClasses() { + return new Class[] { ViewSelector.class }; + } + + protected Object doConvert(Object source, Class targetClass, ConversionContext context) throws Exception { + String encodedView = (String)source; + if (!StringUtils.hasText(encodedView)) { + return NullViewSelector.INSTANCE; + } + else { + return convertEncodedViewSelector(encodedView); + } + } + + /** + * Convert given encoded view into an appropriate view selector. + * @param encodedView the encoded view selector + * @return the view selector + */ + protected ViewSelector convertEncodedViewSelector(String encodedView) { + if (encodedView.startsWith(REDIRECT_PREFIX)) { + String viewName = encodedView.substring(REDIRECT_PREFIX.length()); + Expression viewNameExpr = (Expression)fromStringTo(Expression.class).execute(viewName); + // just show the application view using a redirect + return new ApplicationViewSelector(viewNameExpr, true); + } + else if (encodedView.startsWith(EXTERNAL_REDIRECT_PREFIX)) { + String externalUrl = encodedView.substring(EXTERNAL_REDIRECT_PREFIX.length()); + Expression urlExpr = (Expression)fromStringTo(Expression.class).execute(externalUrl); + return new ExternalRedirectSelector(urlExpr); + } + else if (encodedView.startsWith(FLOW_DEFINITION_REDIRECT_PREFIX)) { + String flowRedirect = encodedView.substring(FLOW_DEFINITION_REDIRECT_PREFIX.length()); + Expression redirectExpr = (Expression)fromStringTo(Expression.class).execute(flowRedirect); + return new FlowDefinitionRedirectSelector(redirectExpr); + } + else if (encodedView.startsWith(BEAN_PREFIX)) { + String id = encodedView.substring(BEAN_PREFIX.length()); + return flowServiceLocator.getViewSelector(id); + } + else { + Expression viewNameExpr = (Expression)fromStringTo(Expression.class).execute(encodedView); + return new ApplicationViewSelector(viewNameExpr); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/package.html b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/package.html new file mode 100644 index 00000000..8a80803c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/package.html @@ -0,0 +1,33 @@ + + +

+The flow builder subsystem for building and assembling executable flow definitions. +

+

+You construct a Flow using a {@link org.springframework.webflow.engine.builder.FlowBuilder}. +This package defines the following flow builder implementations: +

    +
  • +{@link org.springframework.webflow.engine.builder.AbstractFlowBuilder} - A +convenience superclass to use when you want to assemble the web flow +in Java code. +
  • +
  • +{@link org.springframework.webflow.engine.builder.xml.XmlFlowBuilder} - A flow +builder that reads an XML file containing a web flow definition and +constructs the flow accordingly. +
  • +
+During flow construction, a flow builder may need to access externally +managed flow artifacts referenced by the flow definition. +The {@link org.springframework.webflow.engine.builder.FlowServiceLocator} fulfills +this need, acting as a facade or gateway to an external registry of +flow artifacts (such as a Spring Bean Factory). +

+

+To direct flow construction, use the +{@link org.springframework.webflow.engine.builder.FlowAssembler}. +This package is based on the classic GoF Builder design pattern. +

+ + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/DefaultDocumentLoader.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/DefaultDocumentLoader.java new file mode 100644 index 00000000..ba812e73 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/DefaultDocumentLoader.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder.xml; + +import java.io.IOException; +import java.io.InputStream; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.io.Resource; +import org.springframework.util.xml.SimpleSaxErrorHandler; +import org.w3c.dom.Document; +import org.xml.sax.EntityResolver; +import org.xml.sax.SAXException; + +/** + * The default document loader strategy for XSD-based XML documents with + * validation enabled by default. + *

+ * Note: full XSD support requires JDK 5.0 or a capable parser such as Xerces + * 2.0. JDK 1.4 or < do not fully support XSD out of the box. To use this + * implementation on JDK 1.4 make sure Xerces is available in your classpath or + * disable XSD validation by + * {@link #setValidating(boolean) setting the validating property to false}. + * + * @author Keith Donald + */ +public class DefaultDocumentLoader implements DocumentLoader { + + private static final Log logger = LogFactory.getLog(DefaultDocumentLoader.class); + + /** + * JAXP attribute used to configure the schema language for validation. + */ + private static final String SCHEMA_LANGUAGE_ATTRIBUTE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage"; + + /** + * JAXP attribute value indicating the XSD schema language. + */ + private static final String XSD_SCHEMA_LANGUAGE = "http://www.w3.org/2001/XMLSchema"; + + /** + * Flag indicating if the XML document parser will perform schema + * validation. + */ + private boolean validating = true; + + /** + * The spring-webflow schema resolution strategy. + */ + private EntityResolver entityResolver = new WebFlowEntityResolver(); + + /** + * Returns whether or not the XML parser will validate the document. + */ + public boolean isValidating() { + return validating; + } + + /** + * Set if the XML parser should validate the document and thus enforce a + * schema. Defaults to true. + */ + public void setValidating(boolean validating) { + this.validating = validating; + } + + /** + * Returns the SAX entity resolver used by the XML parser. + */ + public EntityResolver getEntityResolver() { + return entityResolver; + } + + /** + * Set a SAX entity resolver to be used for parsing. Can be overridden for + * custom entity resolution, for example relative to some specific base + * path. + * @see org.springframework.webflow.engine.builder.xml.WebFlowEntityResolver + */ + public void setEntityResolver(EntityResolver entityResolver) { + this.entityResolver = entityResolver; + } + + public Document loadDocument(Resource resource) throws IOException, ParserConfigurationException, SAXException { + InputStream is = null; + try { + is = resource.getInputStream(); + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + factory.setValidating(isValidating()); + factory.setNamespaceAware(true); + try { + factory.setAttribute(SCHEMA_LANGUAGE_ATTRIBUTE, XSD_SCHEMA_LANGUAGE); + } + catch (IllegalArgumentException ex) { + throw new IllegalStateException("Unable to validate using XSD: Your JAXP provider [" + factory + + "] does not support XML Schema. " + + "Are you running on Java 1.4 or below with Apache Crimson? " + + "If so you must upgrade to Apache Xerces (or Java 5 or >) for full XSD support."); + } + DocumentBuilder docBuilder = factory.newDocumentBuilder(); + docBuilder.setErrorHandler(new SimpleSaxErrorHandler(logger)); + docBuilder.setEntityResolver(getEntityResolver()); + return docBuilder.parse(is); + } + finally { + if (is != null) { + is.close(); + } + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/DocumentLoader.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/DocumentLoader.java new file mode 100644 index 00000000..17ce64d7 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/DocumentLoader.java @@ -0,0 +1,42 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder.xml; + +import java.io.IOException; + +import javax.xml.parsers.ParserConfigurationException; + +import org.springframework.core.io.Resource; +import org.w3c.dom.Document; +import org.xml.sax.SAXException; + +/** + * A generic strategy interface encapsulating the logic to load an XML-based document. + * + * @author Keith Donald + */ +public interface DocumentLoader { + + /** + * Load the XML-based document from the external resource. + * @param resource the document resource + * @return the loaded (parsed) document + * @throws IOException an exception occured accessing the resource input stream + * @throws ParserConfigurationException an exception occured building the document parser + * @throws SAXException a error occured during document parsing + */ + public Document loadDocument(Resource resource) throws IOException, ParserConfigurationException, SAXException; +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/ImmutableFlowAttributeMapper.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/ImmutableFlowAttributeMapper.java new file mode 100644 index 00000000..1c478ff2 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/ImmutableFlowAttributeMapper.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder.xml; + +import java.io.Serializable; + +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.core.style.ToStringCreator; +import org.springframework.webflow.engine.support.AbstractFlowAttributeMapper; + +/** + * Simple flow attribute mapper that holds an input and output mapper strategy. + * This is an internal helper class of the XmlFlowBuilder. + * + * @see org.springframework.webflow.engine.builder.xml.XmlFlowBuilder + * + * @author Keith Donald + */ +final class ImmutableFlowAttributeMapper extends AbstractFlowAttributeMapper implements Serializable { + + private final AttributeMapper inputMapper; + + private final AttributeMapper outputMapper; + + /** + * Create a new flow attribute mapper using given mapping strategies. + * @param inputMapper the input mapping strategy + * @param outputMapper the output mapping strategy + */ + public ImmutableFlowAttributeMapper(AttributeMapper inputMapper, AttributeMapper outputMapper) { + this.inputMapper = inputMapper; + this.outputMapper = outputMapper; + } + + protected AttributeMapper getInputMapper() { + return inputMapper; + } + + protected AttributeMapper getOutputMapper() { + return outputMapper; + } + + public String toString() { + return new ToStringCreator(this).append("inputMapper", inputMapper) + .append("outputMapper", outputMapper).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/LocalFlowServiceLocator.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/LocalFlowServiceLocator.java new file mode 100644 index 00000000..6a0d904a --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/LocalFlowServiceLocator.java @@ -0,0 +1,234 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder.xml; + +import java.util.Stack; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.core.io.ResourceLoader; +import org.springframework.webflow.action.BeanInvokingActionFactory; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.FlowAttributeMapper; +import org.springframework.webflow.engine.FlowExecutionExceptionHandler; +import org.springframework.webflow.engine.TargetStateResolver; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.engine.ViewSelector; +import org.springframework.webflow.engine.builder.FlowArtifactFactory; +import org.springframework.webflow.engine.builder.FlowArtifactLookupException; +import org.springframework.webflow.engine.builder.FlowServiceLocator; +import org.springframework.webflow.execution.Action; + +/** + * Searches flow-local registries first before querying the global, externally + * managed flow service locator. + *

+ * Internal helper class of the {@link org.springframework.webflow.engine.builder.xml.XmlFlowBuilder}. + * Package private to highlight it's non-public nature. + * + * @see org.springframework.webflow.engine.builder.xml.XmlFlowBuilder + * + * @author Keith Donald + */ +class LocalFlowServiceLocator implements FlowServiceLocator { + + /** + * The stack of registries. + */ + private Stack localRegistries = new Stack(); + + /** + * The parent service locator. + */ + private FlowServiceLocator parent; + + /** + * Creates a new local service locator. + * @param parent the parent service locator + */ + public LocalFlowServiceLocator(FlowServiceLocator parent) { + this.parent = parent; + } + + /** + * Push a new registry onto the stack. + * @param registry the local registry + */ + public void push(LocalFlowServiceRegistry registry) { + registry.init(this, parent); + localRegistries.push(registry); + } + + /** + * Pop a registry off the stack. + */ + public LocalFlowServiceRegistry pop() { + return (LocalFlowServiceRegistry)localRegistries.pop(); + } + + /** + * Pops all registries off the stack until the stack is empty. + */ + public void diposeOfAnyRegistries() { + while (!localRegistries.isEmpty()) { + pop(); + } + } + + /** + * Returns the top registry on the stack + */ + public LocalFlowServiceRegistry top() { + return (LocalFlowServiceRegistry)localRegistries.peek(); + } + + /** + * Returns true if this locator has no local registries. + */ + public boolean isEmpty() { + return localRegistries.isEmpty(); + } + + // implementing FlowServiceLocator + + public Flow getSubflow(String id) throws FlowArtifactLookupException { + Flow currentFlow = getCurrentFlow(); + // quick check for recursive subflow + if (currentFlow.getId().equals(id)) { + return currentFlow; + } + // check local inline flows + if (currentFlow.containsInlineFlow(id)) { + return currentFlow.getInlineFlow(id); + } + // check externally managed top-level flows + return parent.getSubflow(id); + } + + public Action getAction(String id) throws FlowArtifactLookupException { + if (containsBean(id)) { + return (Action)getBean(id, Action.class); + } + else { + return parent.getAction(id); + } + } + + public FlowAttributeMapper getAttributeMapper(String id) throws FlowArtifactLookupException { + if (containsBean(id)) { + return (FlowAttributeMapper)getBean(id, FlowAttributeMapper.class); + } + else { + return parent.getAttributeMapper(id); + } + } + + public TransitionCriteria getTransitionCriteria(String id) throws FlowArtifactLookupException { + if (containsBean(id)) { + return (TransitionCriteria)getBean(id, TransitionCriteria.class); + } + else { + return parent.getTransitionCriteria(id); + } + } + + public TargetStateResolver getTargetStateResolver(String id) throws FlowArtifactLookupException { + if (containsBean(id)) { + return (TargetStateResolver)getBean(id, TargetStateResolver.class); + } + else { + return parent.getTargetStateResolver(id); + } + } + + public ViewSelector getViewSelector(String id) throws FlowArtifactLookupException { + if (containsBean(id)) { + return (ViewSelector)getBean(id, ViewSelector.class); + } + else { + return parent.getViewSelector(id); + } + } + + public FlowExecutionExceptionHandler getExceptionHandler(String id) throws FlowArtifactLookupException { + if (containsBean(id)) { + return (FlowExecutionExceptionHandler)getBean(id, FlowExecutionExceptionHandler.class); + } + else { + return parent.getExceptionHandler(id); + } + } + + public FlowArtifactFactory getFlowArtifactFactory() { + return parent.getFlowArtifactFactory(); + } + + public BeanInvokingActionFactory getBeanInvokingActionFactory() { + return parent.getBeanInvokingActionFactory(); + } + + public BeanFactory getBeanFactory() { + return top().getContext(); + } + + public ResourceLoader getResourceLoader() { + return parent.getResourceLoader(); + } + + public ExpressionParser getExpressionParser() { + return parent.getExpressionParser(); + } + + public ConversionService getConversionService() { + return parent.getConversionService(); + } + + // internal helpers + + /** + * Returns the flow for the registry at the top of the stack. + */ + protected Flow getCurrentFlow() { + return top().getFlow(); + } + + /** + * Does this flow local service locator contain a bean defintion + * for given id? + */ + protected boolean containsBean(String id) { + if (localRegistries.isEmpty()) { + return false; + } + else { + return getBeanFactory().containsBean(id); + } + } + + /** + * Get the identified bean and make sure it is of the required type. + */ + protected Object getBean(String id, Class artifactType) throws FlowArtifactLookupException { + try { + return getBeanFactory().getBean(id, artifactType); + } + catch (BeansException e) { + throw new FlowArtifactLookupException(id, artifactType, e); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/LocalFlowServiceRegistry.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/LocalFlowServiceRegistry.java new file mode 100644 index 00000000..dba9c09f --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/LocalFlowServiceRegistry.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder.xml; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.GenericWebApplicationContext; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.builder.FlowServiceLocator; + +/** + * Simple value object that holds a reference to a local artifact registry + * of a flow definition that is in the process of being constructed. + *

+ * Internal helper class of the {@link org.springframework.webflow.engine.builder.xml.XmlFlowBuilder}. + * Package private to highlight it's non-public nature. + * + * @see org.springframework.webflow.engine.builder.xml.XmlFlowBuilder + * @see org.springframework.webflow.engine.builder.xml.LocalFlowServiceLocator + * + * @author Keith Donald + */ +class LocalFlowServiceRegistry { + + /** + * The flow this registry is for (and scoped by). + */ + private Flow flow; + + /** + * The locations of the registry resource definitions. + */ + private Resource[] resources; + + /** + * The local registry holding the artifacts local to the flow. + */ + private GenericApplicationContext context; + + /** + * Create a new registry, loading artifact definitions from + * given resources. + * @param flow the flow this registry is for (and scoped by) + * @param resources the registry resource definitions + */ + public LocalFlowServiceRegistry(Flow flow, Resource[] resources) { + this.flow = flow; + this.resources = resources; + } + + /** + * Returns the flow this registry is for (and scoped by). + */ + public Flow getFlow() { + return flow; + } + + /** + * Returns the resources defining registry artifacts. + */ + public Resource[] getResources() { + return resources; + } + + /** + * Retuns the application context holding registry artifacts. + */ + public ApplicationContext getContext() { + return context; + } + + /** + * Initialize this registry of the local flow service locator. + * @param localFactory the local flow service locator + * @param rootFactory the root service locator + */ + public void init(LocalFlowServiceLocator localFactory, FlowServiceLocator rootFactory) { + BeanFactory parent = null; + if (localFactory.isEmpty()) { + try { + parent = rootFactory.getBeanFactory(); + } + catch (UnsupportedOperationException e) { + // can't link to a parent + } + } + else { + parent = localFactory.top().context; + } + context = createLocalFlowContext(parent, rootFactory); + new XmlBeanDefinitionReader(context).loadBeanDefinitions(resources); + context.refresh(); + } + + /** + * Create the flow local application context. + * @param parent the parent application context + * @param rootFactory the root service locator, used to obtain a resource + * loader + * @return the flow local application context + */ + private GenericApplicationContext createLocalFlowContext(BeanFactory parent, FlowServiceLocator rootFactory) { + if (parent instanceof WebApplicationContext) { + GenericWebApplicationContext context = new GenericWebApplicationContext(); + context.setServletContext(((WebApplicationContext)parent).getServletContext()); + context.setParent((WebApplicationContext)parent); + context.setResourceLoader(rootFactory.getResourceLoader()); + return context; + } + else { + GenericApplicationContext context = new GenericApplicationContext(); + if (parent instanceof ApplicationContext) { + context.setParent((ApplicationContext)parent); + } + else { + if (parent != null) { + context.getBeanFactory().setParentBeanFactory(parent); + } + } + context.setResourceLoader(rootFactory.getResourceLoader()); + return context; + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/WebFlowEntityResolver.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/WebFlowEntityResolver.java new file mode 100644 index 00000000..b7fceef2 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/WebFlowEntityResolver.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder.xml; + +import java.io.IOException; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.Resource; +import org.xml.sax.EntityResolver; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; + +/** + * EntityResolver implementation for the Spring Web Flow 1.0 XML Schema. This + * will load the XSD from the classpath. + *

+ * The xmlns of the XSD expected to be resolved: + * + *

+ *     <?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-1.0.xsd">
+ * 
+ * + * @author Erwin Vervaet + * @author Ben Hale + */ +public class WebFlowEntityResolver implements EntityResolver { + + private static final String WEBFLOW_ELEMENT = "spring-webflow-1.0"; + + public InputSource resolveEntity(String publicId, String systemId) throws SAXException, IOException { + if (systemId != null && systemId.indexOf(WEBFLOW_ELEMENT) > systemId.lastIndexOf("/")) { + String filename = systemId.substring(systemId.indexOf(WEBFLOW_ELEMENT)); + try { + Resource resource = new ClassPathResource(filename, getClass()); + InputSource source = new InputSource(resource.getInputStream()); + source.setPublicId(publicId); + source.setSystemId(systemId); + return source; + } + catch (IOException ex) { + // fall through below + } + } + // let the parser handle it + return null; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilder.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilder.java new file mode 100644 index 00000000..11ccc401 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowBuilder.java @@ -0,0 +1,1070 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder.xml; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import javax.xml.parsers.ParserConfigurationException; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.binding.convert.ConversionExecutor; +import org.springframework.binding.convert.ConversionService; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.binding.expression.SettableExpression; +import org.springframework.binding.expression.support.CollectionAddingExpression; +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.binding.mapping.DefaultAttributeMapper; +import org.springframework.binding.mapping.Mapping; +import org.springframework.binding.mapping.RequiredMapping; +import org.springframework.binding.method.MethodSignature; +import org.springframework.binding.method.Parameter; +import org.springframework.binding.method.Parameters; +import org.springframework.core.io.Resource; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.util.xml.DomUtils; +import org.springframework.webflow.action.ActionResultExposer; +import org.springframework.webflow.action.EvaluateAction; +import org.springframework.webflow.action.SetAction; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.engine.AnnotatedAction; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.FlowAttributeMapper; +import org.springframework.webflow.engine.FlowExecutionExceptionHandler; +import org.springframework.webflow.engine.FlowVariable; +import org.springframework.webflow.engine.TargetStateResolver; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.engine.ViewSelector; +import org.springframework.webflow.engine.builder.BaseFlowBuilder; +import org.springframework.webflow.engine.builder.FlowArtifactFactory; +import org.springframework.webflow.engine.builder.FlowBuilderException; +import org.springframework.webflow.engine.builder.FlowServiceLocator; +import org.springframework.webflow.engine.support.AttributeExpression; +import org.springframework.webflow.engine.support.BeanFactoryFlowVariable; +import org.springframework.webflow.engine.support.BooleanExpressionTransitionCriteria; +import org.springframework.webflow.engine.support.SimpleFlowVariable; +import org.springframework.webflow.engine.support.TransitionCriteriaChain; +import org.springframework.webflow.engine.support.TransitionExecutingStateExceptionHandler; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.util.ResourceHolder; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; + +/** + * Flow builder that builds flows as defined in an XML document. The XML document + * should adhere to the following format: + * + *
+ *     <?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-1.0.xsd">
+ *                                        
+ *         <!-- Define your states here -->
+ *                  
+ *     </flow>
+ * 
+ * + *

+ * Consult the webflow + * XML schema for more information on the XML-based flow definition format. + *

+ * This builder will setup a flow-local bean factory for the flow being + * constructed. That flow-local bean factory will be populated with XML bean + * definitions contained in files referenced using the "import" element. The + * flow-local bean factory will use the bean factory defing this flow builder as + * a parent. As such, the flow can access artifacts in either its flow-local + * bean factory or in the parent bean factory hierarchy, e.g. the bean factory + * of the dispatcher. + * + * @author Erwin Vervaet + * @author Keith Donald + */ +public class XmlFlowBuilder extends BaseFlowBuilder implements ResourceHolder { + + // recognized XML elements and attributes + + private static final String ID_ATTRIBUTE = "id"; + + private static final String IDREF_ATTRIBUTE = "idref"; + + private static final String BEAN_ATTRIBUTE = "bean"; + + private static final String FLOW_ELEMENT = "flow"; + + private static final String START_STATE_ELEMENT = "start-state"; + + private static final String ACTION_STATE_ELEMENT = "action-state"; + + private static final String ACTION_ELEMENT = "action"; + + private static final String NAME_ATTRIBUTE = "name"; + + private static final String METHOD_ATTRIBUTE = "method"; + + private static final String BEAN_ACTION_ELEMENT = "bean-action"; + + private static final String METHOD_ARGUMENTS_ELEMENT = "method-arguments"; + + private static final String ARGUMENT_ELEMENT = "argument"; + + private static final String EXPRESSION_ATTRIBUTE = "expression"; + + private static final String PARAMETER_TYPE_ATTRIBUTE = "parameter-type"; + + private static final String METHOD_RESULT_ELEMENT = "method-result"; + + private static final String EVALUATE_ACTION_ELEMENT = "evaluate-action"; + + private static final String SET_ELEMENT = "set"; + + private static final String ATTRIBUTE_ATTRIBUTE = "attribute"; + + private static final String EVALUATION_RESULT_ELEMENT = "evaluation-result"; + + private static final String DEFAULT_VALUE = "default"; + + private static final String VIEW_STATE_ELEMENT = "view-state"; + + private static final String VIEW_ATTRIBUTE = "view"; + + private static final String DECISION_STATE_ELEMENT = "decision-state"; + + private static final String IF_ELEMENT = "if"; + + private static final String TEST_ATTRIBUTE = "test"; + + private static final String THEN_ATTRIBUTE = "then"; + + private static final String ELSE_ATTRIBUTE = "else"; + + private static final String SUBFLOW_STATE_ELEMENT = "subflow-state"; + + private static final String FLOW_ATTRIBUTE = "flow"; + + private static final String ATTRIBUTE_MAPPER_ELEMENT = "attribute-mapper"; + + private static final String OUTPUT_MAPPER_ELEMENT = "output-mapper"; + + private static final String OUTPUT_ATTRIBUTE_ELEMENT = "output-attribute"; + + private static final String INPUT_MAPPER_ELEMENT = "input-mapper"; + + private static final String INPUT_ATTRIBUTE_ELEMENT = "input-attribute"; + + private static final String MAPPING_ELEMENT = "mapping"; + + private static final String SOURCE_ATTRIBUTE = "source"; + + private static final String TARGET_ATTRIBUTE = "target"; + + private static final String FROM_ATTRIBUTE = "from"; + + private static final String TO_ATTRIBUTE = "to"; + + private static final String REQUIRED_ATTRIBUTE = "required"; + + private static final String TARGET_COLLECTION_ATTRIBUTE = "target-collection"; + + private static final String END_STATE_ELEMENT = "end-state"; + + private static final String TRANSITION_ELEMENT = "transition"; + + private static final String GLOBAL_TRANSITIONS_ELEMENT = "global-transitions"; + + private static final String ON_ATTRIBUTE = "on"; + + private static final String ON_EXCEPTION_ATTRIBUTE = "on-exception"; + + private static final String ATTRIBUTE_ELEMENT = "attribute"; + + private static final String TYPE_ATTRIBUTE = "type"; + + private static final String VALUE_ELEMENT = "value"; + + private static final String VALUE_ATTRIBUTE = "value"; + + private static final String VAR_ELEMENT = "var"; + + private static final String SCOPE_ATTRIBUTE = "scope"; + + private static final String CLASS_ATTRIBUTE = "class"; + + private static final String START_ACTIONS_ELEMENT = "start-actions"; + + private static final String END_ACTIONS_ELEMENT = "end-actions"; + + private static final String ENTRY_ACTIONS_ELEMENT = "entry-actions"; + + private static final String RENDER_ACTIONS_ELEMENT = "render-actions"; + + private static final String EXIT_ACTIONS_ELEMENT = "exit-actions"; + + private static final String EXCEPTION_HANDLER_ELEMENT = "exception-handler"; + + private static final String INLINE_FLOW_ELEMENT = "inline-flow"; + + private static final String IMPORT_ELEMENT = "import"; + + private static final String RESOURCE_ATTRIBUTE = "resource"; + + /** + * The resource from which the document element being parsed was read. Used + * as a location for relative resource lookup. + */ + protected Resource location; + + /** + * A flow service locator local to this builder that first looks in a + * locally-managed Spring application context for services before searching + * the externally managed {@link #getFlowServiceLocator()}. + */ + private LocalFlowServiceLocator localFlowServiceLocator; + + /** + * The loader for loading the flow definition resource XML document. + */ + private DocumentLoader documentLoader = new DefaultDocumentLoader(); + + /** + * The in-memory document object model (DOM) of the XML Document read from + * the flow definition resource. + */ + private Document document; + + /** + * Create a new XML flow builder parsing the document at the specified + * location. + * @param location the location of the XML-based flow definition resource + */ + public XmlFlowBuilder(Resource location) { + setLocation(location); + } + + /** + * Create a new XML flow builder parsing the document at the specified + * location, using the provided service locator to access externally managed + * flow artifacts. + * @param location the location of the XML-based flow definition resource + * @param flowServiceLocator the locator for services needed by this builder + * to build its Flow + */ + public XmlFlowBuilder(Resource location, FlowServiceLocator flowServiceLocator) { + super(flowServiceLocator); + setLocation(location); + } + + /** + * Returns the resource from which the document element was loaded. This is + * used for location relative loading of other resources. + */ + public Resource getLocation() { + return location; + } + + /** + * Sets the resource from which the document element was loaded. This is + * used for location relative loading of other resources. + */ + public void setLocation(Resource location) { + Assert.notNull(location, "The resource location of the XML-based flow definition is required"); + this.location = location; + } + + /** + * Sets the loader that will load the XML-based flow definition document. + * Optional, defaults to {@link DefaultDocumentLoader}. + * @param documentLoader the document loader + */ + public void setDocumentLoader(DocumentLoader documentLoader) { + Assert.notNull(documentLoader, "The XML document loader is required"); + this.documentLoader = documentLoader; + } + + public String toString() { + return new ToStringCreator(this).append("location", location).toString(); + } + + // implementing FlowBuilder + + public void init(String id, AttributeMap attributes) throws FlowBuilderException { + localFlowServiceLocator = new LocalFlowServiceLocator(getFlowServiceLocator()); + try { + document = documentLoader.loadDocument(location); + } + catch (IOException e) { + throw new FlowBuilderException("Could not access the XML flow definition resource at " + location, e); + } + catch (ParserConfigurationException e) { + throw new FlowBuilderException("Could not configure the parser to parse the XML flow definition at " + + location, e); + } + catch (SAXException e) { + throw new FlowBuilderException("Could not parse the XML flow definition document at " + location, e); + } + setFlow(parseFlow(id, attributes, getDocumentElement())); + } + + public void buildVariables() throws FlowBuilderException { + parseAndAddFlowVariables(getDocumentElement(), getFlow()); + } + + public void buildInputMapper() throws FlowBuilderException { + getFlow().setInputMapper(parseInputMapper(getDocumentElement())); + } + + public void buildStartActions() throws FlowBuilderException { + parseAndAddStartActions(getDocumentElement(), getFlow()); + } + + public void buildInlineFlows() throws FlowBuilderException { + parseAndAddInlineFlowDefinitions(getDocumentElement(), getFlow()); + } + + public void buildStates() throws FlowBuilderException { + parseAndAddStateDefinitions(getDocumentElement(), getFlow()); + } + + public void buildGlobalTransitions() throws FlowBuilderException { + parseAndAddGlobalTransitions(getDocumentElement(), getFlow()); + } + + public void buildEndActions() throws FlowBuilderException { + parseAndAddEndActions(getDocumentElement(), getFlow()); + } + + public void buildOutputMapper() throws FlowBuilderException { + getFlow().setOutputMapper(parseOutputMapper(getDocumentElement())); + } + + public void buildExceptionHandlers() throws FlowBuilderException { + getFlow().getExceptionHandlerSet().addAll(parseExceptionHandlers(getDocumentElement())); + } + + public void dispose() { + super.dispose(); + localFlowServiceLocator.diposeOfAnyRegistries(); + document = null; + } + + // implementing ResourceHolder + + public Resource getResource() { + return location; + } + + // helpers + + /** + * Returns the DOM document parsed from the XML file. + */ + protected Document getDocument() { + return document; + } + + /** + * Returns the root document element. + */ + protected Element getDocumentElement() { + return document.getDocumentElement(); + } + + /** + * Returns the flow service locator local to this builder. + */ + protected FlowServiceLocator getLocalFlowServiceLocator() { + return localFlowServiceLocator; + } + + /** + * Returns the artifact factory of the flow service locator local + * to this builder. + */ + protected FlowArtifactFactory getFlowArtifactFactory() { + return getLocalFlowServiceLocator().getFlowArtifactFactory(); + } + + // utility (from Spring 2.x DomUtils) + + /** + * Utility method that returns the first child element identified by its + * name. + * @param ele the DOM element to analyze + * @param childEleName the child element name to look for + * @return the org.w3c.dom.Element instance, or + * null if none found + */ + protected Element getChildElementByTagName(Element ele, String childEleName) { + NodeList nl = ele.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element && nodeNameEquals(node, childEleName)) { + return (Element)node; + } + } + return null; + } + + /** + * Namespace-aware equals comparison. Returns true if either + * {@link Node#getLocalName} or {@link Node#getNodeName} equals + * desiredName, otherwise returns false. + */ + protected boolean nodeNameEquals(Node node, String desiredName) { + return desiredName.equals(node.getNodeName()) || desiredName.equals(node.getLocalName()); + } + + // internal parsing logic + + private Flow parseFlow(String id, AttributeMap attributes, Element flowElement) { + if (!isFlowElement(flowElement)) { + throw new IllegalStateException("This is not the '" + FLOW_ELEMENT + "' element"); + } + Flow flow = getFlowArtifactFactory().createFlow(id, parseAttributes(flowElement).union(attributes)); + initLocalServiceRegistry(flowElement, flow); + return flow; + } + + private boolean isFlowElement(Element flowElement) { + return nodeNameEquals(flowElement, FLOW_ELEMENT); + } + + private void initLocalServiceRegistry(Element flowElement, Flow flow) { + List importElements = DomUtils.getChildElementsByTagName(flowElement, IMPORT_ELEMENT); + Resource[] resources = new Resource[importElements.size()]; + for (int i = 0; i < importElements.size(); i++) { + Element importElement = (Element)importElements.get(i); + try { + resources[i] = getLocation().createRelative(importElement.getAttribute(RESOURCE_ATTRIBUTE)); + } + catch (IOException e) { + throw new FlowBuilderException("Could not access flow-relative artifact resource '" + + importElement.getAttribute(RESOURCE_ATTRIBUTE) + "'", e); + } + } + localFlowServiceLocator.push(new LocalFlowServiceRegistry(flow, resources)); + } + + private void destroyLocalServiceRegistry(Flow flow) { + localFlowServiceLocator.pop(); + } + + private void parseAndAddFlowVariables(Element flowElement, Flow flow) { + List varElements = DomUtils.getChildElementsByTagName(flowElement, VAR_ELEMENT); + for (Iterator it = varElements.iterator(); it.hasNext();) { + flow.addVariable(parseVariable((Element)it.next())); + } + } + + private FlowVariable parseVariable(Element element) { + ScopeType scope = parseScope(element, ScopeType.FLOW); + if (StringUtils.hasText(element.getAttribute(BEAN_ATTRIBUTE))) { + BeanFactory beanFactory = getLocalFlowServiceLocator().getBeanFactory(); + return new BeanFactoryFlowVariable(element.getAttribute(NAME_ATTRIBUTE), + element.getAttribute(BEAN_ATTRIBUTE), beanFactory, scope); + } + else { + if (StringUtils.hasText(element.getAttribute(CLASS_ATTRIBUTE))) { + Class variableClass = (Class)fromStringTo(Class.class).execute(element.getAttribute(CLASS_ATTRIBUTE)); + return new SimpleFlowVariable(element.getAttribute(NAME_ATTRIBUTE), variableClass, scope); + } + else { + BeanFactory beanFactory = getLocalFlowServiceLocator().getBeanFactory(); + return new BeanFactoryFlowVariable(element.getAttribute(NAME_ATTRIBUTE), null, beanFactory, scope); + } + } + } + + private void parseAndAddStartActions(Element element, Flow flow) { + Element startElement = getChildElementByTagName(element, START_ACTIONS_ELEMENT); + if (startElement != null) { + flow.getStartActionList().addAll(parseAnnotatedActions(startElement)); + } + } + + private void parseAndAddEndActions(Element element, Flow flow) { + Element endElement = getChildElementByTagName(element, END_ACTIONS_ELEMENT); + if (endElement != null) { + flow.getEndActionList().addAll(parseAnnotatedActions(endElement)); + } + } + + private void parseAndAddGlobalTransitions(Element element, Flow flow) { + Element globalTransitionsElement = getChildElementByTagName(element, GLOBAL_TRANSITIONS_ELEMENT); + if (globalTransitionsElement != null) { + flow.getGlobalTransitionSet().addAll(parseTransitions(globalTransitionsElement)); + } + } + + private void parseAndAddInlineFlowDefinitions(Element parentFlowElement, Flow flow) { + List inlineFlowElements = DomUtils.getChildElementsByTagName(parentFlowElement, INLINE_FLOW_ELEMENT); + for (Iterator it = inlineFlowElements.iterator(); it.hasNext();) { + Element inlineFlowElement = (Element)it.next(); + String inlineFlowId = inlineFlowElement.getAttribute(ID_ATTRIBUTE); + Element flowElement = getChildElementByTagName(inlineFlowElement, FLOW_ATTRIBUTE); + Flow inlineFlow = parseFlow(inlineFlowId, null, flowElement); + buildInlineFlow(flowElement, inlineFlow); + flow.addInlineFlow(inlineFlow); + } + } + + private void buildInlineFlow(Element flowElement, Flow inlineFlow) { + parseAndAddFlowVariables(flowElement, inlineFlow); + inlineFlow.setInputMapper(parseInputMapper(flowElement)); + parseAndAddStartActions(flowElement, inlineFlow); + parseAndAddInlineFlowDefinitions(flowElement, inlineFlow); + parseAndAddStateDefinitions(flowElement, inlineFlow); + parseAndAddGlobalTransitions(flowElement, inlineFlow); + parseAndAddEndActions(flowElement, inlineFlow); + inlineFlow.setOutputMapper(parseOutputMapper(flowElement)); + inlineFlow.getExceptionHandlerSet().addAll(parseExceptionHandlers(flowElement)); + + destroyLocalServiceRegistry(inlineFlow); + } + + private void parseAndAddStateDefinitions(Element flowElement, Flow flow) { + NodeList childNodeList = flowElement.getChildNodes(); + for (int i = 0; i < childNodeList.getLength(); i++) { + Node childNode = childNodeList.item(i); + if (childNode instanceof Element) { + Element stateElement = (Element)childNode; + if (nodeNameEquals(stateElement, ACTION_STATE_ELEMENT)) { + parseAndAddActionState(stateElement, flow); + } + else if (nodeNameEquals(stateElement, VIEW_STATE_ELEMENT)) { + parseAndAddViewState(stateElement, flow); + } + else if (nodeNameEquals(stateElement, DECISION_STATE_ELEMENT)) { + parseAndAddDecisionState(stateElement, flow); + } + else if (nodeNameEquals(stateElement, SUBFLOW_STATE_ELEMENT)) { + parseAndAddSubflowState(stateElement, flow); + } + else if (nodeNameEquals(stateElement, END_STATE_ELEMENT)) { + parseAndAddEndState(stateElement, flow); + } + } + } + parseAndSetStartState(flowElement, flow); + } + + private void parseAndSetStartState(Element element, Flow flow) { + String startStateId = getStartStateId(element); + flow.setStartState(startStateId); + } + + private String getStartStateId(Element element) { + Element startStateElement = getChildElementByTagName(element, START_STATE_ELEMENT); + return startStateElement.getAttribute(IDREF_ATTRIBUTE); + } + + private void parseAndAddActionState(Element element, Flow flow) { + getFlowArtifactFactory().createActionState(parseId(element), flow, parseEntryActions(element), + parseAnnotatedActions(element), parseTransitions(element), parseExceptionHandlers(element), + parseExitActions(element), parseAttributes(element)); + } + + private void parseAndAddViewState(Element element, Flow flow) { + getFlowArtifactFactory().createViewState(parseId(element), flow, parseEntryActions(element), + parseViewSelector(element), parseRenderActions(element), parseTransitions(element), + parseExceptionHandlers(element), parseExitActions(element), parseAttributes(element)); + } + + private void parseAndAddDecisionState(Element element, Flow flow) { + getFlowArtifactFactory().createDecisionState( + parseId(element), flow, parseEntryActions(element), parseIfs(element), + parseExceptionHandlers(element), parseExitActions(element), parseAttributes(element)); + } + + private void parseAndAddSubflowState(Element element, Flow flow) { + getFlowArtifactFactory().createSubflowState(parseId(element), flow, parseEntryActions(element), + parseSubflow(element), parseFlowAttributeMapper(element), parseTransitions(element), + parseExceptionHandlers(element), parseExitActions(element), parseAttributes(element)); + } + + private void parseAndAddEndState(Element element, Flow flow) { + getFlowArtifactFactory().createEndState(parseId(element), flow, parseEntryActions(element), + parseViewSelector(element), parseOutputMapper(element), parseExceptionHandlers(element), + parseAttributes(element)); + } + + private String parseId(Element element) { + return element.getAttribute(ID_ATTRIBUTE); + } + + private Action[] parseEntryActions(Element element) { + Element entryActionsElement = getChildElementByTagName(element, ENTRY_ACTIONS_ELEMENT); + if (entryActionsElement != null) { + return parseAnnotatedActions(entryActionsElement); + } + else { + return null; + } + } + + private Action[] parseRenderActions(Element element) { + Element renderActionsElement = getChildElementByTagName(element, RENDER_ACTIONS_ELEMENT); + if (renderActionsElement != null) { + return parseAnnotatedActions(renderActionsElement); + } + else { + return null; + } + } + + private Action[] parseExitActions(Element element) { + Element exitActionsElement = getChildElementByTagName(element, EXIT_ACTIONS_ELEMENT); + if (exitActionsElement != null) { + return parseAnnotatedActions(exitActionsElement); + } + else { + return null; + } + } + + private Transition[] parseTransitions(Element element) { + List transitions = new LinkedList(); + List transitionElements = DomUtils.getChildElementsByTagName(element, TRANSITION_ELEMENT); + for (Iterator it = transitionElements.iterator(); it.hasNext();) { + Element transitionElement = (Element)it.next(); + if (!StringUtils.hasText(transitionElement.getAttribute(ON_EXCEPTION_ATTRIBUTE))) { + transitions.add(parseTransition(transitionElement)); + } + } + return (Transition[])transitions.toArray(new Transition[transitions.size()]); + } + + private Transition parseTransition(Element element) { + TransitionCriteria matchingCriteria = (TransitionCriteria)fromStringTo(TransitionCriteria.class).execute( + element.getAttribute(ON_ATTRIBUTE)); + TargetStateResolver targetStateResolver = (TargetStateResolver)fromStringTo(TargetStateResolver.class).execute( + element.getAttribute(TO_ATTRIBUTE)); + TransitionCriteria executionCriteria = TransitionCriteriaChain.criteriaChainFor(parseAnnotatedActions(element)); + return getFlowArtifactFactory().createTransition(targetStateResolver, matchingCriteria, executionCriteria, + parseAttributes(element)); + } + + private ViewSelector parseViewSelector(Element element) { + String viewName = element.getAttribute(VIEW_ATTRIBUTE); + return (ViewSelector)fromStringTo(ViewSelector.class).execute(viewName); + } + + private Flow parseSubflow(Element element) { + return getLocalFlowServiceLocator().getSubflow(element.getAttribute(FLOW_ATTRIBUTE)); + } + + private AnnotatedAction[] parseAnnotatedActions(Element element) { + List actions = new LinkedList(); + NodeList childNodeList = element.getChildNodes(); + for (int i=0; i < childNodeList.getLength(); i++) { + Node childNode = childNodeList.item(i); + if (!(childNode instanceof Element)) { + continue; + } + + if (nodeNameEquals(childNode, ACTION_ELEMENT)) { + // parse standard action + actions.add(parseAnnotatedAction((Element)childNode)); + } + else if (nodeNameEquals(childNode, BEAN_ACTION_ELEMENT)) { + // parse bean invoking action + actions.add(parseAnnotatedBeanInvokingAction((Element)childNode)); + } + else if (nodeNameEquals(childNode, EVALUATE_ACTION_ELEMENT)) { + // parse evaluate action + actions.add(parseAnnotatedEvaluateAction((Element)childNode)); + } + else if (nodeNameEquals(childNode, SET_ELEMENT)) { + // parse set action + actions.add(parseAnnotatedSetAction((Element)childNode)); + } + } + return (AnnotatedAction[])actions.toArray(new AnnotatedAction[actions.size()]); + } + + private AnnotatedAction parseAnnotatedAction(Element element) { + AnnotatedAction annotated = new AnnotatedAction(parseAction(element)); + parseCommonProperties(element, annotated); + if (element.hasAttribute(METHOD_ATTRIBUTE)) { + annotated.setMethod(element.getAttribute(METHOD_ATTRIBUTE)); + } + return annotated; + } + + private Action parseAction(Element element) { + String actionId = element.getAttribute(BEAN_ATTRIBUTE); + return getLocalFlowServiceLocator().getAction(actionId); + } + + private AnnotatedAction parseCommonProperties(Element element, AnnotatedAction annotated) { + if (element.hasAttribute(NAME_ATTRIBUTE)) { + annotated.setName(element.getAttribute(NAME_ATTRIBUTE)); + } + annotated.getAttributeMap().putAll(parseAttributes(element)); + return annotated; + } + + private AnnotatedAction parseAnnotatedBeanInvokingAction(Element element) { + AnnotatedAction annotated = new AnnotatedAction(parseBeanInvokingAction(element)); + return parseCommonProperties(element, annotated); + } + + private Action parseBeanInvokingAction(Element element) { + String beanId = element.getAttribute(BEAN_ATTRIBUTE); + String methodName = element.getAttribute(METHOD_ATTRIBUTE); + Parameters parameters = parseMethodParameters(element); + MethodSignature methodSignature = new MethodSignature(methodName, parameters); + ActionResultExposer resultExposer = parseMethodResultExposer(element); + return getLocalFlowServiceLocator().getBeanInvokingActionFactory().createBeanInvokingAction(beanId, + getLocalFlowServiceLocator().getBeanFactory(), methodSignature, resultExposer, + getLocalFlowServiceLocator().getConversionService(), null); + } + + private Parameters parseMethodParameters(Element element) { + Element methodArgumentsElement = getChildElementByTagName(element, METHOD_ARGUMENTS_ELEMENT); + if (methodArgumentsElement == null) { + return Parameters.NONE; + } + Parameters parameters = new Parameters(); + Iterator it = DomUtils.getChildElementsByTagName(methodArgumentsElement, ARGUMENT_ELEMENT).iterator(); + while (it.hasNext()) { + Element argumentElement = (Element)it.next(); + Expression name = getLocalFlowServiceLocator().getExpressionParser() + .parseExpression(argumentElement.getAttribute(EXPRESSION_ATTRIBUTE)); + Class type = null; + if (argumentElement.hasAttribute(PARAMETER_TYPE_ATTRIBUTE)) { + type = (Class)fromStringTo(Class.class).execute(argumentElement.getAttribute(PARAMETER_TYPE_ATTRIBUTE)); + } + parameters.add(new Parameter(type, name)); + } + return parameters; + } + + private ActionResultExposer parseMethodResultExposer(Element element) { + Element resultElement = getChildElementByTagName(element, METHOD_RESULT_ELEMENT); + if (resultElement != null) { + return parseActionResultExposer(resultElement); + } + else { + return null; + } + } + + private ActionResultExposer parseActionResultExposer(Element element) { + String resultName = element.getAttribute(NAME_ATTRIBUTE); + return new ActionResultExposer(resultName, parseScope(element, ScopeType.REQUEST)); + } + + private AnnotatedAction parseAnnotatedEvaluateAction(Element element) { + AnnotatedAction annotated = new AnnotatedAction(parseEvaluateAction(element)); + return parseCommonProperties(element, annotated); + } + + private Action parseEvaluateAction(Element element) { + String expressionString = element.getAttribute(EXPRESSION_ATTRIBUTE); + Expression expression = getLocalFlowServiceLocator().getExpressionParser() + .parseExpression(expressionString); + return new EvaluateAction(expression, parseEvaluationResultExposer(element)); + } + + private ActionResultExposer parseEvaluationResultExposer(Element element) { + Element resultElement = getChildElementByTagName(element, EVALUATION_RESULT_ELEMENT); + if (resultElement != null) { + return parseActionResultExposer(resultElement); + } + else { + return null; + } + } + + private AnnotatedAction parseAnnotatedSetAction(Element element) { + AnnotatedAction annotated = new AnnotatedAction(parseSetAction(element)); + return parseCommonProperties(element, annotated); + } + + private Action parseSetAction(Element element) { + String attributeExpressionString = element.getAttribute(ATTRIBUTE_ATTRIBUTE); + SettableExpression attributeExpression = getLocalFlowServiceLocator().getExpressionParser() + .parseSettableExpression(attributeExpressionString); + Expression valueExpression = getLocalFlowServiceLocator().getExpressionParser() + .parseExpression(element.getAttribute(VALUE_ATTRIBUTE)); + return new SetAction(attributeExpression, parseScope(element, ScopeType.REQUEST), valueExpression); + } + + private ScopeType parseScope(Element element, ScopeType defaultValue) { + if (element.hasAttribute(SCOPE_ATTRIBUTE) && !element.getAttribute(SCOPE_ATTRIBUTE).equals(DEFAULT_VALUE)) { + return (ScopeType)fromStringTo(ScopeType.class).execute(element.getAttribute(SCOPE_ATTRIBUTE)); + } + else { + return defaultValue; + } + } + + private AttributeMap parseAttributes(Element element) { + LocalAttributeMap attributes = new LocalAttributeMap(); + List propertyElements = DomUtils.getChildElementsByTagName(element, ATTRIBUTE_ELEMENT); + for (int i = 0; i < propertyElements.size(); i++) { + parseAndSetAttribute((Element)propertyElements.get(i), attributes); + } + return attributes; + } + + private void parseAndSetAttribute(Element element, MutableAttributeMap attributes) { + String name = element.getAttribute(NAME_ATTRIBUTE); + String value = null; + if (element.hasAttribute(VALUE_ATTRIBUTE)) { + value = element.getAttribute(VALUE_ATTRIBUTE); + } + else { + List valueElements = DomUtils.getChildElementsByTagName(element, VALUE_ELEMENT); + Assert.state(valueElements.size() == 1, "A property value should be specified for property '" + name + "'"); + value = DomUtils.getTextValue((Element)valueElements.get(0)); + } + attributes.put(name, convertPropertyValue(element, value)); + } + + private Object convertPropertyValue(Element element, String stringValue) { + if (element.hasAttribute(TYPE_ATTRIBUTE)) { + ConversionExecutor executor = fromStringTo(element.getAttribute(TYPE_ATTRIBUTE)); + if (executor != null) { + // convert string value to instance of aliased type + return executor.execute(stringValue); + } + else { + Class targetClass = (Class)fromStringTo(Class.class).execute(element.getAttribute(TYPE_ATTRIBUTE)); + // convert string value to instance of target class + return fromStringTo(targetClass).execute(stringValue); + } + } + else { + return stringValue; + } + } + + private Transition[] parseIfs(Element element) { + List transitions = new LinkedList(); + List transitionElements = DomUtils.getChildElementsByTagName(element, IF_ELEMENT); + for (Iterator it = transitionElements.iterator(); it.hasNext();) { + transitions.addAll(Arrays.asList(parseIf((Element)it.next()))); + } + return (Transition[])transitions.toArray(new Transition[transitions.size()]); + } + + private Transition[] parseIf(Element element) { + Transition thenTransition = parseThen(element); + if (StringUtils.hasText(element.getAttribute(ELSE_ATTRIBUTE))) { + Transition elseTransition = parseElse(element); + return new Transition[] { thenTransition, elseTransition }; + } + else { + return new Transition[] { thenTransition }; + } + } + + private Transition parseThen(Element element) { + Expression expression = getLocalFlowServiceLocator().getExpressionParser() + .parseExpression(element.getAttribute(TEST_ATTRIBUTE)); + TransitionCriteria matchingCriteria = new BooleanExpressionTransitionCriteria(expression); + TargetStateResolver targetStateResolver = (TargetStateResolver)fromStringTo(TargetStateResolver.class).execute( + element.getAttribute(THEN_ATTRIBUTE)); + return getFlowArtifactFactory().createTransition(targetStateResolver, matchingCriteria, null, null); + } + + private Transition parseElse(Element element) { + TargetStateResolver targetStateResolver = (TargetStateResolver)fromStringTo(TargetStateResolver.class).execute( + element.getAttribute(ELSE_ATTRIBUTE)); + return getFlowArtifactFactory().createTransition(targetStateResolver, null, null, null); + } + + private FlowAttributeMapper parseFlowAttributeMapper(Element element) { + Element mapperElement = getChildElementByTagName(element, ATTRIBUTE_MAPPER_ELEMENT); + if (mapperElement == null) { + return null; + } + if (StringUtils.hasText(mapperElement.getAttribute(BEAN_ATTRIBUTE))) { + return getLocalFlowServiceLocator().getAttributeMapper(mapperElement.getAttribute(BEAN_ATTRIBUTE)); + } + else { + return new ImmutableFlowAttributeMapper(parseInputMapper(mapperElement), parseOutputMapper(mapperElement)); + } + } + + private AttributeMapper parseInputMapper(Element element) { + Element mapperElement = getChildElementByTagName(element, INPUT_MAPPER_ELEMENT); + if (mapperElement != null) { + DefaultAttributeMapper mapper = new DefaultAttributeMapper(); + parseSimpleAttributeMappings(mapper, + DomUtils.getChildElementsByTagName(mapperElement, INPUT_ATTRIBUTE_ELEMENT)); + parseMappings(mapper, mapperElement); + return mapper; + } + else { + return null; + } + } + + private AttributeMapper parseOutputMapper(Element element) { + Element mapperElement = getChildElementByTagName(element, OUTPUT_MAPPER_ELEMENT); + if (mapperElement != null) { + DefaultAttributeMapper mapper = new DefaultAttributeMapper(); + parseSimpleAttributeMappings(mapper, + DomUtils.getChildElementsByTagName(mapperElement, OUTPUT_ATTRIBUTE_ELEMENT)); + parseMappings(mapper, mapperElement); + return mapper; + } + else { + return null; + } + } + + private void parseMappings(DefaultAttributeMapper mapper, Element element) { + ExpressionParser parser = getLocalFlowServiceLocator().getExpressionParser(); + List mappingElements = DomUtils.getChildElementsByTagName(element, MAPPING_ELEMENT); + for (Iterator it = mappingElements.iterator(); it.hasNext();) { + Element mappingElement = (Element)it.next(); + Expression source = parser.parseExpression(mappingElement.getAttribute(SOURCE_ATTRIBUTE)); + SettableExpression target = null; + if (StringUtils.hasText(mappingElement.getAttribute(TARGET_ATTRIBUTE))) { + target = parser.parseSettableExpression(mappingElement.getAttribute(TARGET_ATTRIBUTE)); + } + else if (StringUtils.hasText(mappingElement.getAttribute(TARGET_COLLECTION_ATTRIBUTE))) { + target = new CollectionAddingExpression( + parser.parseSettableExpression(mappingElement.getAttribute(TARGET_COLLECTION_ATTRIBUTE))); + } + if (getRequired(mappingElement, false)) { + mapper.addMapping(new RequiredMapping(source, target, parseTypeConverter(mappingElement))); + } + else { + mapper.addMapping(new Mapping(source, target, parseTypeConverter(mappingElement))); + } + } + } + + private void parseSimpleAttributeMappings(DefaultAttributeMapper mapper, List elements) { + ExpressionParser parser = getLocalFlowServiceLocator().getExpressionParser(); + for (Iterator it = elements.iterator(); it.hasNext();) { + Element element = (Element)it.next(); + SettableExpression attribute = parser.parseSettableExpression(element.getAttribute(NAME_ATTRIBUTE)); + SettableExpression expression = new AttributeExpression(attribute, parseScope(element, ScopeType.FLOW)); + if (getRequired(element, false)) { + mapper.addMapping(new RequiredMapping(expression, expression, null)); + } + else { + mapper.addMapping(new Mapping(expression, expression, null)); + } + } + } + + private boolean getRequired(Element element, boolean defaultValue) { + if (StringUtils.hasText(element.getAttribute(REQUIRED_ATTRIBUTE))) { + return ((Boolean)fromStringTo(Boolean.class).execute(element.getAttribute(REQUIRED_ATTRIBUTE))) + .booleanValue(); + } + else { + return defaultValue; + } + } + + private ConversionExecutor parseTypeConverter(Element element) { + String from = element.getAttribute(FROM_ATTRIBUTE); + String to = element.getAttribute(TO_ATTRIBUTE); + if (StringUtils.hasText(from)) { + if (StringUtils.hasText(to)) { + ConversionService service = getLocalFlowServiceLocator().getConversionService(); + return service.getConversionExecutor(service.getClassByAlias(from), service.getClassByAlias(to)); + } + else { + throw new IllegalArgumentException("Use of the 'from' attribute requires use of the 'to' attribute"); + } + } + else { + Assert.isTrue(!StringUtils.hasText(to), "Use of the 'to' attribute requires use of the 'from' attribute"); + } + return null; + } + + private FlowExecutionExceptionHandler[] parseExceptionHandlers(Element element) { + FlowExecutionExceptionHandler[] transitionExecutingHandlers = parseTransitionExecutingExceptionHandlers(element); + FlowExecutionExceptionHandler[] customHandlers = parseCustomExceptionHandlers(element); + FlowExecutionExceptionHandler[] exceptionHandlers = + new FlowExecutionExceptionHandler[transitionExecutingHandlers.length + customHandlers.length]; + System.arraycopy(transitionExecutingHandlers, 0, exceptionHandlers, 0, transitionExecutingHandlers.length); + System.arraycopy(customHandlers, 0, exceptionHandlers, transitionExecutingHandlers.length, + customHandlers.length); + return exceptionHandlers; + } + + private FlowExecutionExceptionHandler[] parseTransitionExecutingExceptionHandlers(Element element) { + List transitionElements = Collections.EMPTY_LIST; + if (isFlowElement(element)) { + Element globalTransitionsElement = getChildElementByTagName(element, GLOBAL_TRANSITIONS_ELEMENT); + if (globalTransitionsElement != null) { + transitionElements = DomUtils.getChildElementsByTagName(globalTransitionsElement, TRANSITION_ELEMENT); + } + } + else { + transitionElements = DomUtils.getChildElementsByTagName(element, TRANSITION_ELEMENT); + } + List exceptionHandlers = new LinkedList(); + for (Iterator it = transitionElements.iterator(); it.hasNext();) { + Element transitionElement = (Element)it.next(); + if (StringUtils.hasText(transitionElement.getAttribute(ON_EXCEPTION_ATTRIBUTE))) { + exceptionHandlers.add(parseTransitionExecutingExceptionHandler(transitionElement)); + } + } + return (FlowExecutionExceptionHandler[])exceptionHandlers + .toArray(new FlowExecutionExceptionHandler[exceptionHandlers.size()]); + } + + private FlowExecutionExceptionHandler parseTransitionExecutingExceptionHandler(Element element) { + TransitionExecutingStateExceptionHandler handler = new TransitionExecutingStateExceptionHandler(); + Class exceptionClass = (Class)fromStringTo(Class.class).execute(element.getAttribute(ON_EXCEPTION_ATTRIBUTE)); + handler.add(exceptionClass, element.getAttribute(TO_ATTRIBUTE)); + handler.getActionList().addAll(parseAnnotatedActions(element)); + return handler; + } + + private FlowExecutionExceptionHandler[] parseCustomExceptionHandlers(Element element) { + List exceptionHandlers = new LinkedList(); + List handlerElements = DomUtils.getChildElementsByTagName(element, EXCEPTION_HANDLER_ELEMENT); + for (int i = 0; i < handlerElements.size(); i++) { + Element handlerElement = (Element)handlerElements.get(i); + exceptionHandlers.add(parseCustomExceptionHandler(handlerElement)); + } + return (FlowExecutionExceptionHandler[])exceptionHandlers + .toArray(new FlowExecutionExceptionHandler[exceptionHandlers.size()]); + } + + private FlowExecutionExceptionHandler parseCustomExceptionHandler(Element element) { + return getLocalFlowServiceLocator().getExceptionHandler(element.getAttribute(BEAN_ATTRIBUTE)); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowRegistrar.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowRegistrar.java new file mode 100644 index 00000000..ff92105b --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowRegistrar.java @@ -0,0 +1,147 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder.xml; + +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.webflow.definition.registry.ExternalizedFlowDefinitionRegistrar; +import org.springframework.webflow.definition.registry.FlowDefinitionHolder; +import org.springframework.webflow.definition.registry.FlowDefinitionResource; +import org.springframework.webflow.engine.builder.FlowAssembler; +import org.springframework.webflow.engine.builder.FlowBuilder; +import org.springframework.webflow.engine.builder.FlowServiceLocator; +import org.springframework.webflow.engine.builder.RefreshableFlowDefinitionHolder; + +/** + * A flow definition registrar that populates a flow definition registry with + * flow definitions defined in externalized XML resources. Typically used in + * conjunction with a {@link XmlFlowRegistryFactoryBean} but may also be used + * standalone in programmatic fashion. + *

+ * By default, a flow definition registered by this registrar will be assigned a + * registry identifier equal to the filename of the underlying definition + * resource, minus the filename extension. For example, a XML-based flow + * definition defined in the file "flow1.xml" will be identified as "flow1" when + * registered in a registry. + *

+ * Programmatic usage example: + * + *

+ *     BeanFactory beanFactory = ...
+ *     FlowDefinitionRegistry registry = new FlowDefinitionRegistryImpl();
+ *     FlowServiceLocator flowServiceLocator =
+ *         new DefaultFlowServiceLocator(registry, beanFactory);
+ *     XmlFlowRegistrar registrar = new XmlFlowRegistrar(flowServiceLocator);
+ *     File parent = new File("src/webapp/WEB-INF");
+ *     registrar.addLocation(new FileSystemResource(new File(parent, "flow1.xml"));
+ *     registrar.addLocation(new FileSystemResource(new File(parent, "flow2.xml"));
+ *     registrar.registerFlowDefinitions(registry);
+ * 
+ * + * @author Keith Donald + */ +public class XmlFlowRegistrar extends ExternalizedFlowDefinitionRegistrar { + + /** + * The xml file suffix constant. + */ + private static final String XML_SUFFIX = ".xml"; + + /** + * The locator of services needed by flow definitions. + */ + private FlowServiceLocator flowServiceLocator; + + /** + * The loader of XML-based flow definition documents. + */ + private DocumentLoader documentLoader; + + /** + * Creates a new XML flow registrar. Protected constructor - if used, make + * sure the required {@link #flowServiceLocator} reference is set. + */ + protected XmlFlowRegistrar() { + } + + /** + * Creates a new XML flow registrar. + * @param flowServiceLocator the locator needed to support flow definition + * assembly + */ + public XmlFlowRegistrar(FlowServiceLocator flowServiceLocator) { + setFlowServiceLocator(flowServiceLocator); + } + + /** + * Sets the flow service locator. + * @param flowServiceLocator the flow service locator (may not be null) + */ + protected void setFlowServiceLocator(FlowServiceLocator flowServiceLocator) { + Assert.notNull(flowServiceLocator, "The flow service locator is required"); + this.flowServiceLocator = flowServiceLocator; + } + + /** + * Returns the flow service locator. + */ + protected FlowServiceLocator getFlowServiceLocator() { + return flowServiceLocator; + } + + /** + * Sets the loader to load XML-based flow definition documents during flow + * definition assembly. Allows for customization over how documents are + * loaded. Optional. + * @param documentLoader the document loader + */ + public void setDocumentLoader(DocumentLoader documentLoader) { + this.documentLoader = documentLoader; + } + + /** + * Returns the loader of XML-based flow definition documents. + */ + public DocumentLoader getDocumentLoader() { + return documentLoader; + } + + protected boolean isFlowDefinitionResource(Resource resource) { + return resource.getFilename().endsWith(XML_SUFFIX); + } + + protected FlowDefinitionHolder createFlowDefinitionHolder(FlowDefinitionResource resource) { + FlowBuilder builder = createFlowBuilder(resource.getLocation()); + FlowAssembler assembler = new FlowAssembler(resource.getId(), resource.getAttributes(), builder); + return new RefreshableFlowDefinitionHolder(assembler); + } + + // hook methods + + /** + * Factory method that creates and fully initializes the XML-based flow + * definition builder. + * @param location the xml-based resource + * @return the builder to build the flow definition from the resource. + */ + protected FlowBuilder createFlowBuilder(Resource location) { + XmlFlowBuilder builder = new XmlFlowBuilder(location, getFlowServiceLocator()); + if (documentLoader != null) { + builder.setDocumentLoader(documentLoader); + } + return builder; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowRegistryFactoryBean.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowRegistryFactoryBean.java new file mode 100644 index 00000000..99f12f51 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/XmlFlowRegistryFactoryBean.java @@ -0,0 +1,188 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder.xml; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.springframework.core.io.Resource; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.definition.registry.FlowDefinitionResource; +import org.springframework.webflow.engine.builder.AbstractFlowBuildingFlowRegistryFactoryBean; +import org.springframework.webflow.engine.builder.DefaultFlowServiceLocator; +import org.springframework.webflow.engine.builder.FlowServiceLocator; + +/** + * A factory bean that produces a populated flow registry using a + * {@link XmlFlowRegistrar}. This is the simplest implementation to use when + * using a Spring BeanFactory to deploy an explicit registry of XML-based Flow + * definitions for execution. + *

+ * By default, a configured flow definition will be assigned a registry + * identifier equal to the filename of the underlying definition resource, minus + * the filename extension. For example, a XML-based flow definition defined in + * the file flow1.xml will be identified as flow1 + * in the registry created by this factory bean. + *

+ * This class is also BeanFactoryAware and when used with Spring + * will automatically create a configured {@link DefaultFlowServiceLocator} for + * loading Flow artifacts like Actions from the Spring bean factory during the + * Flow registration process. + *

+ * This class is also ResourceLoaderAware; when an instance is + * created by a Spring BeanFactory the factory will automatically configure the + * XmlFlowRegistrar with a context-relative resource loader for accessing other + * resources during Flow assembly. + * + * Usage example: + * + *

+ *     <bean id="flowRegistry" class="org.springframework.webflow.engine.builder.registry.XmlFlowRegistryFactoryBean">
+ *         <property name="flowLocations"> value="/WEB-INF/flows/*-flow.xml"/> 
+ *     </bean>
+ * 
+ * + * @author Keith Donald + */ +public class XmlFlowRegistryFactoryBean extends AbstractFlowBuildingFlowRegistryFactoryBean { + + /** + * The flow registrar that will perform the definition registrations. + */ + private XmlFlowRegistrar flowRegistrar = new XmlFlowRegistrar(); + + /** + * Temporary holder for flow definitions configured using a property map. + */ + private Properties flowDefinitions; + + /** + * Returns the configured externalized XML flow registrar. + */ + protected XmlFlowRegistrar getXmlFlowRegistrar() { + return flowRegistrar; + } + + /** + * Sets the locations (resource file paths) pointing to XML-based flow + * definitions. + *

+ * When configuring as a Spring bean definition, ANT-style resource + * patterns/wildcards are also supported, taking advantage of Spring's built + * in ResourceArrayPropertyEditor machinery. + *

+ * For example: + * + *

+	 *     <bean id="flowRegistry" class="org.springframework.webflow.engine.builder.xml.XmlFlowRegistryFactoryBean">
+	 *         <property name="flowLocations"> value="/WEB-INF/flows/*-flow.xml"/> 
+	 *     </bean>
+	 * 
+ * + * Another example: + * + *
+	 *    <bean id="flowRegistry" class="org.springframework.webflow.engine.builder.xml.XmlFlowRegistryFactoryBean">
+	 *          <property name="flowLocations"> value="classpath*:/example/flows/*-flow.xml"/> 
+	 *    </bean>
+	 * 
+ * + * Flows registered from this set will be automatically assigned an id based + * on the filename of the matched XML resource. + * @param locations the resource locations + */ + public void setFlowLocations(Resource[] locations) { + getXmlFlowRegistrar().setLocations(locations); + } + + /** + * Convenience method for setting externalized flow definitions + * from a java.util.Properties map. Allows for more control + * over the definition, including which flowId is assigned. + *

+ * Each property key is the flowId and each property value is + * the string encoded location of the externalized flow definition resource. + *

+ * Here is the exact format: + * + *

+	 *      flow id=resource
+	 * 
+ * + * For example: + * + *
+	 *     <bean id="flowRegistry" class="org.springframework.webflow.engine.builder.xml.XmlFlowRegistryFactoryBean">
+	 *         <property name="flowDefinitions">
+	 *             <value>
+	 *                 searchFlow=/WEB-INF/flows/search-flow.xml
+	 *                 detailFlow=/WEB-INF/flows/detail-flow.xml
+	 *             </value>
+	 *         </property>
+	 *     </bean>
+	 * 
+ * @param flowDefinitions the flow definitions, defined within a properties + * map + */ + public void setFlowDefinitions(Properties flowDefinitions) { + this.flowDefinitions = flowDefinitions; + } + + /** + * Sets the loader to load XML-based flow definition documents during flow + * definition assembly. Allows for customization over how flow definition + * documents are loaded. Optional. + * @param documentLoader the document loader + */ + public void setDocumentLoader(DocumentLoader documentLoader) { + getXmlFlowRegistrar().setDocumentLoader(documentLoader); + } + + protected void init(FlowServiceLocator flowServiceLocator) { + // simply wire in the locator to the registrar + flowRegistrar.setFlowServiceLocator(flowServiceLocator); + } + + protected void doPopulate(FlowDefinitionRegistry registry) { + addFlowDefinitionsFromProperties(); + getXmlFlowRegistrar().registerFlowDefinitions(registry); + } + + /** + * Add flow definitions configured using a property map to + * the flow definition registrar. + */ + private void addFlowDefinitionsFromProperties() { + if (flowDefinitions != null && flowDefinitions.size() > 0) { + List flows = new ArrayList(flowDefinitions.size()); + Iterator it = flowDefinitions.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = (Map.Entry)it.next(); + String flowId = (String)entry.getKey(); + String location = (String)entry.getValue(); + Resource resource = getFlowServiceLocator().getResourceLoader().getResource(location); + flows.add(new FlowDefinitionResource(flowId, resource)); + } + getXmlFlowRegistrar().addResources( + (FlowDefinitionResource[])flows.toArray(new FlowDefinitionResource[flows.size()])); + // cleanup + flowDefinitions = null; + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/package.html b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/package.html new file mode 100644 index 00000000..714eaedd --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/package.html @@ -0,0 +1,7 @@ + + +

+The XML-based flow builder implementation. +

+ + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/spring-webflow-1.0.xsd b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/spring-webflow-1.0.xsd new file mode 100644 index 00000000..4f379f97 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/builder/xml/spring-webflow-1.0.xsd @@ -0,0 +1,1991 @@ + + + + + + +This Schema defines the Spring Web Flow (SWF) XML syntax. +
+The root "flow" element of this document defines exactly one flow definition. +A flow definition is a blueprint for an executable task that involves a +single user (aka conversation or dialog). +
+A flow is composed of one or more states that form the steps of the flow. +Each state executes behavior when entered. What behavior is executed is a +function of the state's type. Core state types include view states, +action states, subflow states, decision states, and end states. +
+Each flow definition must specify exactly one start state. +Events that occur in transitionable states drive state transitions. +]]> +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +The action referenced by this element must implement the org.springframework.webflow.execution.Action +interface. The action may be a MultiAction and if so the 'method' attribute can be used to +specify the target method to invoke. +
+An action may be annotated with attributes that can be used to affect the action's execution. +]]> +
+
+
+ + + + +The bean to invoke is typically an arbitrary "POJO" (Plain Old Java Object) with no +dependency on Spring Web Flow. +
+Use this element when the logic to invoke is encapsulated within an object you define. +This element can be used to invoke *any* public method on any bean. +
+If the target method accepts arguments they may be specified in order by using the +'method-arguments' sub-element. +
+If the target method returns a value that value may be exposed to the flow using +the 'method-result' sub-element. +
+For example: +
+	<bean-action bean="orderClerk" method="placeOrder">
+		<method-arguments>
+			<argument expression="flowScope.order"/>
+		</method-arguments>
+		<method-result name="orderConfirmation"/>
+	</bean-action>
+
+The above example instructs this flow to invoke the "placeOrder" method on the "orderClerk" bean, +passing the value of "flowScope.order" as the method argument. After method invocation the +method return value is exposed in the default scope under the name "orderConfirmation". +]]> +
+
+
+ + + + +Use this element when the logic to invoke is encapsulated within an object inside +the flow request context. This element can be used to invoke *any* public method on +a flow-managed bean. +
+For example: +
+	<evaluate-action expression="flowScope.interview.nextQuestion()">
+	    <evaluation-result name="question"/>
+	</evaluate-action>
+
+The above example instructs this flow to invoke the "nextQuestion" method on the "interview" bean +in flow scope. After method invocation the method return value is exposed in the default +scope under the name "question". +]]> +
+
+
+ + + + +For example: +
+	<set attribute="fileUploaded" scope="flash" value="true"/>
+
+The above example instructs this flow to set the "fileUploaded" attribute in "flash scope" to "true". +This action always returns a "success" event unless an exception is thrown. +]]> +
+
+
+
+
+ + + + + + +A flow may also exhibit the following characteristics: +
    +
  • Be annotated with attributes that define descriptive properties that may affect flow execution. +(See the <attribute/> element) + +
  • Instantiate a set of application variables when started. +(See the <var/> element) + +
  • Map input provided by callers that start it +(See the <input-mapper/> element) + +
  • Return output to callers that end it. +(See the <output-mapper/> element) + +
  • Execute custom behaviors at start time and end time. +(See the <start-actions/> and <end-actions/> elements) + +
  • Define transitions shared by all states. +(See the <global-transitions/> element) + +
  • Handle exceptions thrown by its states during execution. +(See the <exception-handler/> element) + +
  • Import one or more local bean definition files defining custom flow artifacts +(such as actions, exception handlers, view selectors, transition criteria, etc). +(See the <import/> element) + +
  • Finally, a flow may nest one or more other flows within this document to +use as subflows, referred to as 'inline flows'. +(See the <inline-flow/> element) + +
+]]> +
+
+ + + + + + + + + + + + + + + + + + + + +
  • The 'source' of each mapping is a Map containing all the input provided by the caller +that launched this flow. +
  • The 'target' of each mapping is this flow execution's RequestContext, exposing access to +data structures such as 'flowScope'. + +
    +For example: +
    +    <input-mapper>
    +        <input-attribute name="id"/>
    +    </input-mapper>
    +
    +... maps the value of "id" input attribute to the "id" attribute in this flow's scope. +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +Once paused a view-state may be 'refreshed' by the user, for example, when the +browser refresh button is clicked. A refresh causes the response to be reissued, +at which point control goes back to the user. +
    +A view state may be configured with one or more <render-action/> elements. Render +actions are executed before the view is rendered. Such actions are often +idempotent and execute without side effects. +
    +A view state is a transitionable state. A view state transition is triggered by a +user input event. +]]> +
    +
    +
    + + + + +A decision state is a transitionable state. A decision state transition can be triggered by +evaluating a boolean expression against the flow execution request context. To +define expressions, use the 'if' element. +
    +Examples: +
    +A simple boolean expression test, using the convenient 'if' element: +
    +    <decision-state id="requiresShipping">
    +	    <if test="flowScope.sale.shipping" then="enterShippingDetails" else="processSale"/>
    +    </decision-state>
    +
    +]]> +
    +
    +
    + + + + +A subflow state is a transitionable state. A state transition is triggered by a +subflow result event, which describes the logical subflow outcome that occurred. Typically the +criteria for this transition is the id of the subflow end state that was entered. +
    +While the subflow is active, this flow is suspended waiting for the subflow to complete execution. +When the subflow completes execution by reaching an end state, this state is expected +to respond to the result of that execution. The result of subflow execution, the end state +that was reached, should be used as grounds for a transition out of this state. +]]> +
    +
    +
    + + + + +A end state is not transitionable--there are never transitions out of an end state. +When an end-state is entered, an instance of this flow is terminated. +
    +When this flow terminates, if it was acting as the "root" or top-level flow the entire +execution (conversation) is terminated. If this flow was acting as a subflow the subflow +session ends and the calling parent session resumes. To resume, the parent session +responds to the result of the subflow, typically by reasoning on the id of the end +state that was reached. +]]> +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + +For the output mapper the following mapping characteristics apply: +
      +
    • The 'source' of each mapping is this flow execution's RequestContext, exposing access to +internal data structures such as 'flowScope'. +
    • The 'target' of each mapping is the flow output map that will contain the output returned to the +caller that launched this flow. +
    +
    +For example: +
    +    <output-mapper>
    +        <mapping source="flowScope.myFlowAttribute" target="clientOutputAttribute"/>
    +    </output-mapper>
    +
    +... maps the value of "myFlowAttribute" in flow scope to "clientOutputAttribute" in this flow's output map. +]]> +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +When specified without the 'class' or 'bean' attributes, this name is also used as the bean name +of a non-singleton bean in the configured Bean Factory to use as the initial variable value. +]]> + + + + + + + +
  • request - The variable goes out of scope when a call to start this flow completes. +
  • flash - The variable goes out of scope when the next user event is signaled. +
  • flow - The variable goes out of scope when this flow session ends. +
  • conversation - The variable goes out of scope when the overall conversation governing this flow ends. + +
    +If not specified the default scope type is used ('flow' by default). +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +A transition defines a supported path through the flow. Transitions may be annotated with attributes +and may execute one or more actions before executing. +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +If the referenced bean implements the org.springframework.webflow.execution.Action interface it is +retrieved from the factory and used as is. If the bean is not an Action an exception is thrown. +
    +This is similar to the <ref bean="myBean"/> notation of the Spring beans DTD. +]]> +
    +
    +
    + + + + +This can be used to execute actions in an ordered chain, where the flow responds +to the the last action result in the chain: +
    +    <action-state id="setupForm">
    +        <action name="setupForm" bean="formAction" method="setupForm"/>
    +        <action name="loadReferenceData" bean="formAction" method="loadReferenceData"/>
    +        <transition on="loadReferenceData.success" to="displayForm">
    +    </action-state>
    +
    +... will execute 'setupForm' followed by 'loadRefenceData', then transition the flow to +the 'displayForm' state on a successful 'loadReferenceData' invocation. +
    +An action with a name is often referred to as a "named action". +]]> +
    +
    +
    + + + + +Use this attribute when the action is a "multi action" extending +org.springframework.webflow.action.MultiAction. The value should be +name of the method to invoke on the multi-action instance. +The method's implementation must have the following signature: +
    +    public Event <methodName>(RequestContext context);
    +
    +As an example: +
    +	<action bean="formAction" method="setupForm"/>
    +
    + ... might invoke: +
    +	public class FormAction extends MultiAction {
    +		public Event setupForm(RequestContext context) {
    +			return success();
    +		}
    +	}
    +
    +]]> +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +It is expected the referenced bean be a simple POJO that does not implement the Spring Web Flow +Action interface. The method to invoke, specified using the 'method' attribute, +will be adapted to the Action interface automatically. +
    +This is similar to the <ref bean="myBean"/> notation of the Spring beans DTD. +]]> +
    +
    +
    + + + + +This can be used to execute actions in an ordered chain, where the flow responds +to the the last action result in the chain: +
    +    <action-state id="setupForm">
    +        <action name="setupForm" bean="formAction" method="setupForm"/>
    +        <action name="loadReferenceData" bean="formAction" method="loadReferenceData"/>
    +        <transition on="loadReferenceData.success" to="displayForm">
    +    </action-state>
    +
    +... will execute 'setupForm' followed by 'loadRefenceData', then transition the flow to +the 'displayForm' state on a successful 'loadReferenceData' invocation. +
    +An action with a name is often referred to as a "named action". +]]> +
    +
    +
    + + + + +If the method has parameters the arguments to those parameters should be specified using +the 'method-arguments' element. +
    +If the method returns a value that should be exposed to this flow, the 'method-result' element +should be specified. +]]> +
    +
    +
    +
    +
    + + + + + + + + +Typically used to pass a value from a flow scope type into this bean method as an argument. +
    +Examples: +
    +	<argument expression="flowScope.order"/>
    +
    +... passes in the value of the 'order' attribute in flow scope. +
    +	<argument expression="'a constant'"/>
    +
    +... passes in the 'a constant' literal. +]]> +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  • request - The result goes out of scope when the call into this flow that invoked this method completes. +
  • flash - The result goes out of scope when the next user event is signaled. +
  • flow - The result goes out of scope when this local flow session ends. +
  • conversation - The result goes out of scope when the overall conversation governing this flow execution ends. + +
    +If not specified the default scope type is used ('request' by default). +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +This can be used to execute actions in an ordered chain, where the flow responds +to the the last action result in the chain: +
    +    <action-state id="setupForm">
    +        <action name="setupForm" bean="formAction" method="setupForm"/>
    +        <action name="loadReferenceData" bean="formAction" method="loadReferenceData"/>
    +        <transition on="loadReferenceData.success" to="displayForm">
    +    </action-state>
    +
    +... will execute 'setupForm' followed by 'loadRefenceData', then transition the flow to +the 'displayForm' state on a successful 'loadReferenceData' invocation. +
    +An action with a name is often referred to as a "named action". +]]> +
    +
    +
    +
    +
    + + + + + + + + + + + + + + +
  • request - The result goes out of scope when the call into this flow that evaluated this expression completes. +
  • flash - The result goes out of scope when the next user event is signaled. +
  • flow - The result goes out of scope when this local flow session ends. +
  • conversation - The result goes out of scope when the overall conversation governing this flow execution ends. + +
    +If not specified the default scope type is used ('request' by default). +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  • request - The attribute goes out of scope when the call into this flow that sets the attribute completes. +
  • flash - The attribute goes out of scope when the next user event is signaled. +
  • flow - The attribute goes out of scope when this local flow session ends. +
  • conversation - The attribute goes out of scope when the overall conversation governing this flow execution ends. + +
    +If not specified the default scope type is used ('request' by default). +]]> + + + + + + + + + + + + + + +This can be used to execute actions in an ordered chain, where the flow responds +to the the last action result in the chain: +
    +    <action-state id="setupForm">
    +        <action name="setupForm" bean="formAction" method="setupForm"/>
    +        <action name="loadReferenceData" bean="formAction" method="loadReferenceData"/>
    +        <transition on="loadReferenceData.success" to="displayForm">
    +    </action-state>
    +
    +... will execute 'setupForm' followed by 'loadRefenceData', then transition the flow to +the 'displayForm' state on a successful 'loadReferenceData' invocation. +
    +An action with a name is often referred to as a "named action". +]]> +
    +
    +
    + + + + + + + + + + + + + + + + + + + +The most basic value is a static event id: +
    +	<transition on="submit" to="state"/>
    +
    +... which reads "on an occurrence of the 'submit' event transition to 'state'" +
    +Sophisticated transitional expressions are also supported when enclosed within ${brackets}: +
    +	<transition on="${#result == 'submit' &;amp;& flowScope.attribute == 'foo'}" to="state"/>
    +
    +Custom transition criteria implementations can be referenced by id: +
    +	<transition on="bean:myCustomCriteria" to="state"/>
    +
    +The exact interpretation of this attribute value depends on the TextToTransitionCriteria +converter that is installed. +]]> +
    +
    +
    + + + + +The value must be a fully-qualified Exception class name (e.g. example.booking.ItineraryExpiredException). +When an exception is thrown, superclasses of the configured exception class match by default. +
    +Use of this attribute results in an exception handler being attached to the object associated +with this transition definition. Use this attribute or the 'on' attribute, not both. +]]> +
    +
    +
    + + + + +The value of this attribute may be a static state identifier (e.g. to="displayForm") +or an expression to be evaluated at runtime against the request context +(e.g. to="${flowScope.previousViewState}"). Custom target state resolvers implementations +can be referenced by id (e.g. to="bean:myCustomTargetStateResolver"). The +exact interpretation of this attribute value depends on the installed TextToTargetStateResolver. +]]> + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +A transition defines a supported path through the flow. Transitions may be annotated with attributes +and may execute one or more actions before executing. +]]> + + + + + + + + + + + + + + + + + + + + + + +This value may be a logical application view name which gets resolved to a template: +
    +	priceForm
    +
    +It may even be a direct pointer to a view template: +
    +	/WEB-INF/jsp/priceForm.jsp
    +
    +This value may also be a view name expression evaluated against the request context: +
    +	${flowScope.view}
    +
    +Use of the "redirect:" prefix indicates this view state should trigger a redirect to a +unique "flow execution URL". This causes the application view to render on the +redirected request to that URL. This allows browsers to refresh a specific +state of the conversation while it remains active: +
    +	redirect:priceForm
    +
    +Use of the "externalRedirect:" prefix indicates this view state should trigger a +redirect to an absolute external URL, typically to interface with an external system. +External redirect query parameters may be specified using ${expressions} that evaluate +against the request context: +
    +	externalRedirect:/someOtherSystem.htm?orderId=${flowScope.order.id}
    +
    +Use of the "flowRedirect:" prefix has this view state generate a redirect to a URL +that launches a new flow execution of an identified flow: +
    +	flowRedirect:editOrderFlow?orderId=${flowScope.order.id}
    +
    +Use of the "bean:" prefix references a custom ViewSelector implementation you define, +exposed by id in either a flow-local context using the "import" element or in the parent +context. +
    +	bean:myCustomViewSelector
    +
    +The exact semantics regarding the interpretation of this value are determined by the +installed TextToViewSelector converter. +
    +Note when no view name is provided, this view state will make a "null" view selection. A null +view does not request the rendering of a view, it only pauses the flow and returns control +the client. Use a null view when another state is expected to generate the response. +]]> +
    +
    +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +The form is: +
    +	<if test="${criteria}" then="trueStateId" else="falseStateId"/>
    +
    +]]> +
    +
    +
    + + + + + + + + + + + + + + +
    +
    +
    +
    +
    + + + + + + + +For example: +
    +	<if test="${flowScope.sale.shipping} then="enterShippingDetails"/>
    +	<if test="${lastEvent.id == 'search'} then="bindSearchParameters"/>
    +
    +]]> +
    +
    +
    + + + + +The value of this attribute may be a static state identifier (e.g. then="displayForm") +or an expression to be evaluated at runtime against the request context +(e.g. then="${flowScope.previousViewState}"). Custom target state resolvers implementations +can be referenced by id (e.g. to="bean:myCustomTargetStateResolver"). The +exact interpretation of this attribute value depends on the installed TextToTargetStateResolver. +]]> + + + + + + + + + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + +For the input mapper the following mapping characteristics apply: +
      +
    • The 'source' of each mapping is the RequestContext, exposing access to internal +data structures of this flow such as flowScope. +
    • The 'target' of each input mapping is the subflow's input Map. +
    +
    +For the output mapper the following mapping characteristics apply: +
      +
    • The 'source' of each output mapping is the subflow's output Map. +
    • The 'target' of each output mapping is the RequestContext, exposing access to +internal data structures of this flow such as flowScope. +
    +
    +For example: +
    +    <attribute-mapper>
    +	    <input-mapper>
    +		    <input-attribute name="myFlowAttribute"/>
    +    	</input-mapper>
    +    	<output-mapper>
    +    		<output-attribute name="aSubflowOutputAttribute"/>
    +    	</output-mapper>
    +    </attribute-mapper>
    +
    +]]> +
    +
    +
    + + + + +A transition defines a supported path through the flow. Transitions may be annotated with attributes +and may specify one or more actions to execute before executing. +]]> + + + + + + + + + + + + + + + + + +
    + + + + + + + +
    +
    +
    +
    + + + + + + + + +
  • The 'source' of each mapping is the RequestContext, exposing access to internal +data structures of this flow such as flowScope. +
  • The 'target' of each input mapping is the subflow's input map. + +
    +For example: +
    +    <input-mapper>
    +	    <mapping source="flowScope.myFlowAttribute" target="subflowInputAttribute"/>
    +    </input-mapper>
    +
    +... maps the value of "flowScope.myFlowAttribute" to the "subflowInputAttribute" in the subflow input map. +]]> + + + + + + + +
  • The 'source' of each output mapping is the subflow's output map. +
  • The 'target' of each output mapping is the RequestContext, exposing access to +internal data structures of this flow such as flowScope. + +
    +For example: +
    +    <output-mapper>
    +	    <mapping source="aSubflowOutputAttribute" target="flowScope.myFlowAttribute"/>
    +    </output-mapper>
    +
    +... maps the value of "aSubflowOutputAttribute" in the subflow output map to "myFlowAttribute" +in flow scope. +]]> + + + + + + + + +Use this as an alternative to the child input-mapper and output-mapper elements +when you need full control of attribute mapping behavior for this subflow state. +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +For the output mapper the following mapping characteristics apply: +
      +
    • The 'source' of each mapping is this flow execution's RequestContext, exposing access to +internal data structures such as 'flowScope'. +
    • The 'target' of each mapping is the flow output map that will contain the output returned to the +caller that launched this flow. +
    +
    +For example: +
    +    <output-mapper>
    +        <mapping source="flowScope.myFlowAttribute" target="clientOutputAttribute"/>
    +    </output-mapper>
    +
    +... maps the value of "myFlowAttribute" in flow scope to "clientOutputAttribute" in this flow's output map. +]]> +
    +
    +
    + + + + + + + +
    + + + + +This value may be a logical application view name which gets resolved to a template: +
    +	priceForm
    +
    +It may even be a direct pointer to a view template: +
    +	/WEB-INF/jsp/priceForm.jsp
    +
    +This value may also be a view name expression evaluated against the request context: +
    +	${flowScope.view}
    +
    +Use of the "externalRedirect:" prefix triggers a redirect to a specific "after conversation completion" +external URL: +
    +    externalRedirect:/home.html
    +
    +Redirect query parameters may also be specified using ${expressions} that evaluate against +the request context: +
    +    externalRedirect:/thankyou.htm?confirmationNumber=${flowScope.order.confirmation.id}
    +
    +Use of the "flowRedirect:" prefix indicates this end state should trigger a redirect that +starts another flow. Flow input parameters may be specified using ${expressions} that +evaluate against the request context: +
    +	flowRedirect:search-flow?firstName=${flowScope.searchCriteria.firstName}
    +
    +Use of the "bean:" prefix references a custom ViewSelector implementation you define, +exposed by id in either a flow-local context using the "import" element or in the parent +context. +
    +    bean:myCustomViewSelector
    +
    +The exact semantics regarding the interpretation of this value are determined by the +installed TextToViewSelector converter. +
    +Note when no view name is provided, this view state will make a "null" view selection. A null +view does not request the rendering of a view, it only pauses the flow and returns control +the client. Use a null view when another state is expected to generate the response. +]]> +
    +
    +
    +
    +
    +
    +
    + + + + + + + + +A transition defines a supported path through the flow. Transitions may be annotated with attributes +and may execute one or more actions before executing. +]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +For example: +
    +    <import resource="orderitem-flow-beans.xml"/>
    +
    +... would look for 'orderitem-flow-beans.xml' in the same directory as this document. +]]> +
    +
    +
    +
    +
    + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowExecutionImpl.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowExecutionImpl.java new file mode 100644 index 00000000..c2fc5dbd --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowExecutionImpl.java @@ -0,0 +1,573 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.impl; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; +import java.util.LinkedList; +import java.util.ListIterator; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.CollectionUtils; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.RequestControlContext; +import org.springframework.webflow.engine.State; +import org.springframework.webflow.engine.ViewState; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.FlowExecutionListener; +import org.springframework.webflow.execution.FlowSession; +import org.springframework.webflow.execution.FlowSessionStatus; +import org.springframework.webflow.execution.ViewSelection; + +/** + * Default implementation of FlowExecution that uses a stack-based data + * structure to manage spawned flow sessions. This class is closely coupled with + * package-private FlowSessionImpl and + * RequestControlContextImpl. The three classes work together to + * form a complete flow execution implementation based on a finite state + * machine. + *

    + * This implementation of FlowExecution is serializable so it can be safely + * stored in an HTTP session or other persistent store such as a file, database, + * or client-side form field. Once deserialized, the + * {@link FlowExecutionImplStateRestorer} strategy is expected to be used to + * restore the execution to a usable state. + * + * @see FlowExecutionImplFactory + * @see FlowExecutionImplStateRestorer + * + * @author Keith Donald + * @author Erwin Vervaet + * @author Ben Hale + */ +public class FlowExecutionImpl implements FlowExecution, Externalizable { + + private static final Log logger = LogFactory.getLog(FlowExecutionImpl.class); + + /** + * The execution's root flow; the top level flow that acts as the starting + * point for this flow execution. + *

    + * Transient to support restoration by the {@link FlowExecutionImplStateRestorer}. + */ + private transient Flow flow; + + /** + * The stack of active, currently executing flow sessions. As subflows are + * spawned, they are pushed onto the stack. As they end, they are popped off + * the stack. + */ + private LinkedList flowSessions; + + /** + * A thread-safe listener list, holding listeners monitoring the lifecycle + * of this flow execution. + *

    + * Transient to support restoration by the {@link FlowExecutionImplStateRestorer}. + */ + private transient FlowExecutionListeners listeners; + + /** + * A data structure for attributes shared by all flow sessions. + *

    + * Transient to support restoration by the {@link FlowExecutionImplStateRestorer}. + */ + private transient MutableAttributeMap conversationScope; + + /** + * A data structure for runtime system execution attributes. + *

    + * Transient to support restoration by the {@link FlowExecutionImplStateRestorer}. + */ + private transient AttributeMap attributes; + + /** + * Set so the transient {@link #flow} field can be restored by the + * {@link FlowExecutionImplStateRestorer}. + */ + private String flowId; + + /** + * Default constructor required for externalizable serialization. Should NOT + * be called programmatically. + */ + public FlowExecutionImpl() { + } + + /** + * Create a new flow execution executing the provided flow. This constructor + * is mainly used for testing. + * @param flow the root flow of this flow execution + */ + public FlowExecutionImpl(Flow flow) { + this(flow, new FlowExecutionListener[0], null); + } + + /** + * Create a new flow execution executing the provided flow. + * @param flow the root flow of this flow execution + * @param listeners the listeners interested in flow execution lifecycle + * events + * @param attributes flow execution system attributes + */ + public FlowExecutionImpl(Flow flow, FlowExecutionListener[] listeners, AttributeMap attributes) { + setFlow(flow); + this.flowSessions = new LinkedList(); + this.listeners = new FlowExecutionListeners(listeners); + this.attributes = (attributes != null ? attributes : CollectionUtils.EMPTY_ATTRIBUTE_MAP); + this.conversationScope = new LocalAttributeMap(); + if (logger.isDebugEnabled()) { + logger.debug("Created new execution of flow '" + flow.getId() + "'"); + } + } + + public String getCaption() { + return "execution of '" + flowId + "'"; + } + + // implementing FlowExecutionContext + + public FlowDefinition getDefinition() { + return flow; + } + + public boolean isActive() { + return !flowSessions.isEmpty(); + } + + public FlowSession getActiveSession() { + return getActiveSessionInternal(); + } + + public MutableAttributeMap getConversationScope() { + return conversationScope; + } + + public AttributeMap getAttributes() { + return attributes; + } + + // methods implementing FlowExecution + + public ViewSelection start(MutableAttributeMap input, ExternalContext externalContext) + throws FlowExecutionException { + Assert.state(!isActive(), + "This flow is already executing -- you cannot call 'start()' more than once"); + if (logger.isDebugEnabled()) { + logger.debug("Starting execution with input '" + input + "'"); + } + RequestControlContext context = createControlContext(externalContext); + getListeners().fireRequestSubmitted(context); + try { + try { + // launch a flow session for the root flow + ViewSelection selectedView = context.start(flow, input); + return pause(context, selectedView); + } + catch (FlowExecutionException e) { + return pause(context, handleException(e, context)); + } + } + finally { + getListeners().fireRequestProcessed(context); + } + } + + public ViewSelection signalEvent(String eventId, ExternalContext externalContext) throws FlowExecutionException { + assertActive(); + if (logger.isDebugEnabled()) { + logger.debug("Resuming execution on user event '" + eventId + "'"); + } + RequestControlContext context = createControlContext(externalContext); + context.getFlashScope().clear(); + getListeners().fireRequestSubmitted(context); + try { + try { + resume(context); + Event event = + new Event(externalContext, eventId, externalContext.getRequestParameterMap().asAttributeMap()); + ViewSelection selectedView = context.signalEvent(event); + return pause(context, selectedView); + } + catch (FlowExecutionException e) { + return pause(context, handleException(e, context)); + } + } + finally { + getListeners().fireRequestProcessed(context); + } + } + + public ViewSelection refresh(ExternalContext externalContext) throws FlowExecutionException { + assertActive(); + if (logger.isDebugEnabled()) { + logger.debug("Resuming execution for refresh"); + } + RequestControlContext context = createControlContext(externalContext); + getListeners().fireRequestSubmitted(context); + try { + try { + resume(context); + State currentState = getCurrentState(); + if (!(currentState instanceof ViewState)) { + throw new IllegalStateException("Current state is not a view state - cannot refresh; " + + "perhaps an unhandled exception occured in another state?"); + } + ViewSelection selectedView = ((ViewState)currentState).refresh(context); + return pause(context, selectedView); + } + catch (FlowExecutionException e) { + return pause(context, handleException(e, context)); + } + } + finally { + getListeners().fireRequestProcessed(context); + } + } + + /** + * Returns the listener list. + * @return the attached execution listeners. + */ + FlowExecutionListeners getListeners() { + return listeners; + } + + /** + * Resume this flow execution. + * @param context the state request context + */ + protected void resume(RequestControlContext context) { + getActiveSessionInternal().setStatus(FlowSessionStatus.ACTIVE); + getListeners().fireResumed(context); + } + + /** + * Pause this flow execution. + * @param context the request control context + * @param selectedView the initial selected view to render + * @return the selected view to render + */ + protected ViewSelection pause(RequestControlContext context, ViewSelection selectedView) { + if (!isActive()) { + // view selected by an end state + return selectedView; + } + getActiveSessionInternal().setStatus(FlowSessionStatus.PAUSED); + getListeners().firePaused(context, selectedView); + if (logger.isDebugEnabled()) { + if (selectedView != null) { + logger.debug("Paused to render " + selectedView + " and wait for user input"); + } + else { + logger.debug("Paused to wait for user input"); + } + } + return selectedView; + } + + /** + * Handles an exception that occured performing an operation on this flow + * execution. First trys the set of exception handlers associated with the + * offending state, then the handlers at the flow level. + * @param exception the exception that occured + * @param context the request control context the exception occured in + * @return the selected error view, never null + * @throws FlowExecutionException rethrows the exception if it was not handled + * at the state or flow level + */ + protected ViewSelection handleException(FlowExecutionException exception, RequestControlContext context) + throws FlowExecutionException { + getListeners().fireExceptionThrown(context, exception); + if (logger.isDebugEnabled()) { + logger.debug("Attempting to handle [" + exception + "]"); + } + // the state could be null if the flow was attempting a start operation + ViewSelection selectedView = tryStateHandlers(exception, context); + if (selectedView != null) { + return selectedView; + } + selectedView = tryFlowHandlers(exception, context); + if (selectedView != null) { + return selectedView; + } + if (logger.isDebugEnabled()) { + logger.debug("Rethrowing unhandled flow execution exception"); + } + throw exception; + } + + /** + * Try to handle given exception using execution exception handlers registered + * at the state level. Returns null if no handler handled the exception. + */ + private ViewSelection tryStateHandlers(FlowExecutionException exception, RequestControlContext context) { + ViewSelection selectedView = null; + if (exception.getStateId() != null) { + selectedView = getActiveFlow().getStateInstance(exception.getStateId()).handleException(exception, context); + if (selectedView != null) { + if (logger.isDebugEnabled()) { + logger.debug("State '" + exception.getStateId() + "' handled exception"); + } + } + } + return selectedView; + } + + /** + * Try to handle given exception using execution exception handlers registered + * at the flow level. Returns null if no handler handled the exception. + */ + private ViewSelection tryFlowHandlers(FlowExecutionException exception, RequestControlContext context) { + ViewSelection selectedView = getActiveFlow().handleException(exception, context); + if (selectedView != null) { + if (logger.isDebugEnabled()) { + logger.debug("Flow '" + exception.getFlowId() + "' handled exception"); + } + } + return selectedView; + } + + // internal helpers + + /** + * Create a flow execution control context. + * @param externalContext the external context triggering this request + */ + protected RequestControlContext createControlContext(ExternalContext externalContext) { + return new RequestControlContextImpl(this, externalContext); + } + + /** + * Returns the currently active flow session. + * @throws IllegalStateException this execution is not active + */ + FlowSessionImpl getActiveSessionInternal() throws IllegalStateException { + assertActive(); + return (FlowSessionImpl)flowSessions.getLast(); + } + + /** + * Set the state that is currently active in this flow execution. + * @param newState the new current state + */ + protected void setCurrentState(State newState) { + getActiveSessionInternal().setState(newState); + } + + /** + * Activate a new FlowSession for the flow definition. + * Pushes the new flow session onto the stack. + * @param flow the flow definition + * @return the new flow session + */ + protected FlowSession activateSession(Flow flow) { + FlowSessionImpl session; + if (!flowSessions.isEmpty()) { + FlowSessionImpl parent = getActiveSessionInternal(); + parent.setStatus(FlowSessionStatus.SUSPENDED); + session = createFlowSession(flow, parent); + } + else { + session = createFlowSession(flow, null); + } + flowSessions.add(session); + session.setStatus(FlowSessionStatus.STARTING); + if (logger.isDebugEnabled()) { + logger.debug("Starting " + session); + } + return session; + } + + /** + * Create a new flow session object. Subclasses can override this to return + * a special implementation if required. + * @param flow the flow that should be associated with the flow session + * @param parent the flow session that should be the parent of the newly + * created flow session (may be null) + * @return the newly created flow session + */ + FlowSessionImpl createFlowSession(Flow flow, FlowSessionImpl parent) { + return new FlowSessionImpl(flow, parent); + } + + /** + * End the active flow session, popping it of the stack. + * @return the ended session + */ + public FlowSession endActiveFlowSession() { + FlowSessionImpl endingSession = (FlowSessionImpl)flowSessions.removeLast(); + endingSession.setStatus(FlowSessionStatus.ENDED); + if (!flowSessions.isEmpty()) { + if (logger.isDebugEnabled()) { + logger.debug("Resuming session '" + getActiveSessionInternal().getDefinition().getId() + "' in state '" + + getActiveSessionInternal().getState().getId() + "'"); + } + getActiveSessionInternal().setStatus(FlowSessionStatus.ACTIVE); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("[Ended] - this execution is now inactive"); + } + } + return endingSession; + } + + /** + * Make sure that this flow execution is active and throw an exception if it's + * not. + */ + private void assertActive() throws IllegalStateException { + if (!isActive()) { + throw new IllegalStateException( + "This flow execution is not active, it has either ended or has never been started."); + } + } + + /** + * Returns the currently active flow. + */ + private Flow getActiveFlow() { + return (Flow)getActiveSessionInternal().getDefinition(); + } + + /** + * Returns the current state of this flow execution. + */ + private State getCurrentState() { + return (State)getActiveSessionInternal().getState(); + } + + // custom serialization (implementation of Externalizable for optimized + // storage) + + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + flowId = (String)in.readObject(); + flowSessions = (LinkedList)in.readObject(); + } + + public void writeExternal(ObjectOutput out) throws IOException { + out.writeObject(flowId); + out.writeObject(flowSessions); + } + + public String toString() { + if (!isActive()) { + return "[Inactive " + getCaption() + "]"; + } + else { + if (flow != null) { + return new ToStringCreator(this).append("flow", flow.getId()).append("flowSessions", flowSessions) + .toString(); + } + else { + return "[Unhydrated " + getCaption() + "]"; + } + } + } + + // package private setters for restoring transient state + // used by FlowExecutionImplStateRestorer + + /** + * Restore the flow definition of this flow execution. + */ + void setFlow(Flow flow) { + Assert.notNull(flow, "The root flow definition is required"); + this.flow = flow; + this.flowId = flow.getId(); + } + + /** + * Restore the listeners of this flow execution. + */ + void setListeners(FlowExecutionListeners listeners) { + Assert.notNull(listeners, "The execution listener list is required"); + this.listeners = listeners; + } + + /** + * Restore the execution attributes of this flow execution. + */ + void setAttributes(AttributeMap attributes) { + Assert.notNull(conversationScope, "The execution attribute map is required"); + this.attributes = attributes; + } + + /** + * Restore conversation scope for this flow execution. + */ + void setConversationScope(MutableAttributeMap conversationScope) { + Assert.notNull(conversationScope, "The conversation scope map is required"); + this.conversationScope = conversationScope; + } + + /** + * Returns the flow definition id of this flow execution. + */ + String getFlowId() { + return flowId; + } + + /** + * Returns the list of flow session maintained by this flow execution. + */ + LinkedList getFlowSessions() { + return flowSessions; + } + + /** + * Are there any flow sessions in this flow execution? + */ + boolean hasSessions() { + return !flowSessions.isEmpty(); + } + + /** + * Are there any sessions for sub flows in this flow execution? + */ + boolean hasSubflowSessions() { + return flowSessions.size() > 1; + } + + /** + * Returns the flow session for the root flow of this flow execution. + */ + FlowSessionImpl getRootSession() { + return (FlowSessionImpl)flowSessions.getFirst(); + } + + /** + * Returns an iterator looping over the subflow sessions + * in this flow execution. + */ + ListIterator getSubflowSessionIterator() { + return flowSessions.listIterator(1); + } + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowExecutionImplFactory.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowExecutionImplFactory.java new file mode 100644 index 00000000..ab360477 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowExecutionImplFactory.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.impl; + +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.Assert; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.CollectionUtils; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.FlowExecutionFactory; +import org.springframework.webflow.execution.FlowExecutionListener; +import org.springframework.webflow.execution.factory.FlowExecutionListenerLoader; +import org.springframework.webflow.execution.factory.StaticFlowExecutionListenerLoader; + +/** + * A factory for instances of the + * {@link FlowExecutionImpl default flow execution} implementation. + * + * @author Keith Donald + */ +public class FlowExecutionImplFactory implements FlowExecutionFactory { + + private static final Log logger = LogFactory.getLog(FlowExecutionImplFactory.class); + + /** + * The strategy for loading listeners that should observe executions of a + * flow definition. The default simply loads an empty static listener list. + */ + private FlowExecutionListenerLoader executionListenerLoader = StaticFlowExecutionListenerLoader.EMPTY_INSTANCE; + + /** + * System execution attributes that may influence flow execution behavior. + * The default is an empty map. + */ + private AttributeMap executionAttributes = CollectionUtils.EMPTY_ATTRIBUTE_MAP; + + /** + * Returns the attributes to apply to flow executions created by this factory. + * Execution attributes may affect flow execution behavior. + * @return flow execution attributes + */ + public AttributeMap getExecutionAttributes() { + return executionAttributes; + } + + /** + * Sets the attributes to apply to flow executions created by this factory. + * Execution attributes may affect flow execution behavior. + * @param executionAttributes flow execution system attributes + */ + public void setExecutionAttributes(AttributeMap executionAttributes) { + Assert.notNull(executionAttributes, "The execution attributes map is required"); + this.executionAttributes = executionAttributes; + } + + /** + * Sets the attributes to apply to flow executions created by this factory. + * Execution attributes may affect flow execution behavior. + *

    + * Convenience setter that takes a simple java.util.Map to ease + * bean style configuration. + * @param executionAttributes flow execution system attributes + */ + public void setExecutionAttributesMap(Map executionAttributes) { + Assert.notNull(executionAttributes, "The execution attributes map is required"); + this.executionAttributes = new LocalAttributeMap(executionAttributes); + } + + /** + * Returns the strategy for loading listeners that should observe executions of + * a flow definition. Allows full control over what listeners should apply + * for executions of a flow definition. + */ + public FlowExecutionListenerLoader getExecutionListenerLoader() { + return executionListenerLoader; + } + + /** + * Sets the strategy for loading listeners that should observe executions of + * a flow definition. Allows full control over what listeners should apply + * for executions of a flow definition. + */ + public void setExecutionListenerLoader(FlowExecutionListenerLoader listenerLoader) { + Assert.notNull(listenerLoader, "The listener loader is required"); + this.executionListenerLoader = listenerLoader; + } + + public FlowExecution createFlowExecution(FlowDefinition flowDefinition) { + Assert.isInstanceOf(Flow.class, flowDefinition, "Flow definition is of wrong type: "); + if (logger.isDebugEnabled()) { + logger.debug("Creating flow execution for flow definition with id '" + flowDefinition.getId() + "'"); + } + FlowExecutionListener[] listeners = executionListenerLoader.getListeners(flowDefinition); + return new FlowExecutionImpl((Flow)flowDefinition, listeners, executionAttributes); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowExecutionImplStateRestorer.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowExecutionImplStateRestorer.java new file mode 100644 index 00000000..240d2756 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowExecutionImplStateRestorer.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.impl; + +import java.util.ListIterator; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.CollectionUtils; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.registry.FlowDefinitionLocator; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.factory.FlowExecutionListenerLoader; +import org.springframework.webflow.execution.factory.StaticFlowExecutionListenerLoader; +import org.springframework.webflow.execution.repository.support.FlowExecutionStateRestorer; + +/** + * Restores the transient state of deserialized {@link FlowExecutionImpl} + * objects. + * + * @author Keith Donald + */ +public class FlowExecutionImplStateRestorer implements FlowExecutionStateRestorer { + + /** + * Used to restore the flow execution's flow definition. + */ + private FlowDefinitionLocator definitionLocator; + + /** + * Used to restore the flow execution's listeners. + */ + private FlowExecutionListenerLoader executionListenerLoader = StaticFlowExecutionListenerLoader.EMPTY_INSTANCE; + + /** + * Used to restore the flow execution's system attributes. + */ + private AttributeMap executionAttributes = CollectionUtils.EMPTY_ATTRIBUTE_MAP; + + /** + * Creates a new execution transient state restorer. + * @param definitionLocator the flow definition locator + */ + public FlowExecutionImplStateRestorer(FlowDefinitionLocator definitionLocator) { + Assert.notNull(definitionLocator, "The flow definition locator is required"); + this.definitionLocator = definitionLocator; + } + + /** + * Sets the attributes to apply to restored flow executions. + * Execution attributes may affect flow execution behavior. + * @param executionAttributes flow execution system attributes + */ + public void setExecutionAttributes(AttributeMap executionAttributes) { + Assert.notNull(executionAttributes, "The execution attributes map is required"); + this.executionAttributes = executionAttributes; + } + + /** + * Sets the attributes to apply to restored flow executions. + * Execution attributes may affect flow execution behavior. + *

    + * Convenience setter that takes a simple java.util.Map to ease + * bean style configuration. + * @param executionAttributes flow execution system attributes + */ + public void setExecutionAttributesMap(Map executionAttributes) { + Assert.notNull(executionAttributes, "The execution attributes map is required"); + this.executionAttributes = new LocalAttributeMap(executionAttributes); + } + + /** + * Sets the strategy for loading listeners that should observe executions of + * a flow definition. Allows full control over what listeners should apply. + * for executions of a flow definition. + */ + public void setExecutionListenerLoader(FlowExecutionListenerLoader executionListenerLoader) { + Assert.notNull(executionListenerLoader, "The listener loader is required"); + this.executionListenerLoader = executionListenerLoader; + } + + public FlowExecution restoreState(FlowExecution flowExecution, MutableAttributeMap conversationScope) { + FlowExecutionImpl impl = (FlowExecutionImpl)flowExecution; + // the root flow should be a top-level flow visible by the flow def locator + Flow flow = (Flow)definitionLocator.getFlowDefinition(impl.getFlowId()); + impl.setFlow(flow); + if (impl.hasSessions()) { + FlowSessionImpl root = impl.getRootSession(); + root.setFlow(flow); + root.setState(flow.getStateInstance(root.getStateId())); + if (impl.hasSubflowSessions()) { + Flow parent = flow; + for (ListIterator it = impl.getSubflowSessionIterator(); it.hasNext();) { + FlowSessionImpl subflow = (FlowSessionImpl)it.next(); + Flow definition; + if (parent.containsInlineFlow(subflow.getFlowId())) { + // subflow is an inline flow of it's parent + definition = parent.getInlineFlow(subflow.getFlowId()); + } else { + // subflow is a top-level flow + definition = (Flow)definitionLocator.getFlowDefinition(subflow.getFlowId()); + } + subflow.setFlow(definition); + subflow.setState(definition.getStateInstance(subflow.getStateId())); + parent = definition; + } + } + } + if (conversationScope == null) { + conversationScope = new LocalAttributeMap(); + } + impl.setConversationScope(conversationScope); + impl.setListeners(new FlowExecutionListeners(executionListenerLoader.getListeners(flow))); + impl.setAttributes(executionAttributes); + return flowExecution; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowExecutionListeners.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowExecutionListeners.java new file mode 100644 index 00000000..bce24616 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowExecutionListeners.java @@ -0,0 +1,201 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.impl; + +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.FlowExecutionListener; +import org.springframework.webflow.execution.FlowSession; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; + +/** + * A helper that aids in publishing events to an array of + * FlowExecutionListener objects. + * + * @see org.springframework.webflow.execution.FlowExecutionListener + * + * @author Keith Donald + * @author Erwin Vervaet + */ +class FlowExecutionListeners { + + /** + * The list of listeners that should receive event callbacks during managed + * flow executions. + */ + private FlowExecutionListener[] listeners; + + /** + * Create a flow execution listener helper that wraps an empty listener + * array. + */ + public FlowExecutionListeners() { + this(null); + } + + /** + * Create a flow execution listener helper that wraps the specified listener + * array. + * @param listeners the listener array + */ + public FlowExecutionListeners(FlowExecutionListener[] listeners) { + if (listeners != null) { + this.listeners = listeners; + } + else { + this.listeners = new FlowExecutionListener[0]; + } + } + + /** + * Returns the wrapped listener array. + * @return the listener array + */ + public FlowExecutionListener[] getArray() { + return listeners; + } + + /** + * Returns the size of the listener array. + */ + public int size() { + return listeners.length; + } + + // methods to fire events to all listeners + + /** + * Notify all interested listeners that a request was submitted to the flow + * execution. + */ + public void fireRequestSubmitted(RequestContext context) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].requestSubmitted(context); + } + } + + /** + * Notify all interested listeners that the flow execution finished + * processing a request. + */ + public void fireRequestProcessed(RequestContext context) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].requestProcessed(context); + } + } + + /** + * Notify all interested listeners that a flow execution session is + * starting. + */ + public void fireSessionStarting(RequestContext context, FlowDefinition flow, MutableAttributeMap input) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].sessionStarting(context, flow, input); + } + } + + /** + * Notify all interested listeners that a flow execution session has + * started. + */ + public void fireSessionStarted(RequestContext context, FlowSession session) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].sessionStarted(context, session); + } + } + + /** + * Notify all interested listeners that an event was signaled in the flow + * execution. + */ + public void fireEventSignaled(RequestContext context, Event event) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].eventSignaled(context, event); + } + } + + /** + * Notify all interested listeners that a state is being entered in the flow + * execution. + */ + public void fireStateEntering(RequestContext context, StateDefinition nextState) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].stateEntering(context, nextState); + } + } + + /** + * Notify all interested listeners that a state was entered in the flow + * execution. + */ + public void fireStateEntered(RequestContext context, StateDefinition previousState) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].stateEntered(context, previousState, context.getCurrentState()); + } + } + + /** + * Notify all interested listeners that a flow session was paused in the + * flow execution. + */ + public void firePaused(RequestContext context, ViewSelection selectedView) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].paused(context, selectedView); + } + } + + /** + * Notify all interested listeners that the flow execution was resumed. + */ + public void fireResumed(RequestContext context) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].resumed(context); + } + } + + /** + * Notify all interested listeners that the active flow execution session is + * ending. + */ + public void fireSessionEnding(RequestContext context, FlowSession session, MutableAttributeMap output) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].sessionEnding(context, session, output); + } + } + + /** + * Notify all interested listeners that a flow execution session has ended. + */ + public void fireSessionEnded(RequestContext context, FlowSession session, AttributeMap output) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].sessionEnded(context, session, output); + } + } + + /** + * Notify all interested listeners that a flow execution threw an exception. + */ + public void fireExceptionThrown(RequestContext context, FlowExecutionException exception) { + for (int i = 0; i < listeners.length; i++) { + listeners[i].exceptionThrown(context, exception); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowSessionImpl.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowSessionImpl.java new file mode 100644 index 00000000..a232c56d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/FlowSessionImpl.java @@ -0,0 +1,219 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.impl; + +import java.io.Externalizable; +import java.io.IOException; +import java.io.ObjectInput; +import java.io.ObjectOutput; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.State; +import org.springframework.webflow.execution.FlowSession; +import org.springframework.webflow.execution.FlowSessionStatus; + +/** + * Implementation of the FlowSession interfaced used internally by the + * FlowExecutionImpl. This class is closely coupled with + * FlowExecutionImpl and RequestControlContextImpl. + * The three classes work together to form a complete flow execution + * implementation. + * + * @author Keith Donald + * @author Erwin Vervaet + * @author Ben Hale + */ +class FlowSessionImpl implements FlowSession, Externalizable { + + /** + * The flow definition (a singleton). + *

    + * Transient to support restoration by the + * {@link FlowExecutionImplStateRestorer}. + */ + private transient Flow flow; + + /** + * Set so the transient {@link #flow} field can be restored by the + * {@link FlowExecutionImplStateRestorer}. + */ + private String flowId; + + /** + * The current state of this flow session. + *

    + * Transient to support restoration by the + * {@link FlowExecutionImplStateRestorer}. + */ + private transient State state; + + /** + * Set so the transient {@link #state} field can be restored by the + * {@link FlowExecutionImplStateRestorer}. + */ + private String stateId; + + /** + * The session status; may be CREATED, STARTING, ACTIVE, PAUSED, SUSPENDED, + * or ENDED. + */ + private FlowSessionStatus status = FlowSessionStatus.CREATED; + + /** + * The session data model ("flow scope"). + */ + private MutableAttributeMap scope = new LocalAttributeMap(); + + /** + * The flash map ("flash scope"). + */ + private MutableAttributeMap flashMap = new LocalAttributeMap(); + + /** + * The parent session of this session (may be null if this is + * a root session.) + */ + private FlowSessionImpl parent; + + /** + * Default constructor required for externalizable serialization. Should NOT + * be called programmatically. + */ + public FlowSessionImpl() { + } + + /** + * Create a new flow session. + * @param flow the flow definition associated with this flow session + * @param parent this session's parent (may be null) + */ + public FlowSessionImpl(Flow flow, FlowSessionImpl parent) { + setFlow(flow); + this.parent = parent; + } + + // implementing FlowSession + + public FlowDefinition getDefinition() { + return flow; + } + + public StateDefinition getState() { + return state; + } + + public FlowSessionStatus getStatus() { + return status; + } + + public MutableAttributeMap getScope() { + return scope; + } + + public MutableAttributeMap getFlashMap() { + return flashMap; + } + + public FlowSession getParent() { + return parent; + } + + public boolean isRoot() { + return parent == null; + } + + // custom serialization + + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + flowId = (String)in.readObject(); + stateId = (String)in.readObject(); + status = (FlowSessionStatus)in.readObject(); + scope = (MutableAttributeMap)in.readObject(); + flashMap = (MutableAttributeMap)in.readObject(); + parent = (FlowSessionImpl)in.readObject(); + } + + public void writeExternal(ObjectOutput out) throws IOException { + out.writeObject(flowId); + out.writeObject(stateId); + out.writeObject(status); + out.writeObject(scope); + out.writeObject(flashMap); + out.writeObject(parent); + } + + // package private setters for setting/updating internal state + // used by FlowExecutionImplStateRestorer + + /** + * Restores the definition of this flow session. + * @param flow the flow sessions definition + * @see FlowExecutionImplStateRestorer + */ + void setFlow(Flow flow) { + Assert.notNull(flow, "The flow is required"); + this.flow = flow; + this.flowId = flow.getId(); + } + + /** + * Set the current state of this flow session. + * @param state the state that is currently active in this flow session + * @see FlowExecutionImpl#setCurrentState(State) + * @see FlowExecutionImplStateRestorer + */ + void setState(State state) { + Assert.notNull(state, "The state is required"); + Assert.isTrue(flow == state.getOwner(), + "The state does not belong to the flow associated with this flow session"); + this.state = state; + this.stateId = state.getId(); + } + + /** + * Set the status of this flow session. + * @param status the new status to set + */ + void setStatus(FlowSessionStatus status) { + Assert.notNull(status, "The flow session status is requred"); + this.status = status; + } + + /** + * Returns the id of the flow of this session. + */ + String getFlowId() { + return flowId; + } + + /** + * Returns the id of the current state of this session. + */ + String getStateId() { + return stateId; + } + + public String toString() { + return new ToStringCreator(this).append("flow", flowId).append("state", stateId).append("scope", scope).append( + "flashMap", flashMap).append("status", status).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/RequestControlContextImpl.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/RequestControlContextImpl.java new file mode 100644 index 00000000..4416d637 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/RequestControlContextImpl.java @@ -0,0 +1,264 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.impl; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.CollectionUtils; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.core.collection.ParameterMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; +import org.springframework.webflow.definition.TransitionDefinition; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.RequestControlContext; +import org.springframework.webflow.engine.State; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecutionContext; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.FlowSession; +import org.springframework.webflow.execution.FlowSessionStatus; +import org.springframework.webflow.execution.ViewSelection; + +/** + * Default request control context implementation used internally by the web + * flow system. This class is closely coupled with + * FlowExecutionImpl and FlowSessionImpl. The + * three classes work together to form a complete flow execution implementation + * based on a finite state machine. + * + * @see org.springframework.webflow.engine.machine.FlowExecutionImpl + * @see org.springframework.webflow.engine.machine.FlowSessionImpl + * + * @author Keith Donald + * @author Erwin Vervaet + */ +class RequestControlContextImpl implements RequestControlContext { + + private static final Log logger = LogFactory.getLog(RequestControlContextImpl.class); + + /** + * The owning flow execution. + */ + private FlowExecutionImpl flowExecution; + + /** + * The request scope data map. + */ + private LocalAttributeMap requestScope = new LocalAttributeMap(); + + /** + * A source context for the caller who initiated this request. + */ + private ExternalContext externalContext; + + /** + * The last event that occured in this request context. + */ + private Event lastEvent; + + /** + * The last transition that executed in this request context. + */ + private Transition lastTransition; + + /** + * Holder for contextual execution properties. + */ + private AttributeMap attributes; + + /** + * Create a new request context. + * @param flowExecution the owning flow execution + * @param externalContext the external context that originated the flow + * execution request + */ + public RequestControlContextImpl(FlowExecutionImpl flowExecution, ExternalContext externalContext) { + Assert.notNull(flowExecution, "The owning flow execution is required"); + this.externalContext = externalContext; + this.flowExecution = flowExecution; + } + + // implementing RequestContext + + public FlowDefinition getActiveFlow() { + return flowExecution.getActiveSession().getDefinition(); + } + + public StateDefinition getCurrentState() { + return flowExecution.getActiveSession().getState(); + } + + public MutableAttributeMap getRequestScope() { + return requestScope; + } + + public MutableAttributeMap getFlashScope() { + return flowExecution.getActiveSession().getFlashMap(); + } + + public MutableAttributeMap getFlowScope() { + return flowExecution.getActiveSession().getScope(); + } + + public MutableAttributeMap getConversationScope() { + return flowExecution.getConversationScope(); + } + + public ParameterMap getRequestParameters() { + return externalContext.getRequestParameterMap(); + } + + public ExternalContext getExternalContext() { + return externalContext; + } + + public FlowExecutionContext getFlowExecutionContext() { + return flowExecution; + } + + public Event getLastEvent() { + return lastEvent; + } + + public TransitionDefinition getLastTransition() { + return lastTransition; + } + + public AttributeMap getAttributes() { + return attributes; + } + + public void setAttributes(AttributeMap attributes) { + if (attributes == null) { + this.attributes = CollectionUtils.EMPTY_ATTRIBUTE_MAP; + } + else { + this.attributes = attributes; + } + } + + public AttributeMap getModel() { + return getConversationScope().union(getFlowScope()).union(getFlashScope()).union(getRequestScope()); + } + + // implementing RequestControlContext + + public void setLastEvent(Event lastEvent) { + this.lastEvent = lastEvent; + } + + public void setLastTransition(Transition lastTransition) { + this.lastTransition = lastTransition; + } + + public void setCurrentState(State state) { + getExecutionListeners().fireStateEntering(this, state); + State previousState = getCurrentStateInternal(); + flowExecution.setCurrentState(state); + if (previousState == null) { + getActiveSession().setStatus(FlowSessionStatus.ACTIVE); + } + getExecutionListeners().fireStateEntered(this, previousState); + } + + public ViewSelection start(Flow flow, MutableAttributeMap input) throws FlowExecutionException { + if (input == null) { + // create a mutable map so entries can be added by listeners! + input = new LocalAttributeMap(); + } + if (logger.isDebugEnabled()) { + logger.debug("Activating new session for flow '" + flow.getId() + "' in state '" + + flow.getStartState().getId() + "' with input " + input); + } + getExecutionListeners().fireSessionStarting(this, flow, input); + FlowSession session = flowExecution.activateSession(flow); + ViewSelection selectedView = flow.start(this, input); + getExecutionListeners().fireSessionStarted(this, session); + return selectedView; + } + + public ViewSelection signalEvent(Event event) throws FlowExecutionException { + if (logger.isDebugEnabled()) { + logger.debug("Signaling event '" + event.getId() + "' in state '" + getCurrentState().getId() + + "' of flow '" + getActiveFlow().getId() + "'"); + } + setLastEvent(event); + getExecutionListeners().fireEventSignaled(this, event); + ViewSelection selectedView = getActiveFlowInternal().onEvent(this); + return selectedView; + } + + public FlowSession endActiveFlowSession(MutableAttributeMap output) throws IllegalStateException { + FlowSession session = getFlowExecutionContext().getActiveSession(); + getExecutionListeners().fireSessionEnding(this, session, output); + getActiveFlowInternal().end(this, output); + if (logger.isDebugEnabled()) { + logger.debug("Ending active session " + session + "; exposed session output is " + output); + } + session = flowExecution.endActiveFlowSession(); + getExecutionListeners().fireSessionEnded(this, session, output); + return session; + } + + public ViewSelection execute(Transition transition) { + return transition.execute(getCurrentStateInternal(), this); + } + + // internal helpers + + /** + * Returns the execution listerns for the flow execution of this request + * context. + */ + protected FlowExecutionListeners getExecutionListeners() { + return flowExecution.getListeners(); + } + + /** + * Returns the active flow in the flow execution of this request context. + */ + protected Flow getActiveFlowInternal() { + return (Flow)getActiveSession().getDefinition(); + } + + /** + * Returns the current state in the flow execution of this request context. + */ + protected State getCurrentStateInternal() { + return (State)getActiveSession().getState(); + } + + /** + * Returns the active flow session in the flow execution of this request + * context. + */ + protected FlowSessionImpl getActiveSession() { + return flowExecution.getActiveSessionInternal(); + } + + public String toString() { + return new ToStringCreator(this).append("externalContext", externalContext) + .append("requestScope", requestScope).append("attributes", attributes).append("flowExecution", + flowExecution).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/package.html b/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/package.html new file mode 100644 index 00000000..2c63e8a3 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/impl/package.html @@ -0,0 +1,7 @@ + + +

    +The implementation of Spring Web Flow's flow execution machine. +

    + + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/package.html b/spring-webflow/src/main/java/org/springframework/webflow/engine/package.html new file mode 100644 index 00000000..6f3bfe6f --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/package.html @@ -0,0 +1,12 @@ + + +

    +The implementation of the core flow definition artifacts that serve the basis of the flow execution engine. +

    +

    +The engine implementation itself is located within the {@link org.springframework.webflow.engine.impl impl} package. +Builders for assembling flow definitions executable by this engine are located within the +{@link org.springframework.webflow.engine.builder builder} package. +

    + + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/AbstractFlowAttributeMapper.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/AbstractFlowAttributeMapper.java new file mode 100644 index 00000000..9893704d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/AbstractFlowAttributeMapper.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import java.io.Serializable; + +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.binding.mapping.MappingContext; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.engine.FlowAttributeMapper; +import org.springframework.webflow.execution.RequestContext; + +/** + * Convenient base class for attribute mapper implementations. Encapsulates + * common attribute mapper workflow. Contains no state. Subclasses must override + * the {@link #getInputMapper()} and {@link #getOutputMapper()} methods to + * return the input mapper and output mapper, respectively. + * + * @author Keith Donald + */ +public abstract class AbstractFlowAttributeMapper implements FlowAttributeMapper, Serializable { + + /** + * Returns the input mapper to use to map attributes of a parent flow + * {@link RequestContext} to a subflow input attribute {@link AttributeMap map}. + * @return the input mapper, or null if none + * @see #createFlowInput(RequestContext) + */ + protected abstract AttributeMapper getInputMapper(); + + /** + * Returns the output mapper to use to map attributes from a subflow output + * attribute map to the {@link RequestContext}. + * @return the output mapper, or null if none + * @see #mapFlowOutput(AttributeMap, RequestContext) + */ + protected abstract AttributeMapper getOutputMapper(); + + public MutableAttributeMap createFlowInput(RequestContext context) { + if (getInputMapper() != null) { + LocalAttributeMap input = new LocalAttributeMap(); + // map from request context to input map + getInputMapper().map(context, input, getMappingContext(context)); + return input; + } + else { + // an empty, but modifiable map + return new LocalAttributeMap(); + } + } + + public void mapFlowOutput(AttributeMap subflowOutput, RequestContext context) { + if (getOutputMapper() != null && subflowOutput != null) { + // map from subflow output map to request context + getOutputMapper().map(subflowOutput, context, getMappingContext(context)); + } + } + + /** + * Returns a map of contextual data available during mapping. + * This implementation just returns null. + */ + protected MappingContext getMappingContext(RequestContext context) { + return null; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/ActionTransitionCriteria.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/ActionTransitionCriteria.java new file mode 100644 index 00000000..dd577c75 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/ActionTransitionCriteria.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import org.springframework.util.Assert; +import org.springframework.webflow.engine.ActionExecutor; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +/** + * A transition criteria that will execute an action when tested and return + * true if the action's result is equal to the 'trueEventId', + * false otherwise. + *

    + * This effectively adapts an Action to a TransitionCriteria. + * + * @see org.springframework.webflow.execution.Action + * @see org.springframework.webflow.engine.TransitionCriteria + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class ActionTransitionCriteria implements TransitionCriteria { + + /** + * The result event id that should map to a true + * return value. + */ + private String trueEventId = "success"; + + /** + * The action to execute when the criteria is tested, annotated with + * usage attributes. + */ + private Action action; + + /** + * Create action transition criteria delegating to the specified action. + * @param action the action + */ + public ActionTransitionCriteria(Action action) { + this.action = action; + } + + /** + * Returns the action result eventId that should cause this + * criteria to return true (it will return false otherwise). Defaults to + * "success". + */ + public String getTrueEventId() { + return trueEventId; + } + + /** + * Sets the action result eventId that should cause this + * precondition to return true (it will return false otherwise). + * @param trueEventId the true result event ID + */ + public void setTrueEventId(String trueEventId) { + Assert.notNull(trueEventId, "The trueEventId is required"); + this.trueEventId = trueEventId; + } + + /** + * Returns the action wrapped by this object. + * @return the action + */ + protected Action getAction() { + return action; + } + + public boolean test(RequestContext context) { + Event result = ActionExecutor.execute(getAction(), context); + return result != null && getTrueEventId().equals(result.getId()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/ApplicationViewSelector.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/ApplicationViewSelector.java new file mode 100644 index 00000000..fffaf21f --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/ApplicationViewSelector.java @@ -0,0 +1,176 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import java.io.Serializable; + +import org.springframework.binding.expression.Expression; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.webflow.engine.ViewSelector; +import org.springframework.webflow.engine.ViewState; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.execution.support.FlowExecutionRedirect; + +/** + * Simple view selector that makes an {@link ApplicationView} selection using a + * view name expression. + *

    + * This factory will treat all attributes returned from calling + * {@link RequestContext#getModel()} as the application model exposed to the + * view during rendering. This is typically the union of attributes in request, + * flow, and conversation scope. + *

    + * This selector also supports setting a redirect flag that can be used + * to trigger a redirect to the {@link ApplicationView} at a bookmarkable URL + * using an {@link FlowExecutionRedirect}}. + * + * @see org.springframework.webflow.execution.support.ApplicationView + * @see org.springframework.webflow.execution.support.FlowExecutionRedirect + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class ApplicationViewSelector implements ViewSelector, Serializable { + + /** + * Flow execution attribute name that indicates that we should always render + * an application view via a redirect. + */ + public static final String ALWAYS_REDIRECT_ON_PAUSE_ATTRIBUTE = "alwaysRedirectOnPause"; + + /** + * The view name to render. + */ + private Expression viewName; + + /** + * A flag indicating if a redirect to the selected application view should + * be requested. + *

    + * Setting this allows you to redirect while the flow is in progress to a + * stable URL that can be safely refreshed. + */ + private boolean redirect; + + /** + * Creates a application view selector that will make application view + * selections requesting that the specified view be rendered. + * @param viewName the view name expression + */ + public ApplicationViewSelector(Expression viewName) { + this(viewName, false); + } + + /** + * Creates a application view selector that will make application view + * selections requesting that the specified view be rendered. No redirects + * will be done. + * @param viewName the view name expression + * @param redirect indicates if a redirect to the view should be initiated + */ + public ApplicationViewSelector(Expression viewName, boolean redirect) { + Assert.notNull(viewName, "The view name expression is required"); + this.viewName = viewName; + this.redirect = redirect; + } + + /** + * Returns the name of the view that should be rendered. + */ + public Expression getViewName() { + return viewName; + } + + /** + * Returns if a redirect to the view should be done. + */ + public boolean isRedirect() { + return redirect; + } + + public boolean isEntrySelectionRenderable(RequestContext context) { + return !shouldRedirect(context); + } + + public ViewSelection makeEntrySelection(RequestContext context) { + if (shouldRedirect(context)) { + return FlowExecutionRedirect.INSTANCE; + } + else { + return makeRefreshSelection(context); + } + } + + public ViewSelection makeRefreshSelection(RequestContext context) { + String viewName = resolveViewName(context); + if (!StringUtils.hasText(viewName)) { + throw new IllegalStateException("Resolved application view name was empty; programmer error! -- " + + "The expression that was evaluated against the request context was '" + getViewName() + "'"); + } + return createApplicationView(viewName, context); + } + + // internal helpers + + /** + * Resolves the application view name from the request context. + * @param context the context + * @return the view name + */ + protected String resolveViewName(RequestContext context) { + return (String) getViewName().evaluate(context, null); + } + + /** + * Creates the application view selection. + * @param viewName the resolved view name + * @param context the context + * @return the application view + */ + protected ApplicationView createApplicationView(String viewName, RequestContext context) { + return new ApplicationView(viewName, context.getModel().asMap()); + } + + /** + * Determine whether or not a redirect should be used to render the + * application view. + * @param context the context + * @return true or false + */ + protected boolean shouldRedirect(RequestContext context) { + return context.getCurrentState() instanceof ViewState && (redirect || alwaysRedirectOnPause(context)); + } + + /** + * Checks the {@link #ALWAYS_REDIRECT_ON_PAUSE_ATTRIBUTE} to see if every + * application view of the flow execution should be rendered via a redirect. + * @param context the flow execution request context + * @return true or false + */ + protected boolean alwaysRedirectOnPause(RequestContext context) { + String attributeValue = String.valueOf(context.getFlowExecutionContext().getAttributes().get( + ALWAYS_REDIRECT_ON_PAUSE_ATTRIBUTE, "false")); + return new Boolean(attributeValue).booleanValue(); + } + + public String toString() { + return new ToStringCreator(this).append("viewName", viewName).append("redirect", redirect).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/AttributeExpression.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/AttributeExpression.java new file mode 100644 index 00000000..2a63710e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/AttributeExpression.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import org.springframework.binding.expression.EvaluationContext; +import org.springframework.binding.expression.EvaluationException; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.SettableExpression; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ScopeType; + +/** + * Expression evaluator that can evalute attribute maps and supported + * request context scope types. + * + * @see org.springframework.webflow.execution.RequestContext + * @see org.springframework.webflow.core.collection.AttributeMap + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class AttributeExpression implements SettableExpression { + + /** + * The expression to evaluate. + */ + private Expression expression; + + /** + * The scope type. + */ + private ScopeType scopeType; + + /** + * Create a new expression evaluator that executes given expression in an + * attribute map. When using this wrapper to set a property value, make + * sure the given expression is a {@link SettableExpression}}. + * @param expression the nested evaluator to execute + */ + public AttributeExpression(Expression expression) { + this(expression, null); + } + + /** + * Create a new expression evaluator that executes given expression in the + * specified scope. When using this wrapper to set a property value, make + * sure the given expression is a {@link SettableExpression}}. + * @param expression the nested evaluator to execute + * @param scopeType the scopeType + */ + public AttributeExpression(Expression expression, ScopeType scopeType) { + this.expression = expression; + this.scopeType = scopeType; + } + + /** + * Returns the expression that will be evaluated. + */ + protected Expression getExpression() { + return expression; + } + + public Object evaluate(Object target, EvaluationContext context) throws EvaluationException { + if (target instanceof RequestContext) { + RequestContext requestContext = (RequestContext)target; + AttributeMap scope = scopeType.getScope(requestContext); + return expression.evaluate(scope, context); + } + else if (target instanceof AttributeMap) { + return expression.evaluate(target, context); + } + else { + throw new IllegalArgumentException( + "Only supports evaluation against a [RequestContext] or [AttributeMap] instance, but was a [" + + target.getClass() + "]"); + } + } + + public void evaluateToSet(Object target, Object value, EvaluationContext context) throws EvaluationException { + Assert.isInstanceOf(SettableExpression.class, expression, + "When an AttributeExpression is used to set a property value, the nested expression needs " + + "to be a SettableExpression"); + if (target instanceof RequestContext) { + RequestContext requestContext = (RequestContext)target; + MutableAttributeMap scope = scopeType.getScope(requestContext); + ((SettableExpression)expression).evaluateToSet(scope, value, context); + } + else if (target instanceof AttributeMap) { + ((SettableExpression)expression).evaluateToSet(target, value, context); + } + else { + throw new IllegalArgumentException( + "Only supports evaluation against a [RequestContext] or [AttributeMap] instance, but was a [" + + target.getClass() + "]"); + } + } + + public String toString() { + return new ToStringCreator(this).append("expression", expression).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/BeanFactoryFlowVariable.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/BeanFactoryFlowVariable.java new file mode 100644 index 00000000..0e151a17 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/BeanFactoryFlowVariable.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.webflow.engine.FlowVariable; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ScopeType; + +/** + * A concrete flow variable subclass that obtains variable values from a Spring + * {@link BeanFactory}. + * + * @author Keith Donald + */ +public class BeanFactoryFlowVariable extends FlowVariable { + + /** + * The name of the bean whose value will be used as the flow + * variable. The bean should be a prototype. + */ + private String beanName; + + /** + * The bean factory where initial variable values will be obtained. + */ + private BeanFactory beanFactory; + + /** + * Creates a new bean factory flow variable. + * @param variableName the variable name + * @param beanName the bean name, will default to the variable name if not specified + * @param beanFactory the bean factory where initial variable values will be + * obtained + * @param scope the variable scope + */ + public BeanFactoryFlowVariable(String variableName, String beanName, BeanFactory beanFactory, ScopeType scope) { + super(variableName, scope); + if (StringUtils.hasText(beanName)) { + this.beanName = beanName; + } + else { + this.beanName = variableName; + } + Assert.notNull(beanFactory, "The bean factory is required"); + Assert.isTrue(!beanFactory.isSingleton(this.beanName), "The bean with name '" + this.beanName + + "' must be a prototype (singleton=false)"); + this.beanFactory = beanFactory; + } + + protected Object createVariableValue(RequestContext context) { + return beanFactory.getBean(beanName); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/BooleanExpressionTransitionCriteria.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/BooleanExpressionTransitionCriteria.java new file mode 100644 index 00000000..774d01bb --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/BooleanExpressionTransitionCriteria.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.binding.expression.EvaluationContext; +import org.springframework.binding.expression.Expression; +import org.springframework.util.Assert; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.execution.RequestContext; + +/** + * Transition criteria that tests the value of an expression. The + * expression is used to express a condition that guards transition + * execution in a web flow. Expressions will be evaluated agains the request + * context and should return a boolean result. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class BooleanExpressionTransitionCriteria implements TransitionCriteria { + + /** + * Constant alias that points to the id of the last event that occured + * in a web flow execution. + */ + private static final String RESULT_ALIAS = "result"; + + /** + * The expression evaluator to use. + */ + private Expression booleanExpression; + + /** + * Create a new expression based transition criteria object. + * @param booleanExpression the expression evaluator testing the criteria, + * this expression should be a condition that returns a Boolean value + */ + public BooleanExpressionTransitionCriteria(Expression booleanExpression) { + Assert.notNull(booleanExpression, "The expression to test is required"); + this.booleanExpression = booleanExpression; + } + + public boolean test(RequestContext context) { + Object result = booleanExpression.evaluate(context, getEvaluationContext(context)); + Assert.isInstanceOf(Boolean.class, result, "Impossible to determine result of boolean expression: "); + return ((Boolean)result).booleanValue(); + } + + /** + * Setup a context with a few aliased values to make writing expression based + * transition conditions a bit easier. + */ + protected EvaluationContext getEvaluationContext(RequestContext context) { + final Map attributes = new HashMap(1, 1); + // ${#result == lastEvent.id} + if (context.getLastEvent() != null) { + attributes.put(RESULT_ALIAS, context.getLastEvent().getId()); + } + return new EvaluationContext() { + public Map getAttributes() { + return attributes; + } + }; + } + + public String toString() { + return booleanExpression.toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/ConfigurableFlowAttributeMapper.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/ConfigurableFlowAttributeMapper.java new file mode 100644 index 00000000..b73eaba4 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/ConfigurableFlowAttributeMapper.java @@ -0,0 +1,224 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import java.io.Serializable; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.binding.expression.SettableExpression; +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.binding.mapping.DefaultAttributeMapper; +import org.springframework.binding.mapping.Mapping; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.execution.ScopeType; + +/** + * Generic flow attribute mapper implementation that allows mappings to be + * configured in a declarative fashion. + *

    + * Two types of mappings may be configured, input mappings and output mappings: + *

      + *
    1. Input mappings define the rules for mapping attributes in a parent flow + * to a spawning subflow. + *
    2. Output mappings define the rules for mapping attributes returned from an + * ended subflow into the resuming parent. + *
    + *

    + * The mappings defined using the configuration properties fully support bean + * property access. So an entry name in a mapping can either be "beanName" or + * "beanName.propName". Nested property values are also supported + * ("beanName.propName.nestedPropName"). When the from mapping string is + * enclosed in "${...}", it will be interpreted as an expression that will be + * evaluated against the flow execution request context. + * + * @see org.springframework.webflow.execution.RequestContext + * + * @author Erwin Vervaet + * @author Keith Donald + * @author Colin Sampaleanu + */ +public class ConfigurableFlowAttributeMapper extends AbstractFlowAttributeMapper implements Serializable { + + /** + * The expression parser that will parse input and output attribute + * expressions. + */ + private ExpressionParser expressionParser = DefaultExpressionParserFactory.getExpressionParser(); + + /** + * The mapper that maps attributes into a spawning subflow. + */ + private DefaultAttributeMapper inputMapper = new DefaultAttributeMapper(); + + /** + * The mapper that maps attributes returned by an ended subflow. + */ + private DefaultAttributeMapper outputMapper = new DefaultAttributeMapper(); + + /** + * Set the expression parser responsible for parsing expression strings into + * evaluatable expression objects. + */ + public void setExpressionParser(ExpressionParser expressionParser) { + Assert.notNull(expressionParser, "The expression parser is required"); + this.expressionParser = expressionParser; + } + + /** + * Adds a new input mapping. Use when you need full control over defining + * how a subflow input attribute mapping will be perfomed. + * @param inputMapping the input mapping + * @return this, to support call chaining + */ + public ConfigurableFlowAttributeMapper addInputMapping(AttributeMapper inputMapping) { + inputMapper.addMapping(inputMapping); + return this; + } + + /** + * Adds a collection of input mappings. Use when you need full control over + * defining how a subflow input attribute mapping will be perfomed. + * @param inputMappings the input mappings + */ + public void addInputMappings(AttributeMapper[] inputMappings) { + inputMapper.addMappings(inputMappings); + } + + /** + * Adds a new output mapping. Use when you need full control over defining + * how a subflow output attribute mapping will be perfomed. + * @param outputMapping the output mapping + * @return this, to support call chaining + */ + public ConfigurableFlowAttributeMapper addOutputMapping(AttributeMapper outputMapping) { + outputMapper.addMapping(outputMapping); + return this; + } + + /** + * Adds a collection of output mappings. Use when you need full control over + * defining how a subflow output attribute mapping will be perfomed. + * @param outputMappings the output mappings + */ + public void addOutputMappings(AttributeMapper[] outputMappings) { + outputMapper.addMappings(outputMappings); + } + + /** + * Adds an input mapping that maps a single attribute in parent flow + * scope into the subflow input map. For instance: "x" will result in + * the "x" attribute in parent flow scope being mapped into the subflow + * input map as "x". + * @param attributeName the attribute in flow scope to map into the subflow + * @return this, to support call chaining + */ + public ConfigurableFlowAttributeMapper addInputAttribute(String attributeName) { + SettableExpression attribute = expressionParser.parseSettableExpression(attributeName); + Expression scope = new AttributeExpression(attribute, ScopeType.FLOW); + addInputMapping(new Mapping(scope, attribute, null)); + return this; + } + + /** + * Adds a collection of input mappings that map attributes in parent flow + * scope into the subflow input map. For instance: "x" will result in + * the "x" attribute in parent flow scope being mapped into the subflow + * input map as "x". + * @param attributeNames the attributes in flow scope to map into the + * subflow + */ + public void addInputAttributes(String[] attributeNames) { + if (attributeNames == null) { + return; + } + for (int i = 0; i < attributeNames.length; i++) { + addInputAttribute(attributeNames[i]); + } + } + + /** + * Adds an output mapping that maps a single subflow output attribute into + * the flow scope of the resuming parent flow. For instance: "y" + * will result in the "y" attribute of the subflow output map being mapped + * into the flowscope of the resuming parent flow as "y". + * @param attributeName the subflow output attribute to map into the parent + * flow scope + * @return this, to support call chaining + */ + public ConfigurableFlowAttributeMapper addOutputAttribute(String attributeName) { + Expression attribute = expressionParser.parseExpression(attributeName); + SettableExpression scope = new AttributeExpression(attribute, ScopeType.FLOW); + addOutputMapping(new Mapping(attribute, scope, null)); + return this; + } + + /** + * Adds a collection of output mappings that map subflow output attributes + * into the scope of the resuming parent flow. For instance: "y" will result + * in the "y" attribute of the subflow output map being mapped into the + * flowscope of the resuming parent flow as "y". + * @param attributeNames the subflow output attributes to map into the + * parent flow + */ + public void addOutputAttributes(String[] attributeNames) { + if (attributeNames == null) { + return; + } + for (int i = 0; i < attributeNames.length; i++) { + addOutputAttribute(attributeNames[i]); + } + } + + /** + * Returns a typed-array of configured input mappings. + * @return the configured input mappings + */ + public AttributeMapper[] getInputMappings() { + return inputMapper.getMappings(); + } + + /** + * Returns a typed-array of configured output mappings. + * @return the configured output mappings + */ + public AttributeMapper[] getOutputMappings() { + return outputMapper.getMappings(); + } + + /** + * Returns the configured expression parser. Can be used by subclasses that + * build mappings. + */ + protected ExpressionParser getExpressionParser() { + return expressionParser; + } + + protected AttributeMapper getInputMapper() { + return inputMapper; + } + + protected AttributeMapper getOutputMapper() { + return outputMapper; + } + + public String toString() { + return new ToStringCreator(this).append("inputMapper", inputMapper).append("outputMapper", outputMapper) + .toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/DefaultTargetStateResolver.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/DefaultTargetStateResolver.java new file mode 100644 index 00000000..c3cc619d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/DefaultTargetStateResolver.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.support.StaticExpression; +import org.springframework.util.Assert; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.State; +import org.springframework.webflow.engine.TargetStateResolver; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.execution.RequestContext; + +/** + * A transition target state resolver that evaluates an expression to resolve + * the target state. The default implementation. + * + * @author Keith Donald + */ +public class DefaultTargetStateResolver implements TargetStateResolver { + + /** + * The expression for the target state identifier. + */ + private Expression targetStateIdExpression; + + /** + * Creates a new target state resolver that always returns the same + * target state id. + * @param targetStateId the id of the target state + */ + public DefaultTargetStateResolver(String targetStateId) { + this(new StaticExpression(targetStateId)); + } + + /** + * Creates a new target state resolver. + * @param targetStateIdExpression the target state id expression + */ + public DefaultTargetStateResolver(Expression targetStateIdExpression) { + Assert.notNull(targetStateIdExpression, "The target state id expression is required"); + this.targetStateIdExpression = targetStateIdExpression; + } + + public State resolveTargetState(Transition transition, State sourceState, RequestContext context) { + String stateId = String.valueOf(targetStateIdExpression.evaluate(context, null)); + return ((Flow)context.getActiveFlow()).getStateInstance(stateId); + } + + public String toString() { + return targetStateIdExpression.toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/EventIdTransitionCriteria.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/EventIdTransitionCriteria.java new file mode 100644 index 00000000..b70d30ac --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/EventIdTransitionCriteria.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import java.io.Serializable; + +import org.springframework.util.Assert; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +/** + * Simple transition criteria that matches on an eventId and nothing else. + * Specifically, if the id of the last event that occured equals + * {@link #getEventId()} this criteria will return true. + * + * @see RequestContext#getLastEvent() + * + * @author Erwin Vervaet + * @author Keith Donald + */ +public class EventIdTransitionCriteria implements TransitionCriteria, Serializable { + + /** + * The event id to match. + */ + private String eventId; + + /** + * Whether or not to match case sensitively. Default is true. + */ + private boolean caseSensitive = true; + + /** + * Create a new event id matching criteria object. + * @param eventId the event id + */ + public EventIdTransitionCriteria(String eventId) { + Assert.hasText(eventId, "The event id is required"); + this.eventId = eventId; + } + + /** + * Returns the event id to match. + */ + public String getEventId() { + return eventId; + } + + /** + * Set whether or not the event id should be matched in a case sensitve + * manner. Defaults to true. + */ + public void setCaseSensitive(boolean caseSensitive) { + this.caseSensitive = caseSensitive; + } + + public boolean test(RequestContext context) { + Event lastEvent = context.getLastEvent(); + if (lastEvent == null) { + return false; + } + if (caseSensitive) { + return eventId.equals(lastEvent.getId()); + } + else { + return eventId.equalsIgnoreCase(lastEvent.getId()); + } + } + + public String toString() { + return "[eventId = '" + eventId + "']"; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/ExternalRedirectSelector.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/ExternalRedirectSelector.java new file mode 100644 index 00000000..73f47311 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/ExternalRedirectSelector.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import java.io.Serializable; + +import org.springframework.binding.expression.Expression; +import org.springframework.core.style.ToStringCreator; +import org.springframework.webflow.engine.ViewSelector; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.support.ExternalRedirect; + +/** + * Makes view selections requesting a client side redirect to an external + * URL outside of the flow. + *

    + * This selector is usefull when you wish to request a redirect after + * conversation completion as part of entering an EndState. + *

    + * This selector may also be used to redirect to an external URL from a + * ViewState of an active conversation. The external system redirected to will + * be provided the flow execution context necessary to allow it to communicate + * back to the executing flow at a later time. + * + * @see org.springframework.webflow.execution.support.ExternalRedirect + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class ExternalRedirectSelector implements ViewSelector, Serializable { + + /** + * The parsed, evaluatable redirect URL expression. + */ + private Expression urlExpression; + + /** + * Create a new redirecting view selector that takes given URL expression as + * input. The expression is the parsed form (expression-tokenized) of the + * encoded view (e.g. "/pathInfo?param0=value0¶m1=value1"). + * @param urlExpression the url expression + */ + public ExternalRedirectSelector(Expression urlExpression) { + this.urlExpression = urlExpression; + } + + /** + * Returns the expression used by this view selector. + */ + public Expression getUrlExpression() { + return urlExpression; + } + + public boolean isEntrySelectionRenderable(RequestContext context) { + return true; + } + + public ViewSelection makeEntrySelection(RequestContext context) { + String url = (String)urlExpression.evaluate(context, null); + return new ExternalRedirect(url); + } + + public ViewSelection makeRefreshSelection(RequestContext context) { + return makeEntrySelection(context); + } + + public String toString() { + return new ToStringCreator(this).append("urlExpression", urlExpression).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/FlowDefinitionRedirectSelector.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/FlowDefinitionRedirectSelector.java new file mode 100644 index 00000000..e0eae9e1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/FlowDefinitionRedirectSelector.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.binding.expression.Expression; +import org.springframework.util.StringUtils; +import org.springframework.webflow.engine.ViewSelector; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; + +/** + * Makes a {@link FlowDefinitionRedirect} selection when requested, calculating the + * flowDefinitionId and executionInput by + * evaluating an expression against the request context. + * + * @see org.springframework.webflow.execution.support.FlowDefinitionRedirect + * + * @author Keith Donald + */ +public class FlowDefinitionRedirectSelector implements ViewSelector { + + /** + * The parsed flow expression, evaluatable to the string format: + * flowDefinitionId?param1Name=parmValue¶m2Name=paramValue. + */ + private Expression expression; + + /** + * Creates a new launch flow redirect selector. + * @param expression the parsed flow redirect expression, evaluatable to the + * string format: flowDefinitionId?param1Name=parmValue¶m2Name=paramValue + */ + public FlowDefinitionRedirectSelector(Expression expression) { + this.expression = expression; + } + + public boolean isEntrySelectionRenderable(RequestContext context) { + return true; + } + + public ViewSelection makeEntrySelection(RequestContext context) { + String encodedRedirect = (String)expression.evaluate(context, null); + if (encodedRedirect == null) { + throw new IllegalStateException( + "Flow definition redirect expression evaluated to [null], the expression was " + expression); + } + // the encoded FlowDefinitionRedirect should look something like + // "flowDefinitionId?param0=value0¶m1=value1" + // now parse that and build a corresponding view selection + int index = encodedRedirect.indexOf('?'); + String flowDefinitionId; + Map executionInput = null; + if (index != -1) { + flowDefinitionId = encodedRedirect.substring(0, index); + String[] parameters = StringUtils.delimitedListToStringArray(encodedRedirect.substring(index + 1), "&"); + executionInput = new HashMap(parameters.length, 1); + for (int i = 0; i < parameters.length; i++) { + String nameAndValue = parameters[i]; + index = nameAndValue.indexOf('='); + if (index != -1) { + executionInput.put(nameAndValue.substring(0, index), nameAndValue.substring(index + 1)); + } + else { + executionInput.put(nameAndValue, ""); + } + } + } + else { + flowDefinitionId = encodedRedirect; + } + if (!StringUtils.hasText(flowDefinitionId)) { + // equivalent to restart + flowDefinitionId = context.getFlowExecutionContext().getDefinition().getId(); + } + return new FlowDefinitionRedirect(flowDefinitionId, executionInput); + } + + public ViewSelection makeRefreshSelection(RequestContext context) { + return makeEntrySelection(context); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/NotTransitionCriteria.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/NotTransitionCriteria.java new file mode 100644 index 00000000..5b9e8951 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/NotTransitionCriteria.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import java.io.Serializable; + +import org.springframework.util.Assert; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.execution.RequestContext; + +/** + * Transition criteria that negates the result of the evaluation of + * another criteria object. + * + * @author Keith Donald + */ +public class NotTransitionCriteria implements TransitionCriteria, Serializable { + + /** + * The criteria to negate. + */ + private TransitionCriteria criteria; + + /** + * Create a new transition criteria object that will negate + * the result of given criteria object. + * @param criteria the criteria to negate + */ + public NotTransitionCriteria(TransitionCriteria criteria) { + Assert.notNull(criteria, "The criteria object to negate is required"); + this.criteria = criteria; + } + + public boolean test(RequestContext context) { + return !criteria.test(context); + } + + public String toString() { + return "[not(" + criteria + ")]"; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/SimpleFlowVariable.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/SimpleFlowVariable.java new file mode 100644 index 00000000..0d168a24 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/SimpleFlowVariable.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import java.lang.reflect.Modifier; + +import org.springframework.beans.BeanUtils; +import org.springframework.util.Assert; +import org.springframework.webflow.engine.FlowVariable; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ScopeType; + +/** + * A trivial concrete flow variable subclass that creates new variable values + * using Java reflection. + * + * @author Keith Donald + */ +public class SimpleFlowVariable extends FlowVariable { + + /** + * The concrete variable value class. + */ + private Class variableClass; + + /** + * Creates a new simple flow variable. + * @param name the variable name + * @param variableClass the concrete variable class + * @param scope the variable scope + */ + public SimpleFlowVariable(String name, Class variableClass, ScopeType scope) { + super(name, scope); + Assert.notNull(variableClass, "The variable class is required"); + Assert.isTrue(!variableClass.isInterface(), "The variable class cannot be an interface"); + Assert.isTrue(!Modifier.isAbstract(variableClass.getModifiers()), "The variable class cannot be abstract"); + this.variableClass = variableClass; + } + + /** + * Returns the variable value class. + */ + public Class getVariableClass() { + return variableClass; + } + + protected Object createVariableValue(RequestContext context) { + return BeanUtils.instantiateClass(variableClass); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/TransitionCriteriaChain.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/TransitionCriteriaChain.java new file mode 100644 index 00000000..ed9fe746 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/TransitionCriteriaChain.java @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.webflow.engine.AnnotatedAction; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.engine.WildcardTransitionCriteria; +import org.springframework.webflow.execution.RequestContext; + +/** + * An ordered chain of TransitionCriteria. Iterates over each element + * in the chain, continues until one returns false or the list is exhausted. So + * in effect it will do a logical AND between the contained criteria. + * + * @author Keith Donald + */ +public class TransitionCriteriaChain implements TransitionCriteria { + + /** + * The ordered chain of TransitionCriteria objects. + */ + private List criteriaChain = new LinkedList(); + + /** + * Creates an initially empty transition criteria chain. + * @see #add(TransitionCriteria) + */ + public TransitionCriteriaChain() { + } + + /** + * Creates a transition criteria chain with the specified criteria. + * @param criteria the criteria + */ + public TransitionCriteriaChain(TransitionCriteria[] criteria) { + criteriaChain.addAll(Arrays.asList(criteria)); + } + + /** + * Add given criteria object to the end of the chain. + * @param criteria the criteria + * @return this object, so multiple criteria can be added in a single + * statement + */ + public TransitionCriteriaChain add(TransitionCriteria criteria) { + this.criteriaChain.add(criteria); + return this; + } + + public boolean test(RequestContext context) { + Iterator it = criteriaChain.iterator(); + while (it.hasNext()) { + TransitionCriteria criteria = (TransitionCriteria)it.next(); + if (!criteria.test(context)) { + return false; + } + } + return true; + } + + public String toString() { + return new ToStringCreator(this).append("criteriaChain", criteriaChain).toString(); + } + + // static helpers + + /** + * Create a transition criteria chain chaining given list of actions. + * @param actions the actions (and their execution properties) to chain together + */ + public static TransitionCriteria criteriaChainFor(AnnotatedAction[] actions) { + if (actions == null || actions.length == 0) { + return WildcardTransitionCriteria.INSTANCE; + } + TransitionCriteriaChain chain = new TransitionCriteriaChain(); + for (int i = 0; i < actions.length; i++) { + chain.add(new ActionTransitionCriteria(actions[i])); + } + return chain; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/TransitionExecutingStateExceptionHandler.java b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/TransitionExecutingStateExceptionHandler.java new file mode 100644 index 00000000..c1ed6999 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/TransitionExecutingStateExceptionHandler.java @@ -0,0 +1,264 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.JdkVersion; +import org.springframework.core.NestedRuntimeException; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.engine.ActionList; +import org.springframework.webflow.engine.FlowExecutionExceptionHandler; +import org.springframework.webflow.engine.RequestControlContext; +import org.springframework.webflow.engine.TargetStateResolver; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.ViewSelection; + +/** + * A flow execution exception handler that maps the occurence of a specific type of + * exception to a transition to a new {@link org.springframework.webflow.engine.State}. + * + * @author Keith Donald + */ +public class TransitionExecutingStateExceptionHandler implements FlowExecutionExceptionHandler { + + private static final Log logger = LogFactory.getLog(TransitionExecutingStateExceptionHandler.class); + + /** + * The name of the attribute to expose a handled exception under in + * flash scope. + */ + public static final String STATE_EXCEPTION_ATTRIBUTE = "stateException"; + + /** + * The name of the attribute to expose a root cause of a handled exception + * under in flash scope. + */ + public static final String ROOT_CAUSE_EXCEPTION_ATTRIBUTE = "rootCauseException"; + + /** + * The exceptionType->targetStateResolver map. + */ + private Map exceptionTargetStateMappings = new HashMap(); + + /** + * The list of actions to execute when this handler handles an exception. + */ + private ActionList actionList = new ActionList(); + + /** + * Adds an exception->state mapping to this handler. + * @param exceptionClass the type of exception to map + * @param targetStateId the id of the state to transition to if the + * specified type of exception is handled + * @return this handler, to allow for adding multiple mappings in a single + * statement + */ + public TransitionExecutingStateExceptionHandler add(Class exceptionClass, String targetStateId) { + return add(exceptionClass, new DefaultTargetStateResolver(targetStateId)); + } + + /** + * Adds a exception->state mapping to this handler. + * @param exceptionClass the type of exception to map + * @param targetStateResolver the resolver to calculate the state to + * transition to if the specified type of exception is handled + * @return this handler, to allow for adding multiple mappings in a single + * statement + */ + public TransitionExecutingStateExceptionHandler add(Class exceptionClass, TargetStateResolver targetStateResolver) { + Assert.notNull(exceptionClass, "The exception class is required"); + Assert.notNull(targetStateResolver, "The target state resolver is required"); + exceptionTargetStateMappings.put(exceptionClass, targetStateResolver); + return this; + } + + /** + * Returns the list of actions to execute when this handler handles an exception. + * The returned list is mutable. + */ + public ActionList getActionList() { + return actionList; + } + + public boolean handles(FlowExecutionException e) { + return getTargetStateResolver(e) != null; + } + + public ViewSelection handle(FlowExecutionException e, RequestControlContext context) { + if (logger.isDebugEnabled()) { + logger.debug("Handling state exception " + e, e); + } + // expose state exception in flash scope so it's available for response rendering + context.getFlashScope().put(STATE_EXCEPTION_ATTRIBUTE, e); + Throwable rootCause = findRootCause(e); + if (logger.isDebugEnabled()) { + logger.debug("Exposing state exception root cause " + rootCause + " under attribute '" + + ROOT_CAUSE_EXCEPTION_ATTRIBUTE + "'"); + } + // expose root cause in flash scope so it's available for response rendering + context.getFlashScope().put(ROOT_CAUSE_EXCEPTION_ATTRIBUTE, rootCause); + actionList.execute(context); + return context.execute(new Transition(getTargetStateResolver(e))); + } + + // helpers + + /** + * Find the mapped target state resolver for given exception. Returns + * null if no mapping can be found for given exception. Will + * try all exceptions in the exception cause chain. + */ + protected TargetStateResolver getTargetStateResolver(FlowExecutionException e) { + if (JdkVersion.getMajorJavaVersion() == JdkVersion.JAVA_13) { + return getTargetStateResolver13(e); + } + else { + return getTargetStateResolver14(e); + } + } + + /** + * Internal getTargetStateResolver implementation for use with JDK 1.3. + */ + private TargetStateResolver getTargetStateResolver13(NestedRuntimeException e) { + TargetStateResolver targetStateResolver; + if (isRootCause13(e)) { + return findTargetStateResolver(e.getClass()); + } + else { + targetStateResolver = (TargetStateResolver)exceptionTargetStateMappings.get(e.getClass()); + if (targetStateResolver != null) { + return targetStateResolver; + } + else { + if (e.getCause() instanceof NestedRuntimeException) { + return getTargetStateResolver13((NestedRuntimeException)e.getCause()); + } + else { + return null; + } + } + } + } + + /** + * Internal getTargetStateResolver implementation for use with JDK 1.4 or later. + */ + private TargetStateResolver getTargetStateResolver14(Throwable t) { + TargetStateResolver targetStateResolver; + if (isRootCause14(t)) { + return findTargetStateResolver(t.getClass()); + } + else { + targetStateResolver = (TargetStateResolver)exceptionTargetStateMappings.get(t.getClass()); + if (targetStateResolver != null) { + return targetStateResolver; + } + else { + return getTargetStateResolver14(t.getCause()); + } + } + } + + /** + * Check if given exception is the root of the exception cause chain. + * For use with JDK 1.3. + */ + private boolean isRootCause13(NestedRuntimeException e) { + return e.getCause() == null; + } + + /** + * Check if given exception is the root of the exception cause chain. + * For use with JDK 1.4 or later. + */ + private boolean isRootCause14(Throwable t) { + return t.getCause() == null; + } + + /** + * Try to find a mapped target state resolver for given exception type. Will + * also try to lookup using the class hierarchy of given exception type. + * @param exceptionType the exception type to lookup + * @return the target state id or null if not found + */ + private TargetStateResolver findTargetStateResolver(Class exceptionType) { + while (exceptionType != null && exceptionType.getClass() != Object.class) { + if (exceptionTargetStateMappings.containsKey(exceptionType)) { + return (TargetStateResolver)exceptionTargetStateMappings.get(exceptionType); + } + else { + exceptionType = exceptionType.getSuperclass(); + } + } + return null; + } + + /** + * Find the root cause of given throwable. + */ + protected Throwable findRootCause(Throwable t) { + if (JdkVersion.getMajorJavaVersion() == JdkVersion.JAVA_13) { + return findRootCause13(t); + } + else { + return findRootCause14(t); + } + } + + /** + * Find the root cause of given throwable. For use on JDK 1.3. + */ + private Throwable findRootCause13(Throwable t) { + if (t instanceof NestedRuntimeException) { + NestedRuntimeException nre = (NestedRuntimeException)t; + Throwable cause = nre.getCause(); + if (cause == null) { + return nre; + } + else { + return findRootCause13(cause); + } + } + else { + return t; + } + } + + /** + * Find the root cause of given throwable. For use on JDK 1.4 or later. + */ + private Throwable findRootCause14(Throwable e) { + Throwable cause = e.getCause(); + if (cause == null) { + return e; + } + else { + return findRootCause14(cause); + } + } + + public String toString() { + return new ToStringCreator(this).append("exceptionTargetStateMappings", exceptionTargetStateMappings) + .toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/engine/support/package.html b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/package.html new file mode 100644 index 00000000..44d04e2b --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/engine/support/package.html @@ -0,0 +1,7 @@ + + +

    +Support implementations for internal engine-specific types. +

    + + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/Action.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/Action.java new file mode 100644 index 00000000..8d4d36f1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/Action.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +/** + * A command that executes a behavior and returns a logical execution result a + * calling flow execution can respond to. + *

    + * Actions typically delegate down to the application (or service) layer to + * perform business operations. They often retrieve data to support response + * rendering. They act as a bridge between a SWF web-tier and your middle-tier + * business logic layer. + *

    + * When an action completes execution it signals a result event describing the + * outcome of that execution (for example, "success", "error", "yes", "no", + * "tryAgain", etc). In addition to providing a logical outcome the flow can + * respond to, a result event may have payload associated with it, for example a + * "success" return value or an "error" error code. The result event is + * typically used as grounds for a state transition out of the current state of + * the calling Flow. + *

    + * Action implementations are often application-scoped singletons instantiated + * and managed by a web-tier Spring application context to take advantage of + * Spring's externalized configuration and dependency injection capabilities + * (which is a form of Inversion of Control [IoC]). Actions may also be stateful + * prototypes, storing conversational state as instance variables. Action + * instance definitions may also be locally scoped to a specific flow definition + * (see use of the "import" element of the root XML flow definition element.) + *

    + * Note: Actions are directly instantiatable for use in a standalone test + * environment and can be parameterized with mocks or stubs, as they are simple + * POJOs. Action proxies may also be generated at runtime for delegating to POJO + * business operations that have no dependency on the Spring Web Flow API. + *

    + * Note: if an Action is a singleton managed in application scope, take care not + * to store and/or modify caller-specific state in a unsafe manner. The Action + * {@link #execute(RequestContext)} method runs in an independently executing + * thread on each invocation so make sure you deal only with local data or + * internal, thread-safe services. + *

    + * Note: an Action is not a controller like a Spring MVC controller or a Struts + * action is a controller. Flow actions are commands. Such commands do + * not select views, they execute arbitrary behavioral logic and then return an + * logical execution result. The flow that invokes an Action is responsible for + * responding to the execution result to decide what to do next. In Spring Web + * Flow, the flow is the controller. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface Action { + + /** + * Execute this action. Action execution will occur in the context of a + * request associated with an active flow execution. + *

    + * Action invocation is typically triggered in a production environment by a + * state within a flow carrying out the execution of a flow definition. The + * result of action execution, a logical outcome event, can be used as + * grounds for a transition out of the calling state. + *

    + * Note: The {@link RequestContext} argument to this method provides access + * to data about the active flow execution in the context of the currently + * executing thread. Among other things, this allows this action to access + * {@link RequestContext#getRequestScope() data} set by other actions, as + * well as set its own attributes it wishes to expose in a given scope. + *

    + * Some notes about actions and their usage of the attribute scope types: + *

      + *
    • Attributes set in + * {@link RequestContext#getRequestScope() request scope} exist for the life + * of the currently executing request only. + *
    • Attributes set in {@link RequestContext#getFlashScope() flash scope} + * exist until the next external user event is signaled. That time includes + * the current request plus any redirect or additional refreshes to the next + * view. + *
    • Attributes set in {@link RequestContext#getFlowScope() flow scope} + * exist for the life of the flow session and will be + * cleaned up automatically when the flow session ends. + *
    • Attributes set in + * {@link RequestContext#getConversationScope() conversation scope} exist + * for the life of the entire flow execution representing a single logical + * "conversation" with a user. + *
    + *

    + * All attributes present in any scope are typically exposed in a model + * for access by a view when an "interactive" state type such as a view + * state is entered. + *

    + * Note: flow scope should generally not be used as a general purpose cache, + * but rather as a context for data needed locally by other states of the + * flow this action participates in. For example, it would be inappropriate + * to stuff large collections of objects (like those returned to support a + * search results view) into flow scope. Instead, put such result + * collections in request scope, and ensure you execute this action again + * each time you wish to view those results. 2nd level caches managed + * outside of SWF are more general cache solutions. + *

    + * Note: as flow scoped attributes are eligible for serialization they + * should be Serializable. + * + * @param context the action execution context, for accessing and setting + * data in a {@link ScopeType scope type}, as well as obtaining other flow + * contextual information (e.g. request context attributes and flow + * execution context information) + * @return a logical result outcome, used as grounds for a transition in the + * calling flow (e.g. "success", "error", "yes", "no", * ...) + * @throws Exception a exception occured during action execution, either + * checked or unchecked; note, any recoverable exceptions should be + * caught within this method and an appropriate result outcome returned + * or be handled by the current state of the calling flow execution. + */ + public Event execute(RequestContext context) throws Exception; +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/EnterStateVetoException.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/EnterStateVetoException.java new file mode 100644 index 00000000..c3b439a1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/EnterStateVetoException.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +import org.springframework.webflow.definition.StateDefinition; + +/** + * Exception thrown to veto the entering of a state of a flow. Typically thrown + * by {@link FlowExecutionListener} objects that apply security or other runtime + * constraint checks to flow executions. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class EnterStateVetoException extends FlowExecutionException { + + /** + * The state whose entering was vetoed. + */ + private String vetoedStateId; + + /** + * Create a new enter state veto exception. + * @param flowId the active flow + * @param sourceStateId the current state when the veto operation occured + * @param vetoedStateId the state for which entering is vetoed + * @param message a descriptive message + */ + public EnterStateVetoException(String flowId, String sourceStateId, String vetoedStateId, String message) { + super(flowId, sourceStateId, message); + this.vetoedStateId = vetoedStateId; + } + + /** + * Create a new enter state veto exception. + * @param flowId the active flow + * @param sourceStateId the current state when the veto operation occured + * @param vetoedStateId the state for which entering is vetoed + * @param message a descriptive message + * @param cause the underlying cause + */ + public EnterStateVetoException(String flowId, String sourceStateId, String vetoedStateId, String message, Throwable cause) { + super(flowId, sourceStateId, message, cause); + this.vetoedStateId = vetoedStateId; + } + + /** + * Create a new enter state veto exception. + * @param context the flow execution request context + * @param vetoedState the state for which entering is vetoed + * @param message a descriptive message + */ + public EnterStateVetoException(RequestContext context, StateDefinition vetoedState, String message) { + this(context.getActiveFlow().getId(), context.getCurrentState().getId(), vetoedState.getId(), message); + } + + /** + * Create a new enter state veto exception. + * @param context the flow execution request context + * @param vetoedState the state for which entering is vetoed + * @param message a descriptive message + * @param cause the underlying cause + */ + public EnterStateVetoException(RequestContext context, StateDefinition vetoedState, String message, Throwable cause) { + this(context.getActiveFlow().getId(), context.getCurrentState().getId(), vetoedState.getId(), message, cause); + } + + /** + * Returns the state for which entering was vetoed. + */ + public String getVetoedStateId() { + return vetoedStateId; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/Event.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/Event.java new file mode 100644 index 00000000..a0383e1c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/Event.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +import java.util.EventObject; + +import org.springframework.util.Assert; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.CollectionUtils; + +/** + * Signals the occurrence of something an active flow execution should respond + * to. Each event has a string id that provides a key for identifying what + * happened: e.g "coinInserted", or "pinDropped". Events may have attributes + * that provide arbitrary payload data, e.g. "coin.amount=25", or + * "pinDropSpeed=25ms". + *

    + * As an example, a "submit" event might signal that a Submit button was pressed + * in a web browser. A "success" event might signal an action executed + * successfully. A "finish" event might signal a subflow ended normally. + *

    + * Why is this not an interface? A specific design choice. An event is not a + * strategy that defines a generic type or role--it is essentially an immutable + * value object. It is expected that specializations of this base class be + * "Events" and not part of some other inheritence hierarchy. + * + * @author Keith Donald + * @author Erwin Vervaet + * @author Colin Sampaleanu + */ +public final class Event extends EventObject { + + /** + * The event identifier. + */ + private final String id; + + /** + * The time the event occured. + */ + private final long timestamp = System.currentTimeMillis(); + + /** + * Additional event attributes that form this event's payload. + */ + private final AttributeMap attributes; + + /** + * Create a new event with the specified id and no payload. + * @param source the source of the event (required) + * @param id the event identifier (required) + */ + public Event(Object source, String id) { + this(source, id, null); + } + + /** + * Create a new event with the specified id and payload + * attributes. + * @param source the source of the event (required) + * @param id the event identifier (required) + * @param attributes additional event attributes + */ + public Event(Object source, String id, AttributeMap attributes) { + super(source); + Assert.hasText(id, "The event id is required: please set this event's id to a non-blank string identifier"); + this.id = id; + this.attributes = (attributes != null ? attributes : CollectionUtils.EMPTY_ATTRIBUTE_MAP); + } + + /** + * Returns the event identifier. + * @return the event id + */ + public String getId() { + return id; + } + + /** + * Returns the time at which the event occured, represented as the number of + * milliseconds since January 1, 1970, 00:00:00 GMT. + * @return the timestamp + */ + public long getTimestamp() { + return timestamp; + } + + /** + * Returns an unmodifiable map storing the attributes of this event. Never + * returns null. + * @return the event attributes (payload) + */ + public AttributeMap getAttributes() { + return attributes; + } + + public String toString() { + return getId(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecution.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecution.java new file mode 100644 index 00000000..f5dbdc4f --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecution.java @@ -0,0 +1,125 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; + +/** + * A top-level instance of a flow definition that carries out definition + * execution on behalf of a single client. Typically used to support the + * orchestration of a web conversation. + *

    + * This is the central facade interface for manipulating one runtime execution + * of a flow definition. Implementations of this interface are the finite state + * machine that is the heart of Spring Web Flow. + *

    + * Typically, when a client wants to launch a flow execution at production time, + * she passes the id of the governing {@link FlowDefinition flow definition} to + * a coordinating + * {@link org.springframework.webflow.executor.FlowExecutor#launch(String, ExternalContext) flow executor}. + * This coordinator then typically uses a + * {@link FlowExecutionFactory flow execution factory} to create an object + * implementing this interface, initializing it with the requested flow + * definition which becomes the execution's "root" or top-level flow. + *

    + * After execution creation, the + * {@link #start(MutableAttributeMap, ExternalContext) start} operation is + * called, which causes this execution to activate a new + * {@link FlowSession session} for its root flow definition. That session is + * then said to become the active flow. An execution + * {@link RequestContext request context} is created, the Flow's + * {@link FlowDefinition#getStartState() start state} is entered, and the + * request is processed. + *

    + * In a distributed environment such as HTTP, after a call into this object has + * completed and control returns to the caller, this execution object (if still + * active) is typically saved out to a repository before the server request + * ends. For example it might be saved out to the HttpSession, a Database, or a + * client-side hidden form field for later restoration and manipulation. This + * execution persistence is the responsibility of the + * {@link org.springframework.webflow.execution.repository.FlowExecutionRepository flow execution repository} + * subsystem. + *

    + * Subsequent requests from the client to manipuate this flow execution trigger + * restoration of this object, followed by an invocation of the + * {@link #signalEvent(String, ExternalContext) signal event} operation. The + * signalEvent operation resumes this execution by indicating what action the + * user took from within the current state; for example, the user may have + * pressed the "submit" button, or pressed "cancel". After the user + * event is processed, control again goes back to the caller and if this + * execution is still active, it is again saved out to the repository. + *

    + * This process continues until a client event causes this flow execution to end + * (by the root flow reaching an end state). At that time this object is no + * longer active, and is removed from the repository and discarded. + *

    + * Flow executions can have their lifecycle observed by {@link FlowExecutionListener listeners}. + * + * @see FlowDefinition + * @see FlowSession + * @see RequestContext + * @see FlowExecutionListener + * @see org.springframework.webflow.execution.repository.FlowExecutionRepository + * @see org.springframework.webflow.executor.FlowExecutor + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface FlowExecution extends FlowExecutionContext { + + /** + * Start this flow execution, transitioning it to the root flow's start + * state and returning the starting view selection needed to issue an + * initial user response. Typically called by a flow executor on behalf of a + * browser client, but also from test code. + *

    + * This will start the entire flow execution from scratch. + * @param input input attributes to pass to the flow, which the flow may + * choose to map into its scope + * @param context the external context in which the starting event occured + * @return the starting view selection, a value object to be used to issue a + * suitable response to the caller + * @throws FlowExecutionException if an exception was thrown within a state + * of the flow execution during request processing + */ + public ViewSelection start(MutableAttributeMap input, ExternalContext context) throws FlowExecutionException; + + /** + * Signal an occurrence of the specified user event in the current state of + * this executing flow. The event will be processed in full and control will + * be returned once event processing is complete. + * @param eventId the identifier of the user event that occured + * @param context the external context in which the event occured + * @return the next view selection to render, used by the calling executor + * to issue a suitable response to the client + * @throws FlowExecutionException if an exception was thrown within a state + * of the resumed flow execution during event processing + */ + public ViewSelection signalEvent(String eventId, ExternalContext context) throws FlowExecutionException; + + /** + * Refresh this flow execution, asking the current view selection to be + * reconstituted to support reissuing the last response. This is an idempotent + * operation that may be safely called on a paused execution. + * @param context the externa context in which the refresh event occured + * @return the current view selection for this flow execution + * @throws FlowExecutionException if an exception was thrown within a state + * of the resumed flow execution during event processing + */ + public ViewSelection refresh(ExternalContext context) throws FlowExecutionException; +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionContext.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionContext.java new file mode 100644 index 00000000..0dfb0ed1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionContext.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; + +/** + * Provides contextual information about a flow execution. A flow execution is + * an runnable instance of a {@link FlowDefinition}. In other words, it is the + * central Spring Web Flow construct for carrying out a conversation with a + * client. This immutable interface provides access to runtime information + * about the conversation, such as it's {@link #isActive() status} and + * {@link #getActiveSession() current state}. + *

    + * An object implementing this interface is also traversable from a execution + * request context (see + * {@link org.springframework.webflow.execution.RequestContext#getFlowExecutionContext()}). + *

    + * This interface provides information that may span more than one request in a + * thread safe manner. The {@link RequestContext} interface defines a request + * specific control interface for manipulating exactly one flow execution + * locally from exactly one request. + * + * @see FlowDefinition + * @see FlowSession + * @see RequestContext + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface FlowExecutionContext { + + /** + * Returns the root flow definition associated with this executing flow. + *

    + * A call to this method always returns the same flow definition -- the + * top-level "root" -- no matter what flow may actually be active (for + * example, if subflows have been spawned). + * @return the root flow definition + */ + public FlowDefinition getDefinition(); + + /** + * Is the flow execution active? + *

    + * All methods on an active flow execution context can be called + * successfully. If the flow execution is not active, a caller cannot access + * some methods such as {@link #getActiveSession()}. + * @return true if active, false if the flow execution has terminated + */ + public boolean isActive(); + + /** + * Returns the active flow session of this flow execution. The active flow + * session is the currently executing session -- it may be the "root flow" + * session, or it may be a subflow session if this flow execution has + * spawned a subflow. + * @return the active flow session + * @throws IllegalStateException if this flow execution has not been started + * at all or if this execution has ended and is no longer actively executing + */ + public FlowSession getActiveSession() throws IllegalStateException; + + /** + * Returns a mutable map for data held in "conversation scope". Conversation + * scope is a data structure that exists for the life of this flow execution + * and is accessible to all flow sessions. + * @return conversation scope + */ + public MutableAttributeMap getConversationScope(); + + /** + * Returns runtime execution attributes that may influence the behavior of + * flow artifacts, such as states and actions. + * @return execution attributes + */ + public AttributeMap getAttributes(); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionException.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionException.java new file mode 100644 index 00000000..8ba250a1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionException.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +import org.springframework.webflow.core.FlowException; + +/** + * Base class for exceptions that occur within a flow while it is executing. Can + * be used directly, but you are encouraged to create a specific subclass for a + * particular use case. + *

    + * Execution exceptions occur at runtime when the flow is executing requests on + * behalf of a client. They signal that an execution problem occured: e.g. + * action execution failed or no transition matched the current request context. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class FlowExecutionException extends FlowException { + + /** + * The id of the flow definition in which the exception occured. + */ + private String flowId; + + /** + * The state of the flow where the exception occured (optional). + */ + private String stateId; + + /** + * Creates a new flow execution exception. + * @param flowId the flow where the exception occured + * @param stateId the state where the exception occured + * @param message a descriptive message + */ + public FlowExecutionException(String flowId, String stateId, String message) { + super(message); + this.stateId = stateId; + this.flowId = flowId; + } + + /** + * Creates a new flow execution exception. + * @param flowId the flow where the exception occured + * @param stateId the state where the exception occured + * @param message a descriptive message + * @param cause the root cause + */ + public FlowExecutionException(String flowId, String stateId, String message, Throwable cause) { + super(message, cause); + this.stateId = stateId; + this.flowId = flowId; + } + + /** + * Returns the id of the flow definition that was executing when this + * exception occured. + */ + public String getFlowId() { + return flowId; + } + + /** + * Returns the id of the state definition where the exception occured. Could + * be null if no state was active at the time when the exception was thrown. + */ + public String getStateId() { + return stateId; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionFactory.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionFactory.java new file mode 100644 index 00000000..5b38efd8 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionFactory.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +import org.springframework.webflow.definition.FlowDefinition; + +/** + * An abstract factory for creating flow exections. A flow execution represents + * a runtime, top-level instance of a flow definition. + *

    + * This factory provides encapsulation of the flow execution implementation + * type, as well as its construction and assembly process. + *

    + * Flow execution factories are responsible for registering + * {@link FlowExecutionListener listeners} with the constructed flow execution. + * + * @see FlowExecution + * @see FlowDefinition + * @see FlowExecutionListener + * + * @author Keith Donald + */ +public interface FlowExecutionFactory { + + /** + * Create a new flow execution product for the given flow definition. + * @param flowDefinition the flow definition + * @return the new flow execution, fully initialized and awaiting to be + * started + * @see FlowExecution#start(org.springframework.webflow.core.collection.MutableAttributeMap, org.springframework.webflow.context.ExternalContext) + */ + public FlowExecution createFlowExecution(FlowDefinition flowDefinition); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionListener.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionListener.java new file mode 100644 index 00000000..a5eb059c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionListener.java @@ -0,0 +1,155 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; +import org.springframework.webflow.engine.FlowExecutionExceptionHandler; + +/** + * Interface to be implemented by objects that wish to listen and respond to the + * lifecycle of {@link FlowExecution flow executions}. + *

    + * An 'observer' that is very aspect like, allowing you to insert 'cross + * cutting' behavior at well-defined points within one or more well-defined flow + * execution lifecycles. + *

    + * For example, one custom listener may apply security checks at the flow + * execution level, preventing a flow from starting or a state from entering if + * the curent user does not have the necessary permissions. Another listener may + * track flow execution navigation history to support bread crumbs. Another may + * perform auditing, or setup and tear down connections to a transactional + * resource. + *

    + * Note that flow execution listeners are registered with a flow execution when + * that execution is created by a {@link FlowExecutionFactory factory} or + * restored by a {@link org.springframework.webflow.execution.repository.FlowExecutionRepository}. + * Typically a listener will not be registered with a flow execution at + * runtime, when the flow execution is already active. + * + * @see FlowDefinition + * @see StateDefinition + * @see FlowExecution + * @see RequestContext + * @see Event + * @see ViewSelection + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface FlowExecutionListener { + + /** + * Called when any client request is submitted to manipulate this flow + * execution. This call happens before request processing. + * @param context the source of the event + */ + public void requestSubmitted(RequestContext context); + + /** + * Called when a client request has completed processing. + * @param context the source of the event + */ + public void requestProcessed(RequestContext context); + + /** + * Called immediately after a start event is signaled, indicating a new + * session of the flow is starting but has not yet entered its start state. + * An exception may be thrown from this method to veto the start operation. + * Any type of runtime exception can be used for this purpose. + * @param context the source of the event + * @param definition the flow for which a new session is starting + * @param input a mutable input map to the starting flow session + */ + public void sessionStarting(RequestContext context, FlowDefinition definition, MutableAttributeMap input); + + /** + * Called when a new flow session has started. At this point the start state + * has been entered. + * @param context the source of the event + * @param session the session that was started + */ + public void sessionStarted(RequestContext context, FlowSession session); + + /** + * Called when an event is signaled in the current state, but prior to any + * state transition. + * @param context the source of the event + * @param event the event that occured + */ + public void eventSignaled(RequestContext context, Event event); + + /** + * Called when a state transitions, after the transition is matched but + * before the transition occurs. + * @param context the source of the event + * @param state the proposed state to transition to + * @throws EnterStateVetoException when entering the state is not allowed + */ + public void stateEntering(RequestContext context, StateDefinition state) throws EnterStateVetoException; + + /** + * Called when a state transitions, after the transition occured. + * @param context the source of the event + * @param previousState from state of the transition + * @param state to state of the transition + */ + public void stateEntered(RequestContext context, StateDefinition previousState, StateDefinition state); + + /** + * Called when a flow execution is paused, for instance when it is waiting + * for user input (after event processing). + * @param context the source of the event + * @param selectedView the view that will display + */ + public void paused(RequestContext context, ViewSelection selectedView); + + /** + * Called after a flow execution is successfully reactivated after pause + * (but before event processing). + * @param context the source of the event + */ + public void resumed(RequestContext context); + + /** + * Called when the active flow execution session has been asked to end but + * before it has ended. + * @param context the source of the event + * @param session the current active session that is ending + * @param output the flow output produced by the ending session, this map may + * be modified by this listener to affect the output returned + */ + public void sessionEnding(RequestContext context, FlowSession session, MutableAttributeMap output); + + /** + * Called when a flow execution session ends. If the ended session was the + * root session of the flow execution, the entire flow execution also ends. + * @param context the source of the event + * @param session ending flow session + * @param output final, unmodifiable output returned by the ended session + */ + public void sessionEnded(RequestContext context, FlowSession session, AttributeMap output); + + /** + * Called when an exception is thrown during a flow execution, before the + * exception is handled by any registered {@link FlowExecutionExceptionHandler handler}. + * @param context the source of the exception + * @param exception the exception that occurred + */ + public void exceptionThrown(RequestContext context, FlowExecutionException exception); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionListenerAdapter.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionListenerAdapter.java new file mode 100644 index 00000000..afc7bf7d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowExecutionListenerAdapter.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; + +/** + * An abstract adapter class for listeners (observers) of flow execution + * lifecycle events. The methods in this class are empty. This class exists as + * convenience for creating listener objects; subclass it and override what you + * need. + * + * @author Erwin Vervaet + * @author Keith Donald + */ +public abstract class FlowExecutionListenerAdapter implements FlowExecutionListener { + + public void requestSubmitted(RequestContext context) { + } + + public void requestProcessed(RequestContext context) { + } + + public void sessionStarting(RequestContext context, FlowDefinition definition, MutableAttributeMap input) { + } + + public void sessionStarted(RequestContext context, FlowSession session) { + } + + public void eventSignaled(RequestContext context, Event event) { + } + + public void stateEntering(RequestContext context, StateDefinition state) throws EnterStateVetoException { + } + + public void stateEntered(RequestContext context, StateDefinition previousState, StateDefinition newState) { + } + + public void resumed(RequestContext context) { + } + + public void paused(RequestContext context, ViewSelection selectedView) { + } + + public void sessionEnding(RequestContext context, FlowSession session, MutableAttributeMap output) { + } + + public void sessionEnded(RequestContext context, FlowSession session, AttributeMap output) { + } + + public void exceptionThrown(RequestContext context, FlowExecutionException exception) { + } + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowSession.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowSession.java new file mode 100644 index 00000000..ae8db4cf --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowSession.java @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; + +/** + * A single, local instantiation of a {@link FlowDefinition flow definition} + * launched within an overall flow execution. + *

    + * This object maintains all instance state including session status within + * exactly one governing FlowExecution, as well as the current flow state. This + * object also acts as the local "flow scope" data model. Data in + * {@link #getScope() flow scope} lives for the life of this object and is + * cleaned up automatically when this object is destroyed. Destruction happens + * when this session enters an end state. + *

    + * A flow session will go through several status changes during its lifecycle. + * Initially it will be {@link FlowSessionStatus#CREATED} when a new execution + * is started. + *

    + * After passing through the {@link FlowSessionStatus#STARTING} status, the flow + * session is activated (about to be manipulated) and its status becomes + * {@link FlowSessionStatus#ACTIVE}. In the case of a new execution session + * activation happens immediately after creation to put the "root flow" at the + * top of the stack and transition it to its start state. + *

    + * When control returns to the client for user think time the status is updated + * to {@link FlowSessionStatus#PAUSED}. The flow is no longer actively + * processing then, as it is stored off to a repository waiting on the user to + * resume. + *

    + * If a flow session is pushed down in the stack because a subflow is spawned, + * its status becomes {@link FlowSessionStatus#SUSPENDED} until the subflow + * returns (ends) and is popped off the stack. The resuming flow session then + * becomes active once again. + *

    + * When a flow session is terminated because an EndState is reached its status + * becomes {@link FlowSessionStatus#ENDED}, which ends its life. When this + * happens the session is popped off the stack and discarded, and any allocated + * resources in "flow scope" are automatically cleaned up. + *

    + * Note that a flow session is in no way linked to an HTTP session. It + * just uses the familiar "session" naming convention to denote a stateful + * object. + * + * @see FlowDefinition + * @see FlowExecution + * @see FlowSessionStatus + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface FlowSession { + + /** + * Returns the flow definition backing this session. + */ + public FlowDefinition getDefinition(); + + /** + * Returns the current state of this flow session. This value changes as the + * flow executes. + */ + public StateDefinition getState(); + + /** + * Returns the current status of this flow session. This value changes as + * the flow executes. + */ + public FlowSessionStatus getStatus(); + + /** + * Return this session's local attributes; the basis for "flow scope" (flow + * session scope). + * @return the flow scope attributes + */ + public MutableAttributeMap getScope(); + + /** + * Returns the local "flash map". Attributes in this map are cleared out + * on the next event signaled in the flow execution, so they survive a refresh. + * @return the flash map + */ + public MutableAttributeMap getFlashMap(); + + /** + * Returns the parent flow session in the current flow execution, or + * null if there is no parent flow session. + */ + public FlowSession getParent(); + + /** + * Returns whether this flow session is the root flow session in the ongoing + * flow execution. The root flow session does not have a parent flow + * session. + */ + public boolean isRoot(); + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowSessionStatus.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowSessionStatus.java new file mode 100644 index 00000000..4bde34fa --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/FlowSessionStatus.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +import org.springframework.core.enums.StaticLabeledEnum; + +/** + * Type-safe enumeration of possible flow session statuses. Consult the + * JavaDoc for the {@link FlowSession} for more information on how these + * statuses are used during the life cycle of a flow session. + * + * @see org.springframework.webflow.execution.FlowSession + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class FlowSessionStatus extends StaticLabeledEnum { + + /** + * Initial status of a flow session; the session has been created but not + * yet activated. + */ + public static FlowSessionStatus CREATED = new FlowSessionStatus(0, "Created"); + + /** + * A flow session with STARTING status is about to enter its start state. + */ + public static FlowSessionStatus STARTING = new FlowSessionStatus(1, "Starting"); + + /** + * A flow session with ACTIVE status is currently executing. + */ + public static FlowSessionStatus ACTIVE = new FlowSessionStatus(2, "Active"); + + /** + * A flow session with PAUSED status is currently waiting on the user to + * signal an event. + */ + public static FlowSessionStatus PAUSED = new FlowSessionStatus(3, "Paused"); + + /** + * A flow session that is SUSPENDED is not actively executing a flow. It is + * waiting for subflow execution to complete before continuing. + */ + public static FlowSessionStatus SUSPENDED = new FlowSessionStatus(4, "Suspended"); + + /** + * A flow session that has ENDED is no longer actively executing a flow. + * This is the final status of a flow session. + */ + public static FlowSessionStatus ENDED = new FlowSessionStatus(5, "Ended"); + + /** + * Private constructor because this is a typesafe enum! + */ + private FlowSessionStatus(int code, String label) { + super(code, label); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/RequestContext.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/RequestContext.java new file mode 100644 index 00000000..07215276 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/RequestContext.java @@ -0,0 +1,205 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.core.collection.ParameterMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; +import org.springframework.webflow.definition.TransitionDefinition; + +/** + * A context for a single request to manipulate a flow execution. Allows Web + * Flow users to access contextual information about the executing request, as + * well as the governing + * {@link #getFlowExecutionContext() active flow execution}. + *

    + * The term request is used to describe a single call (thread) into the + * flow system by an external actor to manipulate exactly one flow execution. + *

    + * A new instance of this object is typically created when one of the core + * operations supported by a flow execution is invoked, either + * start to launch the flow execution, signalEvent + * to resume the flow execution, or refresh to reconstitute the + * flow execution's last view selection for purposes of reissuing a user + * response. + *

    + * Once created this context object is passed around throughout flow execution + * request processing where it may be accessed and reasoned upon by SWF-internal + * artifacts such as states, user-implemented action code, and state transition + * criteria. + *

    + * When a call into a flow execution returns this object goes out of scope and + * is disposed of automatically. Thus a request context is an internal artifact + * used within a FlowExecution: this object is not exposed to external client + * code, e.g. a view implementation (JSP). + *

    + * The {@link #getRequestScope() requestScope} property may be used as a store + * for arbitrary data that should exist for the life of this object. + * Request-scoped data, along with all data in {@link #getFlashScope() flash scope}, + * {@link #getFlowScope() flow scope} and + * {@link #getConversationScope() conversation scope} is available for exposing + * to view templates via a {@link #getModel() model} property. + *

    + * The web flow system will ensure that a RequestContext object is local to the + * current thread. It can be safely manipulated without needing to worry about + * concurrent access. + *

    + * Note: this request context is in no way linked to an HTTP or Portlet request. + * It uses the familiar "request" naming convention to indicate a single call to + * manipulate a runtime execution of a flow definition. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface RequestContext { + + /** + * Returns the definition of the flow that is currently executing. + * @return the flow definition for the active session + * @throws IllegalStateException if the flow execution has not been started + * at all, or if the execution has ended and is no longer actively executing + */ + public FlowDefinition getActiveFlow() throws IllegalStateException; + + /** + * Returns the current state of the executing flow. May return + * null if this flow execution is in the process of starting + * and has not yet entered its start state. + * @return the current state, or null if in the process of + * starting + * @throws IllegalStateException if this flow execution has not been started + * at all, or if this execution has ended and is no longer actively + * executing + */ + public StateDefinition getCurrentState() throws IllegalStateException; + + /** + * Returns a mutable accessor for accessing and/or setting attributes in + * request scope. Request scoped attributes exist for the duration of + * this request only. + * @return the request scope + */ + public MutableAttributeMap getRequestScope(); + + /** + * Returns a mutable accessor for accessing and/or setting attributes in + * flash scope. Flash scoped attributes exist untill the next event + * is signaled in the flow execution. + * @return the flash scope + */ + public MutableAttributeMap getFlashScope(); + + /** + * Returns a mutable accessor for accessing and/or setting attributes in + * flow scope. Flow scoped attributes exist for the life of the active + * flow session. + * @return the flow scope + * @see FlowSession + */ + public MutableAttributeMap getFlowScope(); + + /** + * Returns a mutable accessor for accessing and/or setting attributes in + * conversation scope. Conversation scoped attributes exist for the life + * of the executing flow and are shared across all flow sessions. + * @return the conversation scope + * @see FlowExecutionContext + */ + public MutableAttributeMap getConversationScope(); + + /** + * Returns the immutable input parameters associated with this request into + * Spring Web Flow. The map returned is immutable and cannot be changed. + *

    + * This is typically a convenient shortcut for accessing the + * {@link ExternalContext#getRequestParameterMap()} directly. + * @see #getExternalContext() + */ + public ParameterMap getRequestParameters(); + + /** + * Returns the external client context that originated (or triggered) this + * request. + *

    + * Acting as a facade, the returned context object provides a single point + * of access to the calling client's environment. It provides normalized + * access to attributes of the client environment without tying you to + * specific constructs within that environment. + *

    + * In addition, this context may be downcastable to a specific context type + * for a specific client environment, such as a + * {@link org.springframework.webflow.context.servlet.ServletExternalContext} + * for servlets or a + * {@link org.springframework.webflow.context.portlet.PortletExternalContext} + * for portlets. Such downcasting will give you full access to a native + * HttpServletRequest, for example. With that said, for portability reasons + * you should avoid coupling your flow artifacts to a specific deployment + * environment when possible. + * @return the originating external context, the one that triggered the + * current execution request + */ + public ExternalContext getExternalContext(); + + /** + * Returns contextual information about the flow execution itself. + * Information in this context typically spans more than one request. + * @return the flow execution context + */ + public FlowExecutionContext getFlowExecutionContext(); + + /** + * Returns the last event signaled during this request. The event may or may + * not have caused a state transition to happen. + * @return the last signaled event + */ + public Event getLastEvent(); + + /** + * Returns the last state transition that executed in this request. + * @return the last transition, or null if no transition has + * occured yet + */ + public TransitionDefinition getLastTransition(); + + /** + * Returns a context map for accessing arbitrary attributes about the state + * of the current request. These attributes may be used to influence flow + * execution behavior. + * @return the current attributes of this request, or empty if not set + */ + public AttributeMap getAttributes(); + + /** + * Set the contextual attributes describing the state of this request. + * Overwrites any pre-existing collection. + * @param attributes the attributes + */ + public void setAttributes(AttributeMap attributes); + + /** + * Returns the data model capturing the state of this context, suitable for + * exposing to clients (mostly web views). Typically the model will contain + * the union of the data available in request, flash, session and conversation + * scope. + * @return the model that can be exposed to a client view for rendering + * purposes + */ + public AttributeMap getModel(); + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/ScopeType.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/ScopeType.java new file mode 100644 index 00000000..41e499df --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/ScopeType.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +import org.springframework.core.enums.StaticLabeledEnum; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; + +/** + * An enumeration of the core scope types of Spring Web Flow. Provides easy + * access to each scope by type using + * {@link #getScope(RequestContext)}. + *

    + * A "scope" defines a data structure for storing custom user attributes within + * a flow execution. Different scope types have different semantics in terms of + * how long attributes placed in those scope maps remain valid. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public abstract class ScopeType extends StaticLabeledEnum { + + /** + * The "request" scope type. Attributes placed in request scope exist for + * the life of the current request into the flow execution. When the request + * ends any attributes in request scope go out of scope. + */ + public static final ScopeType REQUEST = new ScopeType(0, "Request") { + public MutableAttributeMap getScope(RequestContext context) { + return context.getRequestScope(); + } + }; + + /** + * The "flash" scope type. Attributes placed in flash scope exist through + * the life of the current request and until the next user event is + * signaled in a subsequent request. When the next external user event + * is signaled flash scope is cleared. + *

    + * Flash scope is typically used to store messages that should be preserved + * across refreshes of the next view state (for example, on a redirect and + * any browser refreshes). + */ + public static final ScopeType FLASH = new ScopeType(1, "Flash") { + public MutableAttributeMap getScope(RequestContext context) { + return context.getFlashScope(); + } + }; + + /** + * The "flow" scope type. Attributes placed in flow scope exist through the + * life of an executing flow session, representing an instance a single + * {@link FlowDefinition flow definition}. When the flow session ends any + * data in flow scope goes out of scope. + */ + public static final ScopeType FLOW = new ScopeType(2, "Flow") { + public MutableAttributeMap getScope(RequestContext context) { + return context.getFlowScope(); + } + }; + + /** + * The "conversation" scope type. Attributes placed in conversation scope + * are shared by all flow sessions started within a flow execution and live + * for the life of the entire flow execution (representing a single logical + * user conversation). When the governing execution ends, any data in + * conversation scope goes out of scope. + */ + public static final ScopeType CONVERSATION = new ScopeType(3, "Conversation") { + public MutableAttributeMap getScope(RequestContext context) { + return context.getConversationScope(); + } + }; + + /** + * Private constructor because this is a typesafe enum! + */ + private ScopeType(int code, String label) { + super(code, label); + } + + public Class getType() { + // force ScopeType as type + return ScopeType.class; + } + + /** + * Accessor that returns the mutable attribute map for this scope type for a + * given flow execution request context. + * @param context the context representing an executing request + * @return the scope map of this type for that request, allowing attributes + * to be accessed and set + */ + public abstract MutableAttributeMap getScope(RequestContext context); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/ViewSelection.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/ViewSelection.java new file mode 100644 index 00000000..7af9c49c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/ViewSelection.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +import java.io.ObjectStreamException; +import java.io.Serializable; + +/** + * Abstract base class for value objects that provide callers into a flow + * execution information about a logical response to issue and the data + * necessary to issue it. + *

    + * This class is a generic marker returned when a request into an executing flow + * has completed processing, indicating a client response needs to be issued. An + * instance of a ViewSelection subclass represents the selection of a concrete + * response type. It is expected that callers introspect the returned view + * selection instance to handle the response types they support. + * + * @see FlowExecution + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public abstract class ViewSelection implements Serializable { + + /** + * Constant for a null or empty view selection, indicating no + * response should be issued. + */ + public static final ViewSelection NULL_VIEW = new NullView(); + + /** + * The definition of the 'null' view selection type, indicating that no + * response should be issued. + */ + private static final class NullView extends ViewSelection { + + // resolve the singleton instance + private Object readResolve() throws ObjectStreamException { + return NULL_VIEW; + } + + public String toString() { + return "null"; + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/ConditionalFlowExecutionListenerHolder.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/ConditionalFlowExecutionListenerHolder.java new file mode 100644 index 00000000..4ab5047f --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/ConditionalFlowExecutionListenerHolder.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.factory; + +import java.util.Iterator; +import java.util.Set; + +import org.springframework.core.CollectionFactory; +import org.springframework.util.Assert; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.execution.FlowExecutionListener; + +/** + * A holder that holds a listener plus a set of criteria defining the flows in + * which that listener applies. + *

    + * This is an internal helper class used by the {@link ConditionalFlowExecutionListenerLoader}. + * + * @see ConditionalFlowExecutionListenerLoader + * + * @author Keith Donald + */ +class ConditionalFlowExecutionListenerHolder { + + /** + * The held listener. + */ + private FlowExecutionListener listener; + + /** + * The listener criteria set. + */ + private Set criteriaSet = CollectionFactory.createLinkedSetIfPossible(3); + + /** + * Create a new conditional flow execution listener holder. + * @param listener the listener to hold + */ + public ConditionalFlowExecutionListenerHolder(FlowExecutionListener listener) { + Assert.notNull(listener, "The listener is required"); + this.listener = listener; + } + + /** + * Returns the held listener. + */ + public FlowExecutionListener getListener() { + return listener; + } + + /** + * Add given criteria. + */ + public void add(FlowExecutionListenerCriteria criteria) { + criteriaSet.add(criteria); + } + + /** + * Remove given criteria. If not present, does nothing. + */ + public void remove(FlowExecutionListenerCriteria criteria) { + criteriaSet.remove(criteria); + } + + /** + * Are there any criteria registered? + */ + public boolean isCriteriaSetEmpty() { + return criteriaSet.isEmpty(); + } + + /** + * Determines if the listener held by this holder applies to the specified + * flow definition. Will do a logical OR between the registered criteria. + * @param flowDefinition the flow + * @return true if yes, false otherwise + */ + public boolean listenerAppliesTo(FlowDefinition flowDefinition) { + Iterator it = criteriaSet.iterator(); + while (it.hasNext()) { + FlowExecutionListenerCriteria criteria = (FlowExecutionListenerCriteria)it.next(); + if (criteria.appliesTo(flowDefinition)) { + return true; + } + } + return false; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/ConditionalFlowExecutionListenerLoader.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/ConditionalFlowExecutionListenerLoader.java new file mode 100644 index 00000000..b913d133 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/ConditionalFlowExecutionListenerLoader.java @@ -0,0 +1,242 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.factory; + +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.style.StylerUtils; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.execution.FlowExecutionListener; + +/** + * A flow execution listener loader that stores listeners in a list-backed data + * structure and allows for configuration of which listeners should apply to + * which flow definitions. For trivial listener loading, see + * {@link StaticFlowExecutionListenerLoader}. + * + * @see StaticFlowExecutionListenerLoader + * + * @author Keith Donald + */ +public class ConditionalFlowExecutionListenerLoader implements FlowExecutionListenerLoader { + + /** + * Logger, usable by subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * The list of flow execution listeners containing + * {@link ConditionalFlowExecutionListenerHolder} objects. The list + * determines the conditions in which a single flow execution listener + * applies. + */ + private List listeners = new LinkedList(); + + /** + * Add a listener that will listen to executions for all flows. + * @param listener the listener to add + */ + public void addListener(FlowExecutionListener listener) { + addListener(listener, null); + } + + /** + * Adds a collection of listeners that share a matching criteria. + * @param listeners the listeners + * @param criteria the criteria where these listeners apply + */ + public void addListeners(FlowExecutionListener[] listeners, FlowExecutionListenerCriteria criteria) { + for (int i = 0; i < listeners.length; i++) { + addListener(listeners[i], criteria); + } + } + + /** + * Add a listener that will listen to executions to flows matching the + * specified criteria. + * @param listener the listener + * @param criteria the listener criteria + */ + public void addListener(FlowExecutionListener listener, FlowExecutionListenerCriteria criteria) { + if (listener == null) { + return; + } + if (logger.isDebugEnabled()) { + logger.debug("Adding flow execution listener " + listener + " with criteria " + criteria); + } + ConditionalFlowExecutionListenerHolder conditional = getHolder(listener); + if (conditional == null) { + conditional = new ConditionalFlowExecutionListenerHolder(listener); + listeners.add(conditional); + } + if (criteria == null) { + criteria = new FlowExecutionListenerCriteriaFactory().allFlows(); + } + conditional.add(criteria); + } + + /** + * Set the list of flow execution listeners with corresponding criteria. + * Allows for bean style configuration. The given map should have + * {@link FlowExecutionListener} objects as keys and Strings ("*", "flowId", + * "flowId1,flowId2") or {@link FlowExecutionListenerCriteria} + * objects as values. This will clear any listeners registered with + * this object using the addListener methods. + * @param listenersWithCriteria the map of listeners and their corresponding criteria + */ + public void setListeners(Map listenersWithCriteria) { + removeAllListeners(); + for (Iterator it = listenersWithCriteria.entrySet().iterator(); it.hasNext(); ) { + Entry entry = (Entry)it.next(); + Assert.isInstanceOf(FlowExecutionListener.class, entry.getKey(), + "The key in the listenersWithCriteria map needs to be a FlowExecutionListener object"); + FlowExecutionListener listener = (FlowExecutionListener)entry.getKey(); + FlowExecutionListenerCriteria criteria = null; + if (entry.getValue() instanceof String) { + criteria = getCriteria((String)entry.getValue()); + } + else if (entry.getValue() instanceof FlowExecutionListenerCriteria) { + criteria = (FlowExecutionListenerCriteria)entry.getValue(); + } + else if (entry.getValue() != null) { + throw new IllegalArgumentException( + "The value in the listenersWithCriteria map needs to be a " + + "String or a FlowExecutionListenerCriteria object"); + } + addListener(listener, criteria); + } + } + + /** + * Is the given listener contained by this Flow execution manager? + * @param listener the listener + * @return true if yes, false otherwise + */ + public boolean containsListener(FlowExecutionListener listener) { + Iterator it = listeners.iterator(); + while (it.hasNext()) { + ConditionalFlowExecutionListenerHolder h = (ConditionalFlowExecutionListenerHolder)it.next(); + if (h.getListener().equals(listener)) { + return true; + } + } + return false; + } + + /** + * Remove the flow execution listener from the listener list. + * @param listener the listener + */ + public void removeListener(FlowExecutionListener listener) { + Iterator it = listeners.iterator(); + while (it.hasNext()) { + ConditionalFlowExecutionListenerHolder h = (ConditionalFlowExecutionListenerHolder)it.next(); + if (h.getListener().equals(listener)) { + it.remove(); + } + } + } + + /** + * Remove all listeners loadable by this loader. + */ + public void removeAllListeners() { + listeners.clear(); + } + + /** + * Remove the criteria for the specified listener. + * @param listener the listener + * @param criteria the criteria + */ + public void removeListenerCriteria(FlowExecutionListener listener, FlowExecutionListenerCriteria criteria) { + if (containsListener(listener)) { + ConditionalFlowExecutionListenerHolder listenerHolder = getHolder(listener); + listenerHolder.remove(criteria); + if (listenerHolder.isCriteriaSetEmpty()) { + removeListener(listener); + } + } + } + + /** + * Returns the array of flow execution listeners for specified flow. + * @param flowDefinition the flow definition associated with the execution + * to be listened to + * @return the flow execution listeners that apply + */ + public FlowExecutionListener[] getListeners(FlowDefinition flowDefinition) { + Assert.notNull(flowDefinition, "The Flow to load listeners for cannot be null"); + List listenersToAttach = new LinkedList(); + for (Iterator it = listeners.iterator(); it.hasNext();) { + ConditionalFlowExecutionListenerHolder listenerHolder = (ConditionalFlowExecutionListenerHolder)it.next(); + if (listenerHolder.listenerAppliesTo(flowDefinition)) { + listenersToAttach.add(listenerHolder.getListener()); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Loaded [" + listenersToAttach.size() + "] of possible " + listeners.size() + + " listeners for this execution request for flow '" + flowDefinition.getId() + + "', the listeners to attach are " + StylerUtils.style(listenersToAttach)); + } + return (FlowExecutionListener[])listenersToAttach.toArray(new FlowExecutionListener[listenersToAttach.size()]); + } + + // internal helpers + + /** + * Lookup the listener criteria holder for the listener provided. + * @param listener the listener + * @return the holder, or null if not found + */ + private ConditionalFlowExecutionListenerHolder getHolder(FlowExecutionListener listener) { + Iterator it = listeners.iterator(); + while (it.hasNext()) { + ConditionalFlowExecutionListenerHolder next = (ConditionalFlowExecutionListenerHolder)it.next(); + if (next.getListener().equals(listener)) { + return next; + } + } + return null; + } + + /** + * Decode given string value into one of the well known criteria types. + * @see FlowExecutionListenerCriteriaFactory + */ + protected FlowExecutionListenerCriteria getCriteria(String value) { + if ("*".equals(value)) { + return new FlowExecutionListenerCriteriaFactory().allFlows(); + } + else { + String[] flowIds = StringUtils.commaDelimitedListToStringArray(value); + for (int i = 0; i < flowIds.length; i++) { + flowIds[i] = flowIds[i].trim(); + } + return new FlowExecutionListenerCriteriaFactory().flows(flowIds); + } + } + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/FlowExecutionListenerCriteria.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/FlowExecutionListenerCriteria.java new file mode 100644 index 00000000..bc803a57 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/FlowExecutionListenerCriteria.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.factory; + +import org.springframework.webflow.definition.FlowDefinition; + +/** + * Strategy interface that determines if a flow execution listener should attach + * to executions of a specific flow definition. + *

    + * This selection strategy is typically used by the + * {@link FlowExecutionListenerLoader} to determine which listeners + * should apply to which flow definitions. + * + * @see org.springframework.webflow.execution.FlowExecution + * @see org.springframework.webflow.execution.FlowExecutionListener + * @see org.springframework.webflow.execution.factory.FlowExecutionListenerLoader + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface FlowExecutionListenerCriteria { + + /** + * Do the listeners guarded by this criteria object apply to the provided + * flow definition? + * @param definition the flow definition + * @return true if yes, false if no + */ + public boolean appliesTo(FlowDefinition definition); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/FlowExecutionListenerCriteriaFactory.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/FlowExecutionListenerCriteriaFactory.java new file mode 100644 index 00000000..f55753d2 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/FlowExecutionListenerCriteriaFactory.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.factory; + +import org.springframework.core.style.StylerUtils; +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.definition.FlowDefinition; + +/** + * Static factory for creating commonly used flow execution listener criteria. + * + * @see FlowExecutionListenerCriteria + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class FlowExecutionListenerCriteriaFactory { + + /** + * Returns a wild card criteria that matches all flows. + */ + public FlowExecutionListenerCriteria allFlows() { + return new WildcardFlowExecutionListenerCriteria(); + } + + /** + * Returns a criteria that just matches a flow with the specified id. + * @param flowId the flow id to match + */ + public FlowExecutionListenerCriteria flow(String flowId) { + return new FlowIdFlowExecutionListenerCriteria(flowId); + } + + /** + * Returns a criteria that just matches a flow if it is identified by one of + * the specified ids. + * @param flowIds the flow ids to match + */ + public FlowExecutionListenerCriteria flows(String[] flowIds) { + return new FlowIdFlowExecutionListenerCriteria(flowIds); + } + + /** + * A flow execution listener criteria implementation that matches for all + * flows. + */ + private static class WildcardFlowExecutionListenerCriteria implements FlowExecutionListenerCriteria { + + public boolean appliesTo(FlowDefinition definition) { + return true; + } + + public String toString() { + return "*"; + } + } + + /** + * A flow execution listener criteria implementation that matches flows with + * a specified id. + */ + private static class FlowIdFlowExecutionListenerCriteria implements FlowExecutionListenerCriteria { + + /** + * The flow ids that apply for this criteria. + */ + private String[] flowIds; + + /** + * Create a new flow id matching flow execution listener criteria + * implemenation. + * @param flowId the flow id to match + */ + public FlowIdFlowExecutionListenerCriteria(String flowId) { + Assert.notNull(flowId, "The flow id is required"); + this.flowIds = new String[] { flowId }; + } + + /** + * Create a new flow id matching flow execution listener criteria + * implemenation. + * @param flowIds the flow ids to match + */ + public FlowIdFlowExecutionListenerCriteria(String[] flowIds) { + Assert.notEmpty(flowIds, "The flow id array is required"); + this.flowIds = flowIds; + } + + public boolean appliesTo(FlowDefinition definition) { + for (int i = 0; i < flowIds.length; i++) { + if (flowIds[i].equals(definition.getId())) { + return true; + } + } + return false; + } + + public String toString() { + return new ToStringCreator(this).append("flowIds", StylerUtils.style(flowIds)).toString(); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/FlowExecutionListenerLoader.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/FlowExecutionListenerLoader.java new file mode 100644 index 00000000..2ab46eb4 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/FlowExecutionListenerLoader.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.factory; + +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.execution.FlowExecutionFactory; +import org.springframework.webflow.execution.FlowExecutionListener; + +/** + * A strategy interface for loading the set of FlowExecutionListener's that + * should apply to executions of a given flow definition. Typically used by a + * {@link FlowExecutionFactory} as part of execution creation. + * + * @author Keith Donald + */ +public interface FlowExecutionListenerLoader { + + /** + * Get the flow execution listeners that apply to the given flow definition. + * @param flowDefinition the flow definition + * @return the listeners that apply + */ + public FlowExecutionListener[] getListeners(FlowDefinition flowDefinition); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/StaticFlowExecutionListenerLoader.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/StaticFlowExecutionListenerLoader.java new file mode 100644 index 00000000..297cdc30 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/StaticFlowExecutionListenerLoader.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.factory; + +import org.springframework.util.Assert; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.execution.FlowExecutionListener; + +/** + * A simple flow execution listener loader that simply returns a static listener + * array on each invocation. For more elaborate needs see the + * {@link ConditionalFlowExecutionListenerLoader}. + * + * @see ConditionalFlowExecutionListenerLoader + * + * @author Keith Donald + */ +public final class StaticFlowExecutionListenerLoader implements FlowExecutionListenerLoader { + + /** + * A shared listener loader instance that returns am empty listener array on each invocation. + */ + public static final FlowExecutionListenerLoader EMPTY_INSTANCE = new StaticFlowExecutionListenerLoader(); + + /** + * The listener array to return when {@link #getListeners(FlowDefinition)} + * is invoked. + */ + private final FlowExecutionListener[] listeners; + + /** + * Creates a new flow execution listener loader that returns an empty + * listener array on each invocation. + */ + private StaticFlowExecutionListenerLoader() { + this(new FlowExecutionListener[0]); + } + + /** + * Creates a new flow execution listener loader that returns the provided + * listener on each invocation. + * @param listener the listener + */ + public StaticFlowExecutionListenerLoader(FlowExecutionListener listener) { + this(new FlowExecutionListener[] { listener }); + } + + /** + * Creates a new flow execution listener loader that returns the provided + * listener array on each invocation. Clients should not attempt to modify + * the passed in array as no deep copy is made. + * @param listeners the listener array. + */ + public StaticFlowExecutionListenerLoader(FlowExecutionListener[] listeners) { + Assert.notNull(listeners, "The flow execution listener array is required"); + this.listeners = listeners; + } + + public FlowExecutionListener[] getListeners(FlowDefinition flowDefinition) { + return listeners; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/package.html b/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/package.html new file mode 100644 index 00000000..b54d5624 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/factory/package.html @@ -0,0 +1,9 @@ + + +

    +Supporting types often used by flow execution factory implementations. In particular, this +package provides a number of classes facillitating the registration of flow execution +listeners with a flow execution during its construction by a flow execution factory. +

    + + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/package.html b/spring-webflow/src/main/java/org/springframework/webflow/execution/package.html new file mode 100644 index 00000000..9bca514c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/package.html @@ -0,0 +1,45 @@ + + +

    +Core, stable abstractions for representing runtime executions of flow definitions. +

    +

    +The central concept defined by this package is the +{@link org.springframework.webflow.execution.FlowExecution} interface, which represents +a single instance of a top-level flow definition. +

    +

    +The following classes and interfaces are of particular interest: +

      +
    • +{@link org.springframework.webflow.execution.FlowExecutionFactory} - An abstract +factory for creating new flow executions. +
    • +
    • +{@link org.springframework.webflow.execution.repository.FlowExecutionRepository} - A DAO +for persisting and restoring existing flow executions. +
    • +
    • +{@link org.springframework.webflow.execution.FlowExecutionListener} - An observer +interface to be implemented by objects that are interested in flow execution +lifecycle events. +
    • +
    +

    +

    +Package Usage example: +

    +    // create flow execution
    +    FlowDefinition definition = ...
    +    FlowExecutionFactory factory = ...
    +    FlowExecution execution = factory.createFlowExecution(definition);
    +    
    +    // start execution
    +    ExternalContext context = ...
    +    ViewSelection response = execution.start(null, context);
    +
    +

    +This package depends on the definition package. +

    + + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/BadlyFormattedFlowExecutionKeyException.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/BadlyFormattedFlowExecutionKeyException.java new file mode 100644 index 00000000..22162e91 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/BadlyFormattedFlowExecutionKeyException.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository; + +/** + * Thrown when an encoded flow execution key is badly formatted and could not be + * parsed. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class BadlyFormattedFlowExecutionKeyException extends FlowExecutionRepositoryException { + + /** + * The string encoded flow execution key that was invalid. + */ + private String invalidKey; + + /** + * The format the string key should have been in. Could just be a + * description of that format. + */ + private String format; + + /** + * Creates a bad execution key format exception. + * @param invalidKey the invalid key + * @param format the format the key should have been in + */ + public BadlyFormattedFlowExecutionKeyException(String invalidKey, String format) { + super("Badly formatted flow execution key '" + invalidKey + "', the expected format is '" + format + "'"); + this.invalidKey = invalidKey; + this.format = format; + } + + /** + * Creates a bad execution key format exception. + * @param invalidKey the invalid key + * @param format the format the key should have been in + * @param cause the cause + */ + public BadlyFormattedFlowExecutionKeyException(String invalidKey, String format, Throwable cause) { + super("Badly formatted flow execution key '" + invalidKey + "', the expected format is '" + format + "'", cause); + this.invalidKey = invalidKey; + this.format = format; + } + + /** + * Returns the string key of the flow execution that could not be parsed. + */ + public String getInvalidKey() { + return invalidKey; + } + + /** + * Returns the format the key should have been in. + */ + public String getFormat() { + return format; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionAccessException.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionAccessException.java new file mode 100644 index 00000000..26656b59 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionAccessException.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository; + +/** + * Base class for exceptions that indicate a flow execution could not be + * accessed within a repository. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public abstract class FlowExecutionAccessException extends FlowExecutionRepositoryException { + + /** + * The key of the execution that could not be accessed. + */ + private FlowExecutionKey flowExecutionKey; + + /** + * Creates a new flow execution access exception. + * @param flowExecutionKey the key of the execution that could not be + * accessed + * @param message a descriptive message + */ + public FlowExecutionAccessException(FlowExecutionKey flowExecutionKey, String message) { + this(flowExecutionKey, message, null); + } + + /** + * Creates a new flow execution access exception. + * @param flowExecutionKey the key of the execution that could not be + * accessed + * @param message a descriptive message + * @param cause the root cause of the access failure + */ + public FlowExecutionAccessException(FlowExecutionKey flowExecutionKey, String message, Exception cause) { + super(message, cause); + this.flowExecutionKey = flowExecutionKey; + } + + /** + * Returns key of the flow execution that could not be accessed. + */ + public FlowExecutionKey getFlowExecutionKey() { + return flowExecutionKey; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionKey.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionKey.java new file mode 100644 index 00000000..c80f610d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionKey.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository; + +import java.io.Serializable; + +/** + * A key that uniquely identifies a flow execution in a managed + * {@link FlowExecutionRepository}. Serves as a flow execution's persistent + * identity. + *

    + * This class is abstract. The repository subsystem encapsulates the structure + * of concrete key implementations. + * + * @author Keith Donald + */ +public abstract class FlowExecutionKey implements Serializable { + + /** + * Subclasses should override toString to return a parseable string form of + * the key. + * @see java.lang.Object#toString() + * @see FlowExecutionRepository#parseFlowExecutionKey(String) + */ + public abstract String toString(); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionLock.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionLock.java new file mode 100644 index 00000000..d78ee51f --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionLock.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository; + +/** + * A pessmistic lock to obtain exclusive rights to a flow execution. Used to + * prevent conflicts when multiple requests to manipulate a flow execution + * arrive from different threads concurrently. + * + * @author Keith Donald + */ +public interface FlowExecutionLock { + + /** + * Acquire the flow execution lock. This method will block until the lock + * becomes available for acquisition. + */ + public void lock(); + + /** + * Release the flow execution lock. + */ + public void unlock(); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionRepository.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionRepository.java new file mode 100644 index 00000000..01a1c079 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionRepository.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository; + +import org.springframework.webflow.execution.FlowExecution; + +/** + * Central subsystem interface responsible for the saving and restoring of flow + * executions, where each flow execution represents a state of an active flow + * definition. + *

    + * Flow execution repositories are responsible for managing the storage, restoration + * and removal of flow executions launched by clients of the Spring Web Flow system. + *

    + * When placed in a repository a {@link FlowExecution} object representing the + * state of a flow at a point in time is indexed under a unique + * {@link FlowExecutionKey}. + * + * @see FlowExecution + * @see FlowExecutionKey + * + * @author Erwin Vervaet + * @author Keith Donald + */ +public interface FlowExecutionRepository { + + /** + * Generate a unique flow execution key to be used as the persistent + * identifier of the flow execution. This method should be called after a + * new flow execution is started and remains active; thus needing to be + * saved. The FlowExecutionKey is the execution's persistent identity. + * @param flowExecution the flow execution + * @return the flow execution key + * @throws FlowExecutionRepositoryException a problem occured generating the + * key + */ + public FlowExecutionKey generateKey(FlowExecution flowExecution) throws FlowExecutionRepositoryException; + + /** + * Obtain the "next" flow execution key to be used as the flow + * execution's persistent identity. This method should be called after a + * existing flow execution has resumed and remains active; thus needing to + * be updated. This repository may choose to return the previous key or + * generate a new key. + * @param flowExecution the flow execution + * @param previousKey the current key associated with the flow exection + * @throws FlowExecutionRepositoryException a problem occured generating the + * key + */ + public FlowExecutionKey getNextKey(FlowExecution flowExecution, FlowExecutionKey previousKey) + throws FlowExecutionRepositoryException; + + /** + * Return the lock for the flow execution, allowing for the lock to be + * acquired or released. + *

    + * Caution: care should be made not to allow for a deadlock situation. If + * you acquire a lock make sure you release it when you are done. + *

    + * The general pattern for safely doing work against a locked conversation + * follows: + *

    +	 * FlowExecutionLock lock = repository.getLock(key);
    +	 * lock.lock();
    +	 * try {
    +	 * 	FlowExecution execution = repository.getFlowExecution(key);
    +	 * 	// do work
    +	 * }
    +	 * finally {
    +	 * 	lock.unlock();
    +	 * }
    +	 * 
    + * @param key the identifier of the flow execution to lock + * @return the lock + * @throws FlowExecutionRepositoryException a problem occured accessing the + * lock object + */ + public FlowExecutionLock getLock(FlowExecutionKey key) throws FlowExecutionRepositoryException; + + /** + * Return the FlowExecution indexed by the provided key. The + * returned flow execution represents the restored state of an executing + * flow from a point in time. This should be called to resume a persistent + * flow execution. + * @param key the flow execution key + * @return the flow execution, fully hydrated and ready to signal an event + * against + * @throws FlowExecutionRepositoryException if no flow execution was indexed + * with the key provided + */ + public FlowExecution getFlowExecution(FlowExecutionKey key) throws FlowExecutionRepositoryException; + + /** + * Place the FlowExecution in this repository under the + * provided key. This should be called to save or update the persistent + * state of an active (but paused) flow execution. + * @param key the flow execution key + * @param flowExecution the flow execution + * @throws FlowExecutionRepositoryException the flow execution could not be + * stored + */ + public void putFlowExecution(FlowExecutionKey key, FlowExecution flowExecution) + throws FlowExecutionRepositoryException; + + /** + * Remove the flow execution from the repository. This should be called when + * the flow execution ends (is no longer active). + * @param key the flow execution key + * @throws FlowExecutionRepositoryException the flow execution could not be + * removed. + */ + public void removeFlowExecution(FlowExecutionKey key) throws FlowExecutionRepositoryException; + + /** + * Parse the string-encoded flow execution key into its object form. + * Essentially, the reverse of {@link FlowExecutionKey#toString()}. + * @param encodedKey the string encoded key + * @return the parsed flow execution key, the persistent identifier for + * exactly one flow execution + */ + public FlowExecutionKey parseFlowExecutionKey(String encodedKey) throws FlowExecutionRepositoryException; + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionRepositoryException.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionRepositoryException.java new file mode 100644 index 00000000..038f0f25 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionRepositoryException.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository; + +import org.springframework.webflow.core.FlowException; + +/** + * The root of the {@link FlowExecutionRepository} exception hierarchy. + * Indicates a problem occured either saving, restoring, or invalidating + * a managed flow execution. + * + * @author Erwin Vervaet + * @author Keith Donald + */ +public abstract class FlowExecutionRepositoryException extends FlowException { + + /** + * Creates a new flow execution repository exception. + * @param message a descriptive message + */ + public FlowExecutionRepositoryException(String message) { + super(message); + } + + /** + * Creates a new flow execution repository exception. + * @param message a descriptive message + * @param cause the root cause of the problem + */ + public FlowExecutionRepositoryException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionRestorationFailureException.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionRestorationFailureException.java new file mode 100644 index 00000000..6b264083 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/FlowExecutionRestorationFailureException.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository; + +/** + * Thrown when the flow execution with the persistent identifier provided could + * not be restored. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class FlowExecutionRestorationFailureException extends FlowExecutionAccessException { + + /** + * Creates a new flow execution restoration failure exception. + * @param flowExecutionKey the key of the execution that could not be + * restored + * @param cause the root cause of the restoration failure + */ + public FlowExecutionRestorationFailureException(FlowExecutionKey flowExecutionKey, Exception cause) { + super(flowExecutionKey, "A problem occurred restoring the flow execution with key '" + flowExecutionKey + "'", + cause); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/NoSuchFlowExecutionException.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/NoSuchFlowExecutionException.java new file mode 100644 index 00000000..b8e42da5 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/NoSuchFlowExecutionException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository; + +/** + * Thrown when the flow execution with the persistent identifier provided could + * not be found. This could occur if the execution has been removed from the + * repository and a client still has a handle to the key. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class NoSuchFlowExecutionException extends FlowExecutionAccessException { + + /** + * Creates a new no such flow execution exception. + * @param flowExecutionKey the key of the execution that could not be + * found + * @param cause the root cause of the failure + */ + public NoSuchFlowExecutionException(FlowExecutionKey flowExecutionKey, Exception cause) { + super(flowExecutionKey, "No flow execution could be found with key '" + flowExecutionKey + + "' -- perhaps this executing flow has ended or expired? " + + "This could happen if your users are relying on browser history " + + "(typically via the back button) that references ended flows.", cause); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/PermissionDeniedFlowExecutionAccessException.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/PermissionDeniedFlowExecutionAccessException.java new file mode 100644 index 00000000..3bebbb2d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/PermissionDeniedFlowExecutionAccessException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository; + +/** + * Thrown when access to a flow execution was denied by a repository. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class PermissionDeniedFlowExecutionAccessException extends FlowExecutionAccessException { + + /** + * Creates a new flow execution restoration exception. + * @param flowExecutionKey the key of the execution that could not be + * accessed + * @param cause the root cause of the access failure + */ + public PermissionDeniedFlowExecutionAccessException(FlowExecutionKey flowExecutionKey, Exception cause) { + super(flowExecutionKey, "Unable to restore flow execution with key '" + flowExecutionKey + + "' -- permission denied.", cause); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ClientContinuationFlowExecutionRepository.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ClientContinuationFlowExecutionRepository.java new file mode 100644 index 00000000..48fb4e2f --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ClientContinuationFlowExecutionRepository.java @@ -0,0 +1,250 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.continuation; + +import java.io.Serializable; + +import org.apache.commons.codec.binary.Base64; +import org.springframework.util.Assert; +import org.springframework.webflow.conversation.Conversation; +import org.springframework.webflow.conversation.ConversationException; +import org.springframework.webflow.conversation.ConversationId; +import org.springframework.webflow.conversation.ConversationManager; +import org.springframework.webflow.conversation.ConversationParameters; +import org.springframework.webflow.conversation.NoSuchConversationException; +import org.springframework.webflow.core.collection.CollectionUtils; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.repository.FlowExecutionKey; +import org.springframework.webflow.execution.repository.FlowExecutionRestorationFailureException; +import org.springframework.webflow.execution.repository.support.AbstractConversationFlowExecutionRepository; +import org.springframework.webflow.execution.repository.support.FlowExecutionStateRestorer; + +/** + * Stores flow execution state client side, requiring no use of server-side + * state. + *

    + * More specifically, instead of putting {@link FlowExecution} objects in a + * server-side store this repository encodes them directly into the + * continuationId of the generated {@link FlowExecutionKey}. + * When asked to load a flow execution by its key this repository decodes the + * serialized continuationId, restoring the + * {@link FlowExecution} object at the state it was in when encoded. + *

    + * Note: currently this repository implementation does not by default support + * conversation management. This has two consequences. First, there is no + * conversation invalidation after completion, which enables automatic + * prevention of duplicate submission after a conversation has completed. + * Secondly, The contents of conversation scope will not be maintained + * across requests. Support for these features requires tracking active + * conversations using a conversation service backed by some centralized storage + * medium like a database table. If you want to have proper conversation management, + * configure this class with an appropriate conversation manager (the default + * conversation manager used does nothing). + *

    + * Warning: storing state (a flow execution continuation) on the client entails + * a certain security risk. This implementation does not provide a secure way of + * storing state on the client, so a malicious client could reverse engineer a + * continuation and get access to possible sensitive data stored in the flow + * execution. If you need more security and still want to store continuations on + * the client, subclass this class and override the methods + * {@link #encode(FlowExecution)} and {@link #decode(String)}, implementing + * them with a secure encoding/decoding algorithm, e.g. based on public/private + * key encryption. + *

    + * This class depends on the Jakarta Commons Codec library to do + * BASE64 encoding. Codec code must be available in the classpath + * when using this implementation. + * + * @see Base64 + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class ClientContinuationFlowExecutionRepository extends AbstractConversationFlowExecutionRepository { + + /** + * The continuation factory that will be used to create new continuations to + * be added to active conversations. + */ + private FlowExecutionContinuationFactory continuationFactory = new SerializedFlowExecutionContinuationFactory(); + + /** + * Creates a new client continuation repository. Uses a 'no op' conversation manager by default. + * @param executionStateRestorer the transient flow execution state restorer + */ + public ClientContinuationFlowExecutionRepository(FlowExecutionStateRestorer executionStateRestorer) { + super(executionStateRestorer, new NoOpConversationManager()); + } + + /** + * Creates a new client continuation repository. Use this contructor when you want + * to use a particular conversation manager, e.g. one that does proper conversation + * management. + * @param executionStateRestorer the transient flow execution state restorer + * @param conversationManager the conversation manager for managing centralized conversational state + */ + public ClientContinuationFlowExecutionRepository(FlowExecutionStateRestorer executionStateRestorer, + ConversationManager conversationManager) { + super(executionStateRestorer, conversationManager); + } + + /** + * Returns the continuation factory in use by this repository. + */ + protected FlowExecutionContinuationFactory getContinuationFactory() { + return continuationFactory; + } + + /** + * Sets the continuation factory used by this repository. + */ + public void setContinuationFactory(FlowExecutionContinuationFactory continuationFactory) { + Assert.notNull(continuationFactory, "The continuation factory is required"); + this.continuationFactory = continuationFactory; + } + + public FlowExecution getFlowExecution(FlowExecutionKey key) { + if (logger.isDebugEnabled()) { + logger.debug("Getting flow execution with key '" + key + "'"); + } + + // note that the call to getConversationScope() below will try to obtain + // the conversation identified by the key, which will fail if that conversation + // is no longer managed by the conversation manager (i.e. it has expired) + + FlowExecutionContinuation continuation = decode((String)getContinuationId(key)); + try { + FlowExecution execution = continuation.unmarshal(); + // the flow execution was deserialized so we need to restore transient + // state + return getExecutionStateRestorer().restoreState(execution, getConversationScope(key)); + } + catch (ContinuationUnmarshalException e) { + throw new FlowExecutionRestorationFailureException(key, e); + } + } + + public void putFlowExecution(FlowExecutionKey key, FlowExecution flowExecution) { + if (logger.isDebugEnabled()) { + logger.debug("Putting flow execution '" + flowExecution + "' into repository with key '" + key + "'"); + } + + // note that the call to putConversationScope() below will try to obtain + // the conversation identified by the key, which will fail if that conversation + // is no longer managed by the conversation manager (i.e. it has expired) + + // the flow execution state is already stored in the key, so + // there's nothing we need to do to store it + putConversationScope(key, flowExecution.getConversationScope()); + } + + protected final Serializable generateContinuationId(FlowExecution flowExecution) { + return encode(flowExecution); + } + + protected final Serializable parseContinuationId(String encodedId) { + // just return here, continuation decoding happens in getFlowExecution + return encodedId; + } + + /** + * Encode given flow execution object into data that can be stored on the + * client. + *

    + * Subclasses can override this to change the encoding algorithm. This class + * just does a BASE64 encoding of the serialized flow execution. + * @param flowExecution the flow execution instance + * @return the encoded representation + */ + protected Serializable encode(FlowExecution flowExecution) { + FlowExecutionContinuation continuation = continuationFactory.createContinuation(flowExecution); + return new String(Base64.encodeBase64(continuation.toByteArray())); + } + + /** + * Decode given data, received from the client, and return the corresponding + * flow execution object. + *

    + * Subclasses can override this to change the decoding algorithm. This class + * just does a BASE64 decoding and then deserializes the flow + * execution. + * @param encodedContinuation the encoded flow execution data + * @return the decoded flow execution instance + */ + protected FlowExecutionContinuation decode(String encodedContinuation) { + byte[] bytes = Base64.decodeBase64(encodedContinuation.getBytes()); + return continuationFactory.createContinuation(bytes); + } + + /** + * Conversation manager that doesn't do anything - the default. Does not support + * conversation scope or conversation invalidation. + * + * @author Keith Donald + */ + private static class NoOpConversationManager implements ConversationManager { + + /** + * The single conversation managed by the manager. + */ + private static final NoOpConversation INSTANCE = new NoOpConversation(); + + public Conversation beginConversation(ConversationParameters conversationParameters) + throws ConversationException { + return INSTANCE; + } + + public Conversation getConversation(ConversationId id) throws NoSuchConversationException { + return INSTANCE; + } + + public ConversationId parseConversationId(String encodedId) throws ConversationException { + return NoOpConversation.ID; + } + + private static class NoOpConversation implements Conversation { + + private static final ConversationId ID = new ConversationId() { + public String toString() { + return "NoOpConversation id"; + } + }; + + public ConversationId getId() { + return ID; + } + + public void lock() { + } + + public Object getAttribute(Object name) { + return CollectionUtils.EMPTY_ATTRIBUTE_MAP; + } + + public void putAttribute(Object name, Object value) { + } + + public void removeAttribute(Object name) { + } + + public void end() { + } + + public void unlock() { + } + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ContinuationCreationException.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ContinuationCreationException.java new file mode 100644 index 00000000..6cdb8f8a --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ContinuationCreationException.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.continuation; + +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.repository.FlowExecutionRepositoryException; + +/** + * Thrown when a continuation snapshot could not be taken of flow execution + * state. + * + * @author Keith Donald + */ +public class ContinuationCreationException extends FlowExecutionRepositoryException { + + /** + * The flow execution that could not be snapshotted. + */ + private FlowExecution flowExecution; + + /** + * Creates a new continuation creation exception. + * @param flowExecution the flow execution + * @param message a descriptive message + * @param cause the cause + */ + public ContinuationCreationException(FlowExecution flowExecution, String message, Throwable cause) { + super(message, cause); + } + + /** + * Returns the flow execution that could not be snapshotted. + */ + public FlowExecution getFlowExecution() { + return flowExecution; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ContinuationFlowExecutionRepository.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ContinuationFlowExecutionRepository.java new file mode 100644 index 00000000..b0b133b6 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ContinuationFlowExecutionRepository.java @@ -0,0 +1,226 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.continuation; + +import java.io.Serializable; + +import org.springframework.util.Assert; +import org.springframework.webflow.conversation.Conversation; +import org.springframework.webflow.conversation.ConversationManager; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.repository.FlowExecutionKey; +import org.springframework.webflow.execution.repository.FlowExecutionRestorationFailureException; +import org.springframework.webflow.execution.repository.support.AbstractConversationFlowExecutionRepository; +import org.springframework.webflow.execution.repository.support.FlowExecutionStateRestorer; +import org.springframework.webflow.util.RandomGuidUidGenerator; +import org.springframework.webflow.util.UidGenerator; + +/** + * Stores one to many flow execution continuations (snapshots) per + * conversation, where each continuation represents a paused, restorable + * view-state of a flow execution snapshotted at a point in time. + *

    + * The set of active user conversations are managed by a + * {@link ConversationManager} implementation, which this repository delegates + * to. + *

    + * This repository is responsible for: + *

      + *
    • Beginning a new conversation when a new flow execution is made + * persistent. Each conversation is assigned a unique conversartion id which + * forms one part of the flow execution key. + *
    • Associating a flow execution with that conversation by adding a + * {@link FlowExecutionContinuation} to a continuation group.
      + * When a flow execution is placed in this repository a new continuation + * snapshot is created, assigned an id, and added to the group. Each + * continuation logically represents a state of the conversation at a point in + * time that can be restored and continued. These continuations can be + * restored to support users going back in their browser to continue a + * conversation from a previous point. + *
    • Ending existing conversations when persistent flow executions end, as + * part of a repository removal operation. + *
    + *

    + * This repository implementation also provides support for conversation + * invalidation after completion, where once a logical conversation + * completes (by one of its FlowExecution's reaching an end state), the entire + * conversation (including all continuations) is invalidated. This prevents the + * possibility of duplicate submission after completion. + *

    + * This repository implementation should be considered when you do have to + * support browser navigational button use, e.g. you cannot lock down the + * browser and require that all navigational events to be routed explicitly + * through Spring Web Flow. + * + * @author Keith Donald + */ +public class ContinuationFlowExecutionRepository extends AbstractConversationFlowExecutionRepository { + + /** + * The conversation attribute that stores the "continuation group". + */ + private static final String CONTINUATION_GROUP_ATTRIBUTE = "continuationGroup"; + + /** + * The continuation factory that will be used to create new continuations to + * be added to active conversations. + */ + private FlowExecutionContinuationFactory continuationFactory = new SerializedFlowExecutionContinuationFactory(); + + /** + * The uid generation strategy to use. + */ + private UidGenerator continuationIdGenerator = new RandomGuidUidGenerator(); + + /** + * The maximum number of continuations that can be active per conversation. + * The default is 30, which is high enough not to interfere with the user experience + * of normal users using the back button, but low enough to avoid excessive + * resource usage or easy denial of service attacks. + */ + private int maxContinuations = 30; + + /** + * Create a new continuation based flow execution repository using given state + * restorer and conversation manager. + * @param executionStateRestorer the state restoration strategy to use + * @param conversationManager the conversation manager to use + */ + public ContinuationFlowExecutionRepository(FlowExecutionStateRestorer executionStateRestorer, + ConversationManager conversationManager) { + super(executionStateRestorer, conversationManager); + } + + /** + * Returns the continuation factory that encapsulates the construction of + * continuations stored in this repository. Defaults to + * {@link SerializedFlowExecutionContinuationFactory}. + */ + public FlowExecutionContinuationFactory getContinuationFactory() { + return continuationFactory; + } + + /** + * Sets the continuation factory that encapsulates the construction of + * continuations stored in this repository. + */ + public void setContinuationFactory(FlowExecutionContinuationFactory continuationFactory) { + Assert.notNull(continuationFactory, "The continuation factory is required"); + this.continuationFactory = continuationFactory; + } + + /** + * Returns the uid generation strategy used to generate continuation + * identifiers. Defaults to {@link RandomGuidUidGenerator}. + */ + public UidGenerator getContinuationIdGenerator() { + return continuationIdGenerator; + } + + /** + * Sets the uid generation strategy used to generate unique continuation + * identifiers for {@link FlowExecutionKey flow execution keys}. + */ + public void setContinuationIdGenerator(UidGenerator continuationIdGenerator) { + Assert.notNull(continuationIdGenerator, "The continuation id generator is required"); + this.continuationIdGenerator = continuationIdGenerator; + } + + /** + * Returns the maximum number of continuations allowed per conversation in + * this repository. + */ + public int getMaxContinuations() { + return maxContinuations; + } + + /** + * Sets the maximum number of continuations allowed per conversation in this + * repository. Use -1 for unlimited. The default is 30. + */ + public void setMaxContinuations(int maxContinuations) { + this.maxContinuations = maxContinuations; + } + + public FlowExecution getFlowExecution(FlowExecutionKey key) { + if (logger.isDebugEnabled()) { + logger.debug("Getting flow execution with key '" + key + "'"); + } + FlowExecutionContinuation continuation = getContinuation(key); + try { + FlowExecution execution = continuation.unmarshal(); + // flow execution was deserialized, so restore transient state + return getExecutionStateRestorer().restoreState(execution, getConversationScope(key)); + } + catch (ContinuationUnmarshalException e) { + throw new FlowExecutionRestorationFailureException(key, e); + } + } + + public void putFlowExecution(FlowExecutionKey key, FlowExecution flowExecution) { + if (logger.isDebugEnabled()) { + logger.debug("Putting flow execution '" + flowExecution + "' into repository with key '" + key + "'"); + } + FlowExecutionContinuationGroup continuationGroup = getContinuationGroup(key); + FlowExecutionContinuation continuation = continuationFactory.createContinuation(flowExecution); + if (logger.isDebugEnabled()) { + logger.debug("Adding new continuation to group with id " + getContinuationId(key)); + } + continuationGroup.add(getContinuationId(key), continuation); + putConversationScope(key, flowExecution.getConversationScope()); + } + + protected void onBegin(Conversation conversation) { + // setup a new continuation group for the conversation + FlowExecutionContinuationGroup continuationGroup = new FlowExecutionContinuationGroup(maxContinuations); + conversation.putAttribute(CONTINUATION_GROUP_ATTRIBUTE, continuationGroup); + } + + protected Serializable generateContinuationId(FlowExecution flowExecution) { + return continuationIdGenerator.generateUid(); + } + + protected Serializable parseContinuationId(String encodedId) { + return continuationIdGenerator.parseUid(encodedId); + } + + /** + * Returns the continuation group associated with the governing + * conversation. + * @param key the flow execution key + * @return the continuation group + */ + FlowExecutionContinuationGroup getContinuationGroup(FlowExecutionKey key) { + FlowExecutionContinuationGroup group = + (FlowExecutionContinuationGroup)getConversation(key).getAttribute(CONTINUATION_GROUP_ATTRIBUTE); + return group; + } + + /** + * Returns the continuation in the group with the specified key. + * @param key the flow execution key + * @return the continuation. + */ + protected FlowExecutionContinuation getContinuation(FlowExecutionKey key) + throws FlowExecutionRestorationFailureException { + try { + return getContinuationGroup(key).get(getContinuationId(key)); + } + catch (ContinuationNotFoundException e) { + throw new FlowExecutionRestorationFailureException(key, e); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ContinuationNotFoundException.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ContinuationNotFoundException.java new file mode 100644 index 00000000..0ad05968 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ContinuationNotFoundException.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.continuation; + +import java.io.Serializable; + +import org.springframework.webflow.execution.repository.FlowExecutionRepositoryException; + +/** + * Thrown when no flow execution continuation exists within a continuation + * group with a particular id. This might occur if the continuation has expired + * or was explictly invalidated but a client's browser page cache still references it. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class ContinuationNotFoundException extends FlowExecutionRepositoryException { + + /** + * The unique continuation identifier that was not found. + */ + private Serializable continuationId; + + /** + * Creates a continuation not found exception. + * @param continuationId the continuation id that could not be found + */ + public ContinuationNotFoundException(Serializable continuationId) { + super("No flow execution continuation could be found in this group with id '" + continuationId + + "' -- perhaps the continuation has expired or has been invalidated? "); + } + + /** + * Returns the continuation id that could not be found. + */ + public Serializable getContinuationId() { + return continuationId; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ContinuationUnmarshalException.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ContinuationUnmarshalException.java new file mode 100644 index 00000000..6596f277 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/ContinuationUnmarshalException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.continuation; + +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.repository.FlowExecutionRepositoryException; + +/** + * Thrown when a FlowExecutionContinuation could not be deserialized into a + * FlowExecution. + * + * @see FlowExecutionContinuation + * @see FlowExecution + * + * @author Keith Donald + */ +public class ContinuationUnmarshalException extends FlowExecutionRepositoryException { + + /** + * Creates a new flow execution unmarshalling exception. + * @param message the exception message + * @param cause the cause + */ + public ContinuationUnmarshalException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/FlowExecutionContinuation.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/FlowExecutionContinuation.java new file mode 100644 index 00000000..581dd287 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/FlowExecutionContinuation.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.continuation; + +import java.io.Serializable; + +import org.springframework.webflow.execution.FlowExecution; + +/** + * A snapshot of a flow execution that can be restored from and serialized to a byte + * array. + * + * @see FlowExecutionContinuationFactory + * + * @author Erwin Vervaet + * @author Keith Donald + */ +public abstract class FlowExecutionContinuation implements Serializable { + + /** + * Restores the flow execution wrapped in this continuation. + * @return the unmarshalled flow execution + * @throws ContinuationUnmarshalException when there is a problem unmarshalling + * this continuation + */ + public abstract FlowExecution unmarshal() throws ContinuationUnmarshalException; + + /** + * Converts this continuation to a byte array for convenient serialization. + * @return this as a byte array + */ + public abstract byte[] toByteArray(); + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/FlowExecutionContinuationFactory.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/FlowExecutionContinuationFactory.java new file mode 100644 index 00000000..cae8f332 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/FlowExecutionContinuationFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.continuation; + +import org.springframework.webflow.execution.FlowExecution; + +/** + * A factory for creating different {@link FlowExecutionContinuation} + * implementations. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface FlowExecutionContinuationFactory { + + /** + * Creates a new flow execution continuation for given flow execution. + * @param flowExecution the flow execution + * @return the continuation + * @throws ContinuationCreationException when the continuation cannot be created + */ + public FlowExecutionContinuation createContinuation(FlowExecution flowExecution) + throws ContinuationCreationException; + + /** + * Creates a new flow execution continuation from the provided byte array. + * @param bytes the flow execution byte array + * @return the continuation + * @throws ContinuationCreationException when the continuation cannot be created + */ + public FlowExecutionContinuation createContinuation(byte[] bytes) throws ContinuationCreationException; +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/FlowExecutionContinuationGroup.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/FlowExecutionContinuationGroup.java new file mode 100644 index 00000000..2c754571 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/FlowExecutionContinuationGroup.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.continuation; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Map; + +/** + * A group of flow execution continuations. Simple typed data structure backed + * by a map and linked list. Supports expelling the oldest continuation once a + * maximum group size is met. + * + * @author Keith Donald + */ +class FlowExecutionContinuationGroup implements Serializable { + + /** + * A map of continuations; the key is the continuation id, and the value is + * the {@link FlowExecutionContinuation} object. + */ + private Map continuations = new HashMap(); + + /** + * An ordered list of continuation ids. Each continuation id represents an + * pointer to a continuation in the map. The first element is the oldest + * continuation and the last is the youngest. + */ + private LinkedList continuationIds = new LinkedList(); + + /** + * The maximum number of continuations allowed in this group. + */ + private int maxContinuations = -1; + + /** + * Creates a new flow execution continuation group. + * @param maxContinuations the maximum number of continuations that can be + * stored in this group, -1 for unlimited + */ + public FlowExecutionContinuationGroup(int maxContinuations) { + this.maxContinuations = maxContinuations; + } + + /** + * Returns the count of continuations in this group. + */ + public int getContinuationCount() { + return continuationIds.size(); + } + + /** + * Returns the continuation with the provided id, or + * null if no such continuation exists with that id. + * @param id the continuation id + * @return the continuation + * @throws ContinuationNotFoundException if the id does not match a + * continuation in this group + */ + public FlowExecutionContinuation get(Serializable id) throws ContinuationNotFoundException { + FlowExecutionContinuation continuation = (FlowExecutionContinuation)continuations.get(id); + if (continuation == null) { + throw new ContinuationNotFoundException(id); + } + return continuation; + } + + /** + * Add a flow execution continuation with given id to this group. + * @param continuationId the continuation id + * @param continuation the continuation + */ + public void add(Serializable continuationId, FlowExecutionContinuation continuation) { + continuations.put(continuationId, continuation); + if (continuationIds.contains(continuationId)) { + continuationIds.remove(continuationId); + } + continuationIds.add(continuationId); + // remove the oldest continuation if them maximium number of + // continuations has been exceeded + if (maxExceeded()) { + removeOldestContinuation(); + } + } + + /** + * Has the maximum number of allowed continuations in this group been exceeded? + */ + private boolean maxExceeded() { + return maxContinuations > 0 && continuationIds.size() > maxContinuations; + } + + /** + * Remove the olders continuation from this group. + */ + private void removeOldestContinuation() { + continuations.remove(continuationIds.removeFirst()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/SerializedFlowExecutionContinuation.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/SerializedFlowExecutionContinuation.java new file mode 100644 index 00000000..8ed24282 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/SerializedFlowExecutionContinuation.java @@ -0,0 +1,237 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.continuation; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.Externalizable; +import java.io.IOException; +import java.io.NotSerializableException; +import java.io.ObjectInput; +import java.io.ObjectInputStream; +import java.io.ObjectOutput; +import java.io.ObjectOutputStream; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import org.springframework.util.FileCopyUtils; +import org.springframework.webflow.execution.FlowExecution; + +/** + * A continuation implementation that is based on standard Java serialization, + * created by a {@link SerializedFlowExecutionContinuationFactory}. + * + * @see SerializedFlowExecutionContinuationFactory + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class SerializedFlowExecutionContinuation extends FlowExecutionContinuation implements Externalizable { + + /** + * The serialized flow execution. + */ + private byte[] flowExecutionData; + + /** + * Whether or not the flow execution byte array is compressed. + */ + private boolean compressed; + + /** + * Default constructor necessary for {@link Externalizable} custom + * serialization semantics. Should not be called by application code. + */ + public SerializedFlowExecutionContinuation() { + } + + /** + * Creates a new serialized flow execution continuation. This will marshall + * given flow execution into a serialized continuation form. + * @param flowExecution the flow execution + * @param compress whether or not the flow execution should be compressed + */ + public SerializedFlowExecutionContinuation(FlowExecution flowExecution, boolean compress) + throws ContinuationCreationException { + try { + flowExecutionData = serialize(flowExecution); + if (compress) { + flowExecutionData = compress(flowExecutionData); + } + } + catch (NotSerializableException e) { + throw new ContinuationCreationException(flowExecution, + "Could not serialize flow execution; " + + "make sure all objects stored in flow or flash scope are serializable", e); + } + catch (IOException e) { + throw new ContinuationCreationException(flowExecution, + "IOException thrown serializing flow execution -- this should not happen!", e); + } + this.compressed = compress; + } + + /** + * Returns whether or not the flow execution data in this continuation is + * compressed. + */ + public boolean isCompressed() { + return compressed; + } + + public FlowExecution unmarshal() throws ContinuationUnmarshalException { + try { + return deserialize(getFlowExecutionData()); + } + catch (IOException e) { + throw new ContinuationUnmarshalException( + "IOException thrown deserializing the flow execution stored in this continuation -- this should not happen!", + e); + } + catch (ClassNotFoundException e) { + throw new ContinuationUnmarshalException( + "ClassNotFoundException thrown deserializing the flow execution stored in this continuation -- " + + "This should not happen! Make sure there are no classloader issues." + + "For example, perhaps the Web Flow system is being loaded by a classloader " + + "that is a parent of the classloader loading application classes?", e); + } + } + + public byte[] toByteArray() { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(flowExecutionData.length + 40); + ObjectOutputStream oos = new ObjectOutputStream(baos); + try { + oos.writeObject(this); + oos.flush(); + } + finally { + oos.close(); + } + return baos.toByteArray(); + } + catch (IOException e) { + throw new IllegalStateException(); + } + } + + // implementing Externalizable for custom serialization + + public void writeExternal(ObjectOutput out) throws IOException { + // write out length first + out.writeInt(flowExecutionData.length); + // write out contents + out.write(flowExecutionData); + out.writeBoolean(compressed); + } + + public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { + // read length of data array + int length = in.readInt(); + flowExecutionData = new byte[length]; + // read in contents in full + in.readFully(flowExecutionData); + compressed = in.readBoolean(); + } + + // internal helpers + + /** + * Return the flow execution data in its raw byte[] form. Will decompress if + * necessary. + * @return the byte array + * @throws IOException a problem occured with decompression + */ + protected byte[] getFlowExecutionData() throws IOException { + if (isCompressed()) { + return decompress(flowExecutionData); + } + else { + return flowExecutionData; + } + } + + /** + * Internal helper method to serialize given flow execution. Override if a + * custom serialization method is used. + * @param flowExecution flow execution to serialize + * @return serialized flow flow execution data + * @throws IOException when something goes wrong during during serialization + */ + protected byte[] serialize(FlowExecution flowExecution) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(1024); + ObjectOutputStream oos = new ObjectOutputStream(baos); + try { + oos.writeObject(flowExecution); + oos.flush(); + return baos.toByteArray(); + } + finally { + oos.close(); + } + } + + /** + * Internal helper method to deserialize given flow execution data. Override + * if a custom serialization method is used. + * @param data serialized flow flow execution data + * @return deserialized flow execution + * @throws IOException when something goes wrong during deserialization + * @throws ClassNotFoundException when required classes cannot be loaded + */ + protected FlowExecution deserialize(byte[] data) throws IOException, ClassNotFoundException { + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data)); + try { + return (FlowExecution)ois.readObject(); + } + finally { + ois.close(); + } + } + + /** + * Internal helper method to compress given flow execution data using GZIP + * compression. Override if custom compression is desired. + */ + protected byte[] compress(byte[] dataToCompress) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + GZIPOutputStream gzipos = new GZIPOutputStream(baos); + try { + gzipos.write(dataToCompress); + gzipos.flush(); + } + finally { + gzipos.close(); + } + return baos.toByteArray(); + } + + /** + * Internal helper method to decompress given flow execution data using GZIP + * decompression. Override if custom decompression is desired. + */ + protected byte[] decompress(byte[] dataToDecompress) throws IOException { + GZIPInputStream gzipin = new GZIPInputStream(new ByteArrayInputStream(dataToDecompress)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + try { + FileCopyUtils.copy(gzipin, baos); + } + finally { + gzipin.close(); + } + return baos.toByteArray(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/SerializedFlowExecutionContinuationFactory.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/SerializedFlowExecutionContinuationFactory.java new file mode 100644 index 00000000..e8be9181 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/SerializedFlowExecutionContinuationFactory.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.continuation; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.ObjectInputStream; + +import org.springframework.webflow.execution.FlowExecution; + +/** + * A factory that creates new instances of flow execution continuations based on + * standard Java serialization. + * + * @see SerializedFlowExecutionContinuation + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class SerializedFlowExecutionContinuationFactory implements FlowExecutionContinuationFactory { + + /** + * Flag to toggle continuation compression; compression is on by default. + */ + private boolean compress = true; + + /** + * Returns whether or not the continuations should be compressed. + */ + public boolean getCompress() { + return compress; + } + + /** + * Set whether or not the continuations should be compressed. + */ + public void setCompress(boolean compress) { + this.compress = compress; + } + + public FlowExecutionContinuation createContinuation(FlowExecution flowExecution) throws ContinuationCreationException { + return new SerializedFlowExecutionContinuation(flowExecution, compress); + } + + public FlowExecutionContinuation createContinuation(byte[] bytes) throws ContinuationUnmarshalException { + try { + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes)); + try { + return (FlowExecutionContinuation)ois.readObject(); + } + finally { + ois.close(); + } + } + catch (IOException e) { + throw new ContinuationUnmarshalException("IO problem while creating a flow execution continuation", e); + } + catch (ClassNotFoundException e) { + throw new ContinuationUnmarshalException("Class not found while creating a flow execution continuation", e); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/package.html b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/package.html new file mode 100644 index 00000000..62dd8f40 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/continuation/package.html @@ -0,0 +1,10 @@ + + +

    +Implementation of continuation-based flow execution repositories. +

    +

    +Usage of ClientContinuationFlowExecutionRepository requires Apache's commons-codec. +

    + + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/package.html b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/package.html new file mode 100644 index 00000000..e72e6a0c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/package.html @@ -0,0 +1,13 @@ + + +

    +The flow execution repository subsystem for saving, and restoring managed flow executions. +

    +

    +The central concept defined by this package is the +{@link org.springframework.webflow.execution.repository.FlowExecutionRepository}, representing +a persistent store for one or more FlowExecution objects that capture the state of +user conversations in a form that can be restored on subsequent requests. +

    + + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/AbstractConversationFlowExecutionRepository.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/AbstractConversationFlowExecutionRepository.java new file mode 100644 index 00000000..71fffa01 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/AbstractConversationFlowExecutionRepository.java @@ -0,0 +1,285 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.support; + +import java.io.Serializable; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.Assert; +import org.springframework.webflow.conversation.Conversation; +import org.springframework.webflow.conversation.ConversationException; +import org.springframework.webflow.conversation.ConversationId; +import org.springframework.webflow.conversation.ConversationManager; +import org.springframework.webflow.conversation.ConversationParameters; +import org.springframework.webflow.conversation.NoSuchConversationException; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.repository.BadlyFormattedFlowExecutionKeyException; +import org.springframework.webflow.execution.repository.FlowExecutionKey; +import org.springframework.webflow.execution.repository.FlowExecutionLock; +import org.springframework.webflow.execution.repository.FlowExecutionRepositoryException; +import org.springframework.webflow.execution.repository.NoSuchFlowExecutionException; + +/** + * A convenient base class for flow execution repository implementations that delegate + * to a conversation service for managing conversations that govern the + * persistent state of paused flow executions. + * + * @see ConversationManager + * + * @author Keith Donald + */ +public abstract class AbstractConversationFlowExecutionRepository extends AbstractFlowExecutionRepository { + + /** + * Logger, usable in subclasses + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * The conversation attribute holding conversation scope ("scope"). + */ + private static final String SCOPE_ATTRIBUTE = "scope"; + + /** + * The conversation service to delegate to for managing conversations + * initiated by this repository. + */ + private ConversationManager conversationManager; + + /** + * Constructor for use in subclasses. + * @param executionStateRestorer the transient flow execution state restorer + * @param conversationManager the conversation manager to use + */ + protected AbstractConversationFlowExecutionRepository(FlowExecutionStateRestorer executionStateRestorer, + ConversationManager conversationManager) { + super(executionStateRestorer); + setConversationManager(conversationManager); + } + + /** + * Returns the configured conversation manager. + */ + public ConversationManager getConversationManager() { + return conversationManager; + } + + /** + * Sets the conversation manager to use. + * @param conversationManager the conversation service, may not be null + */ + private void setConversationManager(ConversationManager conversationManager) { + Assert.notNull(conversationManager, "The conversation manager is required"); + this.conversationManager = conversationManager; + } + + public FlowExecutionKey generateKey(FlowExecution flowExecution) { + // we need to generate a key for a new flow execution, so a new conversation has + // started + ConversationParameters parameters = createConversationParameters(flowExecution); + Conversation conversation = conversationManager.beginConversation(parameters); + onBegin(conversation); + FlowExecutionKey key = + new CompositeFlowExecutionKey(conversation.getId(), generateContinuationId(flowExecution)); + if (logger.isDebugEnabled()) { + logger.debug("Generated new key for flow execution '" + flowExecution + "': '" + key + "'"); + } + return key; + } + + public FlowExecutionKey getNextKey(FlowExecution flowExecution, FlowExecutionKey previousKey) { + CompositeFlowExecutionKey key = (CompositeFlowExecutionKey)previousKey; + // the conversation id remains the same for the life of the flow execution + // but the continuation id changes + FlowExecutionKey nextKey = + new CompositeFlowExecutionKey(key.getConversationId(), generateContinuationId(flowExecution)); + if (logger.isDebugEnabled()) { + logger.debug("Generated next key for flow execution '" + flowExecution + "': '" + nextKey + "'; " + + "previous key was '" + key + "'"); + } + return nextKey; + } + + public FlowExecutionLock getLock(FlowExecutionKey key) throws FlowExecutionRepositoryException { + if (logger.isDebugEnabled()) { + logger.debug("Getting lock for flow execution with key '" + key + "'"); + } + // lock the entire conversation + return new ConversationBackedFlowExecutionLock(getConversation(key)); + } + + public abstract FlowExecution getFlowExecution(FlowExecutionKey key) throws FlowExecutionRepositoryException; + + public abstract void putFlowExecution(FlowExecutionKey key, FlowExecution flowExecution) + throws FlowExecutionRepositoryException; + + public void removeFlowExecution(FlowExecutionKey key) throws FlowExecutionRepositoryException { + if (logger.isDebugEnabled()) { + logger.debug("Removing flow execution with key '" + key + "' from repository"); + } + + // end the governing conversation + Conversation conversation = getConversation(key); + conversation.end(); + onEnd(conversation); + } + + public FlowExecutionKey parseFlowExecutionKey(String encodedKey) throws FlowExecutionRepositoryException { + Assert.hasText(encodedKey, "The string encoded flow execution key is required"); + + String[] keyParts = CompositeFlowExecutionKey.keyParts(encodedKey); + + // parse out the conversation id + ConversationId conversationId; + try { + conversationId = conversationManager.parseConversationId(keyParts[0]); + } + catch (ConversationException e) { + throw new BadlyFormattedFlowExecutionKeyException(encodedKey, + "The conversation id '" + keyParts[0] + "' contained in the composite flow execution key '" + + encodedKey + "' is invalid", e); + } + + // parse out the continuation id + Serializable continuationId; + try { + continuationId = parseContinuationId(keyParts[1]); + } + catch (FlowExecutionRepositoryException e) { + throw new BadlyFormattedFlowExecutionKeyException(encodedKey, + "The continuation id '" + keyParts[1] + "' contained in the composite flow execution key '" + + encodedKey + "' is invalid", e); + } + + if (logger.isDebugEnabled()) { + logger.debug("Parsed encoded flow execution key '" + encodedKey + "', extracted conversation id '" + + conversationId + "' and continuation id '" + continuationId + "'"); + } + + return new CompositeFlowExecutionKey(conversationId, continuationId); + } + + // overridable hooks for use in subclasses + + /** + * Factory method that maps a new flow execution to a descriptive + * {@link ConversationParameters conversation parameters} object. + * @param flowExecution the new flow execution + * @return the conversation parameters object to pass to the conversation + * manager when the conversation is started + */ + protected ConversationParameters createConversationParameters(FlowExecution flowExecution) { + FlowDefinition flow = flowExecution.getDefinition(); + return new ConversationParameters(flow.getId(), flow.getCaption(), flow.getDescription()); + } + + /** + * An "on begin conversation" callback, allowing for insertion of custom + * logic after a new conversation has begun. + * This implementation is emtpy. + * @param conversation the conversation that has begun + */ + protected void onBegin(Conversation conversation) { + } + + /** + * An "on conversation end" callback, allowing for insertion of custom logic + * after a conversation has ended (it's {@link Conversation#end()} method has been + * called). + * This implementation is empty. + * @param conversation the conversation that has ended + */ + protected void onEnd(Conversation conversation) { + } + + /** + * Returns the conversation id part of given composite flow execution key. + * @param key the composite key + * @return the conversationId key part + */ + protected ConversationId getConversationId(FlowExecutionKey key) { + return ((CompositeFlowExecutionKey)key).getConversationId(); + } + + /** + * Returns the continuation id part of given composite flow execution key. + * @param key the composite key + * @return the continuation id key part + */ + protected Serializable getContinuationId(FlowExecutionKey key) { + return ((CompositeFlowExecutionKey)key).getContinuationId(); + } + + /** + * Returns the conversation governing the execution of the + * {@link FlowExecution} with the provided key. + * @param key the flow execution key + * @return the governing conversation + * @throws NoSuchFlowExecutionException when the conversation for identified + * flow execution cannot be found + */ + protected Conversation getConversation(FlowExecutionKey key) throws NoSuchFlowExecutionException { + try { + return getConversationManager().getConversation(getConversationId(key)); + } + catch (NoSuchConversationException e) { + throw new NoSuchFlowExecutionException(key, e); + } + } + + /** + * Returns the "conversation scope" for the flow execution with the + * key provided. This is mainly useful for reinitialisation of a flow execution + * after restoration from the repository. + * @param key the flow execution key + * @return the execution's conversation scope + */ + protected MutableAttributeMap getConversationScope(FlowExecutionKey key) { + return (MutableAttributeMap)getConversation(key).getAttribute(SCOPE_ATTRIBUTE); + } + + /** + * Sets the conversation scope attribute for the flow execution with the key + * provided. + * @param key the flow execution key + * @param scope the execution's conversation scope + */ + protected void putConversationScope(FlowExecutionKey key, MutableAttributeMap scope) { + Assert.notNull(scope, "The conversation scope attribute map is required"); + getConversation(key).putAttribute(SCOPE_ATTRIBUTE, scope); + } + + // abstract template methods + + /** + * Template method used to generate a new continuation id for given flow + * execution. Subclasses must override. + * @param flowExecution the flow execution + * @return the continuation id + */ + protected abstract Serializable generateContinuationId(FlowExecution flowExecution); + + /** + * Template method to parse the continuation id from the encoded string. + * @param encodedId the string identifier + * @return the parsed continuation id + */ + protected abstract Serializable parseContinuationId(String encodedId) throws FlowExecutionRepositoryException; + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/AbstractFlowExecutionRepository.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/AbstractFlowExecutionRepository.java new file mode 100644 index 00000000..d2bf2be8 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/AbstractFlowExecutionRepository.java @@ -0,0 +1,73 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.support; + +import org.springframework.util.Assert; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.FlowExecutionFactory; +import org.springframework.webflow.execution.repository.FlowExecutionRepository; + +/** + * Abstract base class for flow execution repository implementations. Does not + * make any assumptions about the storage medium used to store active flow + * executions. Mandates the use of a {@link FlowExecutionStateRestorer}, used + * to rehydrate a flow execution after it has been obtained from storage + * from resume. + *

    + * The configured {@link FlowExecutionStateRestorer} should be compatible + * with the chosen {@link FlowExecution} implementation and is configuration + * as done by a {@link FlowExecutionFactory} (listeners, execution attributes, ...). + * + * @author Erwin Vervaet + */ +public abstract class AbstractFlowExecutionRepository implements FlowExecutionRepository { + + /** + * The strategy for restoring transient flow execution state after + * obtaining it from storage. + */ + private FlowExecutionStateRestorer executionStateRestorer; + + /** + * Constructor for use in subclasses. + * @param executionStateRestorer the transient flow execution state restorer + */ + protected AbstractFlowExecutionRepository(FlowExecutionStateRestorer executionStateRestorer) { + setExecutionStateRestorer(executionStateRestorer); + } + + /** + * Returns the strategy for restoring transient flow execution state after + * obtaining it from storage. + * @return the transient flow execution state restorer + */ + protected FlowExecutionStateRestorer getExecutionStateRestorer() { + return executionStateRestorer; + } + + /** + * Sets the strategy for restoring transient flow execution state after + * obtaining it from storage. + * @param executionStateRestorer the transient flow execution state restorer, + * may not be null + */ + private void setExecutionStateRestorer( + FlowExecutionStateRestorer executionStateRestorer) { + Assert.notNull(executionStateRestorer, "The flow execution state restorer is required"); + this.executionStateRestorer = executionStateRestorer; + } + +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/CompositeFlowExecutionKey.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/CompositeFlowExecutionKey.java new file mode 100644 index 00000000..23208d6d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/CompositeFlowExecutionKey.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.support; + +import java.io.Serializable; + +import org.springframework.util.Assert; +import org.springframework.webflow.conversation.ConversationId; +import org.springframework.webflow.conversation.ConversationManager; +import org.springframework.webflow.execution.repository.BadlyFormattedFlowExecutionKeyException; +import org.springframework.webflow.execution.repository.FlowExecutionKey; +import org.springframework.webflow.execution.repository.continuation.FlowExecutionContinuation; + +/** + * A flow execution key consisting of two parts: + *

      + *
    1. A conversationId, identifying an active conversation managed by a + * {@link ConversationManager}. + *
    2. A continuationId, identifying a restorable + * {@link FlowExecutionContinuation} within a continuation group governed by + * that conversation. + *
    + *

    + * This key is used to restore a FlowExecution from a conversation-service + * backed store. + * + * @see ConversationManager + * @see FlowExecutionContinuation + * + * @author Keith Donald + */ +class CompositeFlowExecutionKey extends FlowExecutionKey { + + /** + * The default conversation id prefix delimiter ("_c"). + */ + private static final String CONVERSATION_ID_PREFIX = "_c"; + + /** + * The default continuation id prefix delimiter ("_k"). + */ + private static final String CONTINUATION_ID_PREFIX = "_k"; + + /** + * The format of the default string-encoded form, as returned + * by toString(). + */ + private static final String FORMAT = + CONVERSATION_ID_PREFIX + "" + CONTINUATION_ID_PREFIX + ""; + + /** + * The conversation id. + */ + private ConversationId conversationId; + + /** + * The continuation id. + */ + private Serializable continuationId; + + /** + * Create a new composite flow execution key given the composing parts. + * @param conversationId the conversation id + * @param continuationId the continuation id + */ + public CompositeFlowExecutionKey(ConversationId conversationId, Serializable continuationId) { + Assert.notNull(conversationId, "The conversation id is required"); + Assert.notNull(continuationId, "The continuation id is required"); + this.conversationId = conversationId; + this.continuationId = continuationId; + } + + /** + * Returns the conversation id. + */ + public ConversationId getConversationId() { + return conversationId; + } + + /** + * Returns the continuation id. + */ + public Serializable getContinuationId() { + return continuationId; + } + + public boolean equals(Object obj) { + if (!(obj instanceof CompositeFlowExecutionKey)) { + return false; + } + CompositeFlowExecutionKey other = (CompositeFlowExecutionKey)obj; + return conversationId.equals(other.conversationId) && continuationId.equals(other.continuationId); + } + + public int hashCode() { + return conversationId.hashCode() + continuationId.hashCode(); + } + + public String toString() { + return new StringBuffer().append(CONVERSATION_ID_PREFIX).append(getConversationId()) + .append(CONTINUATION_ID_PREFIX).append(getContinuationId()).toString(); + } + + // static helpers + + /** + * Helper that splits the string-form of an instance of this class into its + * "parts" so the parts can be easily parsed. + * @param encodedKey the string-encoded composite flow execution key + * @return the composite key parts as a String array (conversationId = 0, + * continuationId = 1) + */ + public static String[] keyParts(String encodedKey) throws BadlyFormattedFlowExecutionKeyException { + if (!encodedKey.startsWith(CONVERSATION_ID_PREFIX)) { + throw new BadlyFormattedFlowExecutionKeyException(encodedKey, FORMAT); + } + int continuationStart = encodedKey.indexOf(CONTINUATION_ID_PREFIX, CONVERSATION_ID_PREFIX.length()); + if (continuationStart == -1) { + throw new BadlyFormattedFlowExecutionKeyException(encodedKey, FORMAT); + } + String conversationId = encodedKey.substring(CONVERSATION_ID_PREFIX.length(), continuationStart); + String continuationId = encodedKey.substring(continuationStart + CONTINUATION_ID_PREFIX.length()); + return new String[] { conversationId, continuationId }; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/ConversationBackedFlowExecutionLock.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/ConversationBackedFlowExecutionLock.java new file mode 100644 index 00000000..eddbed0c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/ConversationBackedFlowExecutionLock.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.support; + +import org.springframework.webflow.conversation.Conversation; +import org.springframework.webflow.conversation.ConversationManager; +import org.springframework.webflow.execution.repository.FlowExecutionLock; + +/** + * A flow execution lock that locks a conversation managed by a + * {@link ConversationManager}. + *

    + * This implementation ensures multiple threads cannot manipulate the same + * conversation at the same time. The locked conversation is the sole gateway to + * a flow execution, and a lock on it prevents access to any associated + * execution. + * + * @see ConversationManager + * @see Conversation + * @see Conversation#lock() + * @see Conversation#unlock() + * + * @author Keith Donald + */ +class ConversationBackedFlowExecutionLock implements FlowExecutionLock { + + /** + * The conversation to lock. + */ + private Conversation conversation; + + /** + * Creates a new conversation-backed flow execution lock. + * @param conversation the conversation to lock + */ + public ConversationBackedFlowExecutionLock(Conversation conversation) { + this.conversation = conversation; + } + + public void lock() { + conversation.lock(); + } + + public void unlock() { + conversation.unlock(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/FlowExecutionStateRestorer.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/FlowExecutionStateRestorer.java new file mode 100644 index 00000000..71516abf --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/FlowExecutionStateRestorer.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.support; + +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.execution.FlowExecution; + +/** + * A support strategy used by repositories that serialize flow executions to + * restore transient execution state after deserialization. + * + * @author Keith Donald + */ +public interface FlowExecutionStateRestorer { + + /** + * Restore the transient state of the flow execution. + * @param flowExecution the (potentially deserialized) flow execution + * @param conversationScope the execution's conversation scope, which is + * typically not part of the serialized form since it could be shared + * by multiple physical flow execution copies all sharing the + * same logical conversation + * @return the restored flow execution + */ + public FlowExecution restoreState(FlowExecution flowExecution, MutableAttributeMap conversationScope); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/InvalidContinuationIdException.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/InvalidContinuationIdException.java new file mode 100644 index 00000000..17de5d5a --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/InvalidContinuationIdException.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.support; + +import java.io.Serializable; + +import org.springframework.webflow.execution.repository.FlowExecutionRepositoryException; + +/** + * Thrown when no flow execution continuation exists with the provided id. + * This might occur if the continuation has expired or was explictly invalidated + * but a client's browser page cache still references it. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class InvalidContinuationIdException extends FlowExecutionRepositoryException { + + /** + * The unique continuation identifier that was invalid. + */ + private Serializable continuationId; + + /** + * Creates an invalid continuation id exception. + * @param continuationId the invalid continuation id + */ + public InvalidContinuationIdException(Serializable continuationId) { + super("The continuation id '" + continuationId + "' is invalid. Access to flow execution denied."); + } + + /** + * Returns the continuation id. + */ + public Serializable getContinuationId() { + return continuationId; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/SimpleFlowExecutionRepository.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/SimpleFlowExecutionRepository.java new file mode 100644 index 00000000..2bcfcb22 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/SimpleFlowExecutionRepository.java @@ -0,0 +1,223 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.support; + +import java.io.Serializable; + +import org.springframework.util.Assert; +import org.springframework.webflow.conversation.ConversationManager; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.repository.FlowExecutionKey; +import org.springframework.webflow.execution.repository.PermissionDeniedFlowExecutionAccessException; +import org.springframework.webflow.util.RandomGuidUidGenerator; +import org.springframework.webflow.util.UidGenerator; + +/** + * Conversation manager based flow execution repository that stores + * exactly one flow execution per conversation. + *

    + * It is important to note that by default use of this repository does not + * allow for duplicate submission in conjunction with browser navigational buttons + * (such as the back button). Specifically, if you attempt to "go back" and resubmit, + * the continuation id stored on the page in your browser history will not + * match the continuation id of the flow execution entry and access to the + * conversation will be disallowed. This is because the + * continuationId changes on each request to consistently prevent + * the possibility of duplicate submission ({@link #setAlwaysGenerateNewNextKey(boolean)}). + *

    + * This repository is specifically designed to be 'simple': incurring minimal + * resources and overhead, as only one {@link FlowExecution} is stored per + * conversation. This repository implementation should only be used + * when you do not have to support browser navigational button use, e.g. you + * lock down the browser and require that all navigational events to be routed + * explicitly through Spring Web Flow. + * + * @author Erwin Vervaet + * @author Keith Donald + */ +public class SimpleFlowExecutionRepository extends AbstractConversationFlowExecutionRepository { + + /** + * The conversation attribute holding the flow execution entry. + */ + private static final String FLOW_EXECUTION_ENTRY_ATTRIBUTE = "flowExecutionEntry"; + + /** + * Flag to indicate whether or not a new flow execution key should always be + * generated before each put call. Default is true. + */ + private boolean alwaysGenerateNewNextKey = true; + + /** + * The uid generation strategy to use. + */ + private UidGenerator continuationIdGenerator = new RandomGuidUidGenerator(); + + /** + * Create a new simple repository using given state restorer and conversation manager. + * @param executionStateRestorer the flow execution state restoration strategy to use + * @param conversationManager the conversation manager to use + */ + public SimpleFlowExecutionRepository(FlowExecutionStateRestorer executionStateRestorer, + ConversationManager conversationManager) { + super(executionStateRestorer, conversationManager); + } + + /** + * Returns whether or not a new flow execution key should always be + * generated before each put call. Default is true. + */ + public boolean isAlwaysGenerateNewNextKey() { + return alwaysGenerateNewNextKey; + } + + /** + * Sets a flag indicating if a new {@link FlowExecutionKey} should always be + * generated before each put call. By setting this to false a FlowExecution + * can remain identified by the same key throughout its life. + */ + public void setAlwaysGenerateNewNextKey(boolean alwaysGenerateNewNextKey) { + this.alwaysGenerateNewNextKey = alwaysGenerateNewNextKey; + } + + /** + * Returns the uid generation strategy used to generate continuation + * identifiers. Defaults to {@link RandomGuidUidGenerator}. + */ + public UidGenerator getContinuationIdGenerator() { + return continuationIdGenerator; + } + + /** + * Sets the uid generation strategy used to generate unique continuation + * identifiers for {@link FlowExecutionKey flow execution keys}. + */ + public void setContinuationIdGenerator(UidGenerator continuationIdGenerator) { + Assert.notNull(continuationIdGenerator, "The continuation id generator is required"); + this.continuationIdGenerator = continuationIdGenerator; + } + + public FlowExecutionKey getNextKey(FlowExecution flowExecution, FlowExecutionKey previousKey) { + if (isAlwaysGenerateNewNextKey()) { + return super.getNextKey(flowExecution, previousKey); + } + else { + return previousKey; + } + } + + public FlowExecution getFlowExecution(FlowExecutionKey key) { + if (logger.isDebugEnabled()) { + logger.debug("Getting flow execution with key '" + key + "'"); + } + + try { + FlowExecution execution = getEntry(key).access(getContinuationId(key)); + // it could be that the entry was serialized out and read back in, so + // we need to restore transient flow execution state + return getExecutionStateRestorer().restoreState(execution, getConversationScope(key)); + } + catch (InvalidContinuationIdException e) { + throw new PermissionDeniedFlowExecutionAccessException(key, e); + } + } + + public void putFlowExecution(FlowExecutionKey key, FlowExecution flowExecution) { + if (logger.isDebugEnabled()) { + logger.debug("Putting flow execution '" + flowExecution + "' into repository with key '" + key + "'"); + } + + FlowExecutionEntry entry = new FlowExecutionEntry(getContinuationId(key), flowExecution); + putEntry(key, entry); + putConversationScope(key, flowExecution.getConversationScope()); + } + + protected Serializable generateContinuationId(FlowExecution flowExecution) { + return continuationIdGenerator.generateUid(); + } + + protected Serializable parseContinuationId(String encodedId) { + return continuationIdGenerator.parseUid(encodedId); + } + + // internal helpers + + /** + * Lookup the entry for keyed flow execution in the governing conversation. + */ + private FlowExecutionEntry getEntry(FlowExecutionKey key) { + FlowExecutionEntry entry = + (FlowExecutionEntry)getConversation(key).getAttribute(FLOW_EXECUTION_ENTRY_ATTRIBUTE); + if (entry == null) { + throw new IllegalStateException("No '" + FLOW_EXECUTION_ENTRY_ATTRIBUTE + + "' attribute present in the governing conversation: " + + "possible programmer error -- do not call get before calling put"); + } + return entry; + } + + /** + * Store given flow execution entry in the governing conversation using given key. + * @param key the key to use + * @param entry the entry to store + */ + private void putEntry(FlowExecutionKey key, FlowExecutionEntry entry) { + getConversation(key).putAttribute(FLOW_EXECUTION_ENTRY_ATTRIBUTE, entry); + } + + /** + * Simple holder for a flow execution. In order to access the held flow + * execution you must present a valid continuationId. + * + * @author Keith Donald + */ + private static class FlowExecutionEntry implements Serializable { + + /** + * The id required to access the execution. + */ + private Serializable continuationId; + + /** + * The flow execution. + */ + private FlowExecution flowExecution; + + /** + * Creates a new flow execution entry. + * @param continuationId the continuation id + * @param flowExecution the flow execution + */ + public FlowExecutionEntry(Serializable continuationId, FlowExecution flowExecution) { + this.continuationId = continuationId; + this.flowExecution = flowExecution; + } + + /** + * Access the wrapped flow execution, using given continuation id as a password. + * @param continuationId the continuation id to match + * @return the flow execution + * @throws InvalidContinuationIdException given continuation id does not match the + * continuation id stored in this entry + */ + public FlowExecution access(Serializable continuationId) throws InvalidContinuationIdException { + if (!this.continuationId.equals(continuationId)) { + throw new InvalidContinuationIdException(continuationId); + } + return flowExecution; + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/package.html b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/package.html new file mode 100644 index 00000000..7d5b2769 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/package.html @@ -0,0 +1,6 @@ + + +General purpose implementation assistance for flow execution repositories. +Supports the other repository packages within the framework. + + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/support/ApplicationView.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/support/ApplicationView.java new file mode 100644 index 00000000..8be62595 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/support/ApplicationView.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.support; + +import java.util.Collections; +import java.util.Map; + +import org.springframework.util.ObjectUtils; +import org.springframework.webflow.execution.ViewSelection; + +/** + * Concrete response type that requests the rendering of a local, internal + * application view resource such as a JSP, Velocity, or FreeMarker template. + *

    + * This is typically the most common type of view selection. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public final class ApplicationView extends ViewSelection { + + /** + * The name of the view (or page or other response) to render. This name may + * identify a logical view resource or may be a physical + * path to an internal view template. + */ + private final String viewName; + + /** + * A map of the application data to make available to the view for + * rendering. + */ + private final Map model; + + /** + * Creates a new application view. + * @param viewName the name (or resource identifier) of the view that should + * be rendered + * @param model the map of application model data to make available to the + * view during rendering; entries consist of model names (Strings) to model + * objects (Objects), model entries may not be null, but the model Map may + * be null if there is no model data + */ + public ApplicationView(String viewName, Map model) { + if (model == null) { + model = Collections.EMPTY_MAP; + } + this.viewName = viewName; + this.model = model; + } + + /** + * Returns the name of the view to render. + */ + public String getViewName() { + return viewName; + } + + /** + * Return the view's application model that should be made available during + * the rendering process. Never returns null. The returned map is unmodifiable. + */ + public Map getModel() { + return Collections.unmodifiableMap(model); + } + + public boolean equals(Object o) { + if (!(o instanceof ApplicationView)) { + return false; + } + ApplicationView other = (ApplicationView)o; + return ObjectUtils.nullSafeEquals(viewName, other.viewName) && model.equals(other.model); + } + + public int hashCode() { + return (viewName != null ? viewName.hashCode() : 0) + model.hashCode(); + } + + public String toString() { + return "'" + viewName + "' [" + model.keySet() + "]"; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/support/EventFactorySupport.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/support/EventFactorySupport.java new file mode 100644 index 00000000..53e85f03 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/support/EventFactorySupport.java @@ -0,0 +1,262 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.support; + +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.CollectionUtils; +import org.springframework.webflow.execution.Event; + +/** + * A convenience support class assisting in the creation of {@link Event} objects. + *

    + * This class can be used as a simple utility class when you need to create + * common event objects. Alternatively you could extend it as a base support class + * when creating custom event factories. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class EventFactorySupport { + + /** + * The default 'success' result event identifier ("success"). + */ + private static final String SUCCESS_EVENT_ID = "success"; + + /** + * The default 'error' result event identifier ("error"). + */ + private static final String ERROR_EVENT_ID = "error"; + + /** + * The default 'yes' result event identifier ("yes"). + */ + private static final String YES_EVENT_ID = "yes"; + + /** + * The default 'no' result event identifier ("no"). + */ + private static final String NO_EVENT_ID = "no"; + + /** + * The default 'null' result event identifier ("null"). + */ + private static final String NULL_EVENT_ID = "null"; + + /** + * The default 'exception' event attribute name ("exception"). + */ + private static final String EXCEPTION_ATTRIBUTE_NAME = "exception"; + + /** + * The default 'result' event attribute name ("result"). + */ + private static final String RESULT_ATTRIBUTE_NAME = "result"; + + /** + * The success event identifier. + */ + private String successEventId = SUCCESS_EVENT_ID; + + /** + * The error event identifier. + */ + private String errorEventId = ERROR_EVENT_ID; + + /** + * The yes event identifier. + */ + private String yesEventId = YES_EVENT_ID; + + /** + * The no event identifier. + */ + private String noEventId = NO_EVENT_ID; + + /** + * The null event identifier. + */ + private String nullEventId = NULL_EVENT_ID; + + /** + * The exception event attribute name. + */ + private String exceptionAttributeName = EXCEPTION_ATTRIBUTE_NAME; + + /** + * The result event attribute name. + */ + private String resultAttributeName = RESULT_ATTRIBUTE_NAME; + + public String getSuccessEventId() { + return successEventId; + } + + public void setSuccessEventId(String successEventId) { + this.successEventId = successEventId; + } + + public String getErrorEventId() { + return errorEventId; + } + + public void setErrorEventId(String errorEventId) { + this.errorEventId = errorEventId; + } + + public String getYesEventId() { + return yesEventId; + } + + public void setYesEventId(String yesEventId) { + this.yesEventId = yesEventId; + } + + public String getNoEventId() { + return noEventId; + } + + public void setNoEventId(String noEventId) { + this.noEventId = noEventId; + } + + public String getNullEventId() { + return nullEventId; + } + + public void setNullEventId(String nullEventId) { + this.nullEventId = nullEventId; + } + + public String getExceptionAttributeName() { + return exceptionAttributeName; + } + + public void setExceptionAttributeName(String exceptionAttributeName) { + this.exceptionAttributeName = exceptionAttributeName; + } + + public String getResultAttributeName() { + return resultAttributeName; + } + + public void setResultAttributeName(String resultAttributeName) { + this.resultAttributeName = resultAttributeName; + } + + /** + * Returns a "success" event. + * @param source the source of the event + */ + public Event success(Object source) { + return event(source, getSuccessEventId()); + } + + /** + * Returns a "success" event with the provided result object as an + * attribute. The result object is identified by the attribute name + * {@link #getResultAttributeName()}. + * @param source the source of the event + * @param result the action success result + */ + public Event success(Object source, Object result) { + return event(source, getSuccessEventId(), getResultAttributeName(), result); + } + + /** + * Returns an "error" event. + * @param source the source of the event + */ + public Event error(Object source) { + return event(source, getErrorEventId()); + } + + /** + * Returns an "error" event caused by the provided exception. + * @param source the source of the event + * @param e the exception that caused the error event, to be put as an + * event attribute under the name {@link #getExceptionAttributeName()} + */ + public Event error(Object source, Exception e) { + return event(source, getErrorEventId(), getExceptionAttributeName(), e); + } + + /** + * Returns a "yes" event. + * @param source the source of the event + */ + public Event yes(Object source) { + return event(source, getYesEventId()); + } + + /** + * Returns a "no" result event. + * @param source the source of the event + */ + public Event no(Object source) { + return event(source, getNoEventId()); + } + + /** + * Returns an event to communicate an occurrence of a boolean expression. + * @param source the source of the event + * @param booleanResult the boolean + * @return yes or no + */ + public Event event(Object source, boolean booleanResult) { + if (booleanResult) { + return yes(source); + } + else { + return no(source); + } + } + + /** + * Returns a event with the specified identifier. + * @param source the source of the event + * @param eventId the result event identifier + * @return the event + */ + public Event event(Object source, String eventId) { + return new Event(source, eventId, null); + } + + /** + * Returns a event with the specified identifier and the specified set of + * attributes. + * @param source the source of the event + * @param eventId the result event identifier + * @param attributes the event payload attributes + * @return the event + */ + public Event event(Object source, String eventId, AttributeMap attributes) { + return new Event(source, eventId, attributes); + } + + /** + * Returns a result event with the specified identifier and + * a single attribute. + * @param source the source of the event + * @param eventId the result id + * @param attributeName the attribute name + * @param attributeValue the attribute value + * @return the event + */ + public Event event(Object source, String eventId, String attributeName, Object attributeValue) { + return new Event(source, eventId, CollectionUtils.singleEntryMap(attributeName, attributeValue)); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/support/ExternalRedirect.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/support/ExternalRedirect.java new file mode 100644 index 00000000..dd658dd2 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/support/ExternalRedirect.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.support; + +import org.springframework.util.Assert; +import org.springframework.webflow.execution.ViewSelection; + +/** + * Concrete response type that requests a redirect to an external URL outside of + * Spring Web Flow. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public final class ExternalRedirect extends ViewSelection { + + /** + * The arbitrary url path to redirect to. + */ + private final String url; + + /** + * Creates an external redirect request. + * @param url the url path to redirect to + */ + public ExternalRedirect(String url) { + Assert.notNull(url, "The external URL to redirect to is required"); + this.url = url; + } + + /** + * Returns the external URL to redirect to. + */ + public String getUrl() { + return url; + } + + public boolean equals(Object o) { + if (!(o instanceof ExternalRedirect)) { + return false; + } + ExternalRedirect other = (ExternalRedirect)o; + return url.equals(other.url); + } + + public int hashCode() { + return url.hashCode(); + } + + public String toString() { + return "externalRedirect:'" + url + "'"; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/support/FlowDefinitionRedirect.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/support/FlowDefinitionRedirect.java new file mode 100644 index 00000000..033bb86e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/support/FlowDefinitionRedirect.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.support; + +import java.util.Collections; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.webflow.execution.ViewSelection; + +/** + * Concrete response type that requests that a new execution of a flow + * definition (representing the start of a new conversation) be launched. + *

    + * This allows "redirect to new flow" semantics; useful for restarting a flow + * after completion, or starting an entirely new flow from within the end state + * of another flow definition. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public final class FlowDefinitionRedirect extends ViewSelection { + + /** + * The id of the flow definition to launch. + */ + private final String flowDefinitionId; + + /** + * A map of input attributes to pass to the flow. + */ + private final Map executionInput; + + /** + * Creates a new flow definition redirect. + * @param flowDefinitionId the id of the flow definition to launch + * @param executionInput the input data to pass to the new flow execution on launch + */ + public FlowDefinitionRedirect(String flowDefinitionId, Map executionInput) { + Assert.hasText(flowDefinitionId, "The flow definition id is required"); + this.flowDefinitionId = flowDefinitionId; + if (executionInput == null) { + executionInput = Collections.EMPTY_MAP; + } + this.executionInput = executionInput; + } + + /** + * Return the id of the flow definition to launch a new execution of. + */ + public String getFlowDefinitionId() { + return flowDefinitionId; + } + + /** + * Return the flow execution input map as an unmodifiable map. Never returns + * null. + */ + public Map getExecutionInput() { + return Collections.unmodifiableMap(executionInput); + } + + public boolean equals(Object o) { + if (!(o instanceof FlowDefinitionRedirect)) { + return false; + } + FlowDefinitionRedirect other = (FlowDefinitionRedirect)o; + return flowDefinitionId.equals(other.flowDefinitionId) && executionInput.equals(other.executionInput); + } + + public int hashCode() { + return flowDefinitionId.hashCode() + executionInput.hashCode(); + } + + public String toString() { + return "flowRedirect:'" + flowDefinitionId + "'"; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/support/FlowExecutionRedirect.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/support/FlowExecutionRedirect.java new file mode 100644 index 00000000..3442d83e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/support/FlowExecutionRedirect.java @@ -0,0 +1,59 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.support; + +import java.io.ObjectStreamException; + +import org.springframework.webflow.engine.ViewState; +import org.springframework.webflow.execution.ViewSelection; + +/** + * Concrete response type that refreshes an application view by redirecting + * to an existing, active Spring Web Flow execution at a unique + * SWF-specific flow execution URL. This enables the triggering of + * post-redirect-get semantics from within an active flow execution. + *

    + * Once the redirect response is issued a new request is initiated by the + * browser targeted at the flow execution URL. The URL is stabally refreshable + * (and bookmarkable) while the conversation remains active, safely triggering a + * {@link ViewState#refresh(org.springframework.webflow.execution.RequestContext)} + * on each access. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public final class FlowExecutionRedirect extends ViewSelection { + + /** + * The single instance of this class. + */ + public static final FlowExecutionRedirect INSTANCE = new FlowExecutionRedirect(); + + /** + * Avoid instantiation. + */ + private FlowExecutionRedirect() { + } + + // resolve the singleton instance + private Object readResolve() throws ObjectStreamException { + return INSTANCE; + } + + public String toString() { + return "redirect:"; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/support/package.html b/spring-webflow/src/main/java/org/springframework/webflow/execution/support/package.html new file mode 100644 index 00000000..f9cf0083 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/support/package.html @@ -0,0 +1,7 @@ + + +

    +Useful generic support implementations of core flow execution types. +

    + + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/FlowExecutor.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/FlowExecutor.java new file mode 100644 index 00000000..82283d6c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/FlowExecutor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor; + +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.FlowException; + +/** + * The central facade and entry-point service interface into the Spring Web Flow + * system for driving the executions of flow definitions. This interface + * defines a coarse-grained system boundary suitable for invocation by most + * clients. + *

    + * Implementations of this interface abstract away much of the internal + * complexity of the web flow execution subsystem, which consists of launching + * and resuming managed flow executions from repositories. + * + * @author Keith Donald + */ +public interface FlowExecutor { + + /** + * Launch a new execution of identified flow definition in the context of + * the current external client request. + * @param flowDefinitionId the unique id of the flow definition to launch + * @param context the external context representing the state of a request + * into Spring Web Flow from an external system + * @return the starting response instruction + * @throws FlowException if an exception occured launching the new flow + * execution + */ + public ResponseInstruction launch(String flowDefinitionId, ExternalContext context) throws FlowException; + + /** + * Resume an existing, paused flow execution by signaling an event against + * its current state. + * @param flowExecutionKey the identifying key of a paused flow execution + * that is waiting to resume on the occurrence of a user event + * @param eventId the user event that occured + * @param context the external context representing the state of a request + * into Spring Web Flow from an external system + * @return the next response instruction + * @throws FlowException if an exception occured resuming the existing flow + * execution + */ + public ResponseInstruction resume(String flowExecutionKey, String eventId, ExternalContext context) + throws FlowException; + + /** + * Reissue the last response instruction issued by the flow execution. This is + * a logical refresh operation that allows the "current response" to be + * re-issued. This operation is idempotent and does not affect the state of the flow + * execution. + * @param flowExecutionKey the identifying key of a paused flow execution + * that is waiting to resume on the ocurrence of a user event + * @param context the external context representing the state of a request + * into Spring Web Flow from an external system + * @return the current response instruction + * @throws FlowException if an exception occured retrieving the current + * response instruction + */ + public ResponseInstruction refresh(String flowExecutionKey, ExternalContext context) throws FlowException; +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/FlowExecutorImpl.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/FlowExecutorImpl.java new file mode 100644 index 00000000..e3988915 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/FlowExecutorImpl.java @@ -0,0 +1,307 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.util.Assert; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.context.ExternalContextHolder; +import org.springframework.webflow.core.FlowException; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.registry.FlowDefinitionLocator; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.FlowExecutionFactory; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.repository.FlowExecutionKey; +import org.springframework.webflow.execution.repository.FlowExecutionLock; +import org.springframework.webflow.execution.repository.FlowExecutionRepository; + +/** + * The default implementation of the central facade for driving the + * execution of flows within an application. + *

    + * This object is responsible for creating and starting new flow executions as + * requested by clients, as well as signaling events for processing by existing, + * paused executions (that are waiting to be resumed in response to a user + * event). + *

    + * This object is a facade or entry point into the Spring Web Flow execution + * system and makes the overall system easier to use. The name executor + * was chosen as executors drive executions. + *

    + * Commonly used configurable properties
    + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
    namedescriptiondefault
    definitionLocatorThe service locator responsible for loading flow definitions to execute.None
    executionFactoryThe factory responsible for creating new flow executions.None
    executionRepositoryThe repository responsible for managing flow execution persistence.None
    inputMapperThe service responsible for mapping attributes of + * {@link ExternalContext external contexts} that request to launch new + * {@link FlowExecution flow executions}. + * After mapping, the target map is then passed to the FlowExecution, exposing + * external context attributes as input to the flow during startup.A + * {@link org.springframework.webflow.executor.RequestParameterInputMapper request parameter mapper}, + * which exposes all request parameters in to the flow execution for input + * mapping.
    + *

    + * + * @see FlowDefinitionLocator + * @see FlowExecutionFactory + * @see FlowExecutionRepository + * @see AttributeMapper + * + * @author Erwin Vervaet + * @author Keith Donald + * @author Colin Sampaleanu + */ +public class FlowExecutorImpl implements FlowExecutor { + + private static final Log logger = LogFactory.getLog(FlowExecutorImpl.class); + + /** + * A locator to access flow definitions registered in a central registry. + */ + private FlowDefinitionLocator definitionLocator; + + /** + * An abstract factory for creating a new execution of a flow definition. + */ + private FlowExecutionFactory executionFactory; + + /** + * An repository used to save, update, and load existing flow executions + * to/from a persistent store. + */ + private FlowExecutionRepository executionRepository; + + /** + * The service responsible for mapping attributes of an + * {@link ExternalContext} to a new {@link FlowExecution} during the + * {@link #launch(String, ExternalContext) launch flow} operation. + *

    + * This allows developers to control what attributes are made available in + * the inputMap to new top-level flow executions. The + * starting execution may then choose to map that available input into its + * own local scope. + *

    + * The default implementation simply exposes all request parameters as flow + * execution input attributes. May be null. + */ + private AttributeMapper inputMapper = new RequestParameterInputMapper(); + + /** + * Create a new flow executor. + * @param definitionLocator the locator for accessing flow definitions to + * execute + * @param executionFactory the factory for creating executions of flow + * definitions + * @param executionRepository the repository for persisting paused flow + * executions + */ + public FlowExecutorImpl(FlowDefinitionLocator definitionLocator, FlowExecutionFactory executionFactory, + FlowExecutionRepository executionRepository) { + Assert.notNull(definitionLocator, "The locator for accessing flow definitions is required"); + Assert.notNull(executionFactory, "The execution factory for creating new flow executions is required"); + Assert.notNull(executionRepository, "The repository for persisting flow executions is required"); + this.definitionLocator = definitionLocator; + this.executionFactory = executionFactory; + this.executionRepository = executionRepository; + } + + /** + * Exposes the configured input mapper to subclasses and privileged + * accessors. + * @return the input mapper + */ + public AttributeMapper getInputMapper() { + return inputMapper; + } + + /** + * Set the service responsible for mapping attributes of an + * {@link ExternalContext} to a new {@link FlowExecution} during the + * {@link #launch(String, ExternalContext) launch flow} operation. + *

    + * The default implementation simply exposes all request parameters as flow + * execution input attributes. May be null. + * @see RequestParameterInputMapper + */ + public void setInputMapper(AttributeMapper inputMapper) { + this.inputMapper = inputMapper; + } + + /** + * Exposes the configured flow definition locator to subclasses and + * privileged accessors. + * @return the flow definition locator + */ + public FlowDefinitionLocator getDefinitionLocator() { + return definitionLocator; + } + + /** + * Exposes the configured execution factory to subclasses and privileged + * accessors. + * @return the execution factory + */ + public FlowExecutionFactory getExecutionFactory() { + return executionFactory; + } + + /** + * Exposes the execution repository to subclasses and privileged accessors. + * @return the execution repository + */ + public FlowExecutionRepository getExecutionRepository() { + return executionRepository; + } + + public ResponseInstruction launch(String flowDefinitionId, ExternalContext context) throws FlowException { + if (logger.isDebugEnabled()) { + logger.debug("Launching flow execution for flow definition '" + flowDefinitionId + "'"); + } + // expose external context as a thread-bound service + ExternalContextHolder.setExternalContext(context); + try { + FlowDefinition flowDefinition = definitionLocator.getFlowDefinition(flowDefinitionId); + FlowExecution flowExecution = executionFactory.createFlowExecution(flowDefinition); + ViewSelection selectedView = flowExecution.start(createInput(context), context); + if (flowExecution.isActive()) { + // execution still active => store it in the repository + FlowExecutionKey key = executionRepository.generateKey(flowExecution); + executionRepository.putFlowExecution(key, flowExecution); + return new ResponseInstruction(key.toString(), flowExecution, selectedView); + } + else { + // execution already ended => just render the selected view + return new ResponseInstruction(flowExecution, selectedView); + } + } + finally { + ExternalContextHolder.setExternalContext(null); + } + } + + public ResponseInstruction resume(String flowExecutionKey, String eventId, ExternalContext context) + throws FlowException { + if (logger.isDebugEnabled()) { + logger.debug("Resuming flow execution with key '" + flowExecutionKey + + "' on user event '" + eventId + "'"); + } + // expose external context as a thread-bound service + ExternalContextHolder.setExternalContext(context); + try { + FlowExecutionKey key = executionRepository.parseFlowExecutionKey(flowExecutionKey); + FlowExecutionLock lock = executionRepository.getLock(key); + // make sure we're the only one manipulating the flow execution + lock.lock(); + try { + FlowExecution flowExecution = executionRepository.getFlowExecution(key); + ViewSelection selectedView = flowExecution.signalEvent(eventId, context); + if (flowExecution.isActive()) { + // execution still active => store it in the repository + key = executionRepository.getNextKey(flowExecution, key); + executionRepository.putFlowExecution(key, flowExecution); + return new ResponseInstruction(key.toString(), flowExecution, selectedView); + } + else { + // execution ended => remove it from the repository + executionRepository.removeFlowExecution(key); + return new ResponseInstruction(flowExecution, selectedView); + } + } + finally { + lock.unlock(); + } + } + finally { + ExternalContextHolder.setExternalContext(null); + } + } + + public ResponseInstruction refresh(String flowExecutionKey, ExternalContext context) throws FlowException { + if (logger.isDebugEnabled()) { + logger.debug("Refreshing flow execution with key '" + flowExecutionKey + "'"); + } + // expose external context as a thread-bound service + ExternalContextHolder.setExternalContext(context); + try { + FlowExecutionKey key = executionRepository.parseFlowExecutionKey(flowExecutionKey); + FlowExecutionLock lock = executionRepository.getLock(key); + // make sure we're the only one manipulating the flow execution + lock.lock(); + try { + FlowExecution flowExecution = executionRepository.getFlowExecution(key); + ViewSelection selectedView = flowExecution.refresh(context); + // don't generate a new key for a refresh, just update + // the flow execution with it's existing key + executionRepository.putFlowExecution(key, flowExecution); + return new ResponseInstruction(key.toString(), flowExecution, selectedView); + } + finally { + lock.unlock(); + } + } + finally { + ExternalContextHolder.setExternalContext(null); + } + } + + // helper methods + + /** + * Factory method that creates the input attribute map for a newly created + * {@link FlowExecution}. This implementation uses the registered input mapper, + * if any. + * @param context the external context + * @return the input map, or null if no input + */ + protected MutableAttributeMap createInput(ExternalContext context) { + if (inputMapper != null) { + MutableAttributeMap inputMap = new LocalAttributeMap(); + inputMapper.map(context, inputMap, null); + return inputMap; + } + else { + return null; + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/RequestParameterInputMapper.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/RequestParameterInputMapper.java new file mode 100644 index 00000000..10c2e299 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/RequestParameterInputMapper.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor; + +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.binding.mapping.MappingContext; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.collection.MutableAttributeMap; + +/** + * Simple attribute mapper implementation that puts all entries in the + * request parameter map of a source {@link ExternalContext} into the + * FlowExecution inputMap. This makes request parameters available to launching + * flows for input mapping. + *

    + * Used by {@link FlowExecutorImpl} as the default AttributeMapper + * implementation. + * + * @see ExternalContext#getRequestParameterMap() + * @see FlowExecutor#launch(String, ExternalContext) + * + * @author Keith Donald + */ +public class RequestParameterInputMapper implements AttributeMapper { + public void map(Object source, Object target, MappingContext context) { + ExternalContext externalContext = (ExternalContext)source; + MutableAttributeMap inputMap = (MutableAttributeMap)target; + inputMap.putAll(externalContext.getRequestParameterMap().asAttributeMap()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/ResponseInstruction.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/ResponseInstruction.java new file mode 100644 index 00000000..f4d7af7d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/ResponseInstruction.java @@ -0,0 +1,208 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor; + +import java.io.Serializable; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.util.Assert; +import org.springframework.webflow.execution.FlowExecutionContext; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.execution.support.ExternalRedirect; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; +import org.springframework.webflow.execution.support.FlowExecutionRedirect; + +/** + * Immutable value object that provides clients with information about a + * response to issue. + *

    + * There are five different types of response instruction: + *

      + *
    • An {@link #isApplicationView() application view}.
    • + *
    • A {@link #isFlowExecutionRedirect() flow execution redirect}, showing + * an application view via a redirect that refreshes an ongoing flow + * execution.
    • + *
    • A {@link #isFlowDefinitionRedirect() flow definition redirect}, + * launching an entirely new flow execution.
    • + *
    • An {@link #isExternalRedirect() external redirect}, redirecting + * to an external URL.
    • + *
    • A {@link #isNull() null view}, not showing a response at all.
    • + *
    + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class ResponseInstruction implements Serializable { + + /** + * The persistent identifier of the flow execution that + * resulted in this response instruction. + */ + private String flowExecutionKey; + + /** + * Basic state info on the flow execution. + */ + private transient FlowExecutionContext flowExecutionContext; + + /** + * The view selection that was made. + */ + private ViewSelection viewSelection; + + /** + * Create a new response instruction for a paused flow execution. + * @param flowExecutionKey the persistent identifier of the flow execution + * @param flowExecutionContext the current flow execution context + * @param viewSelection the selected view + */ + public ResponseInstruction(String flowExecutionKey, FlowExecutionContext flowExecutionContext, + ViewSelection viewSelection) { + Assert.notNull(flowExecutionKey, "The flow execution key is required"); + this.flowExecutionKey = flowExecutionKey; + init(flowExecutionContext, viewSelection); + } + + /** + * Create a new response instruction for an ended flow execution. No + * flow execution key needs to be provided since the flow execution no longer + * exists and cannot be referenced any longer. + * @param flowExecutionContext the current flow execution context (inactive) + * @param viewSelection the selected view + */ + public ResponseInstruction(FlowExecutionContext flowExecutionContext, ViewSelection viewSelection) { + init(flowExecutionContext, viewSelection); + } + + /** + * Helper to initialize the flow execution context and view selection. + */ + private void init(FlowExecutionContext flowExecutionContext, ViewSelection viewSelection) { + Assert.notNull(flowExecutionContext, "The flow execution context is required"); + Assert.notNull(viewSelection, "The view selection is required"); + this.flowExecutionContext = flowExecutionContext; + this.viewSelection = viewSelection; + } + + /** + * Returns the persistent identifier of the flow execution. + */ + public String getFlowExecutionKey() { + return flowExecutionKey; + } + + /** + * Returns the flow execution context representing the current state of the + * execution. It could be that the returned flow execution is + * {@link FlowExecutionContext#isActive() inactive}. + */ + public FlowExecutionContext getFlowExecutionContext() { + return flowExecutionContext; + } + + /** + * Returns the view selection selected by the flow execution. + */ + public ViewSelection getViewSelection() { + return viewSelection; + } + + /** + * Returns true if this is an instruction to render an application view for + * an "active" (in progress) flow execution. + */ + public boolean isActiveView() { + return isApplicationView() && flowExecutionContext.isActive(); + } + + /** + * Returns true if this is an instruction to render an application view for + * an "ended" (inactive) flow execution from an end state. + */ + public boolean isEndingView() { + return isApplicationView() && !flowExecutionContext.isActive(); + } + + // response types + + /** + * Returns true if this is an "application view" (forward) response + * instruction. + */ + public boolean isApplicationView() { + return viewSelection instanceof ApplicationView; + } + + /** + * Returns true if this is an instruction to perform a redirect to the + * current flow execution to render an application view. + */ + public boolean isFlowExecutionRedirect() { + return viewSelection instanceof FlowExecutionRedirect; + } + + /** + * Returns true if this is an instruction to launch an entirely new + * (independent) flow execution. + */ + public boolean isFlowDefinitionRedirect() { + return viewSelection instanceof FlowDefinitionRedirect; + } + + /** + * Returns true if this an instruction to perform a redirect to an external + * URL. + */ + public boolean isExternalRedirect() { + return viewSelection instanceof ExternalRedirect; + } + + /** + * Returns true if this is a "null" response instruction, e.g. + * no response needs to be rendered. + */ + public boolean isNull() { + return viewSelection == ViewSelection.NULL_VIEW; + } + + public boolean equals(Object o) { + if (!(o instanceof ResponseInstruction)) { + return false; + } + ResponseInstruction other = (ResponseInstruction)o; + if (getFlowExecutionKey() != null) { + return getFlowExecutionKey().equals(other.getFlowExecutionKey()) + && viewSelection.equals(other.viewSelection); + } + else { + return other.getFlowExecutionKey() == null && viewSelection.equals(other.viewSelection); + } + } + + public int hashCode() { + int hashCode = viewSelection.hashCode(); + if (getFlowExecutionKey() != null) { + hashCode += getFlowExecutionKey().hashCode(); + } + return hashCode; + } + + public String toString() { + return new ToStringCreator(this).append("flowExecutionKey", flowExecutionKey) + .append("viewSelection", viewSelection).append("flowExecutionContext", flowExecutionContext).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionHolder.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionHolder.java new file mode 100644 index 00000000..5a377a43 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionHolder.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import java.io.Serializable; + +import org.springframework.core.style.ToStringCreator; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.repository.FlowExecutionKey; + +/** + * A holder storing a reference to a flow execution and the key of that flow + * execution if it has been (or is about to be) managed in a repository. + * + * @author Keith Donald + */ +public class FlowExecutionHolder implements Serializable { + + /** + * The flow execution continuation key (may be null if the flow execution + * has not yet been generated a repository key). May change as well over the + * life of this object, as a flow execution can be given a new key to + * capture its state at another point in time. + */ + private FlowExecutionKey flowExecutionKey; + + /** + * The held flow execution representing the state of an ongoing conversation + * at a point in time. + */ + private FlowExecution flowExecution; + + private ViewSelection viewSelection; + + private boolean needsSave; + + /** + * Creates a new flow execution holder for a flow execution that has not yet + * been placed in a repository. + * @param flowExecution the flow execution to hold + */ + public FlowExecutionHolder(FlowExecution flowExecution) { + this.flowExecution = flowExecution; + } + + /** + * Creates a new flow execution holder. + * @param flowExecutionKey the continuation key + * @param flowExecution the flow execution to hold + */ + public FlowExecutionHolder(FlowExecutionKey flowExecutionKey, FlowExecution flowExecution) { + this.flowExecutionKey = flowExecutionKey; + this.flowExecution = flowExecution; + } + + /** + * Returns the continuation key. + */ + public FlowExecutionKey getFlowExecutionKey() { + return flowExecutionKey; + } + + /** + * Sets the continuation key. + */ + public void setFlowExecutionKey(FlowExecutionKey continuationKey) { + this.flowExecutionKey = continuationKey; + } + + /** + * Returns the flow execution. + */ + public FlowExecution getFlowExecution() { + return flowExecution; + } + + public ViewSelection getViewSelection() { + return viewSelection; + } + + public void setViewSelection(ViewSelection viewSelection) { + this.viewSelection = viewSelection; + } + + public boolean needsSave() { + return needsSave; + } + + public void markNeedsSave() { + this.needsSave = true; + } + + public String toString() { + return new ToStringCreator(this).append("flowExecutionKey", flowExecutionKey).append("flowExecution", + flowExecution).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionHolderUtils.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionHolderUtils.java new file mode 100644 index 00000000..b0303471 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowExecutionHolderUtils.java @@ -0,0 +1,62 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import javax.faces.context.ExternalContext; +import javax.faces.context.FacesContext; + +/** + * A static utility class for accessing the current flow execution holder. + *

    + * By default, the current flow execution holder is stored associated with the + * current thread in the {@link FacesContext}'s + * {@link ExternalContext#getRequestMap()}. + * + * @author Keith Donald + * @author Craig McClanahan + */ +public class FlowExecutionHolderUtils { + + /** + * Returns the current flow execution holder for the given faces context. + * @param context faces context + * @return the flow execution holder, or null if none set. + */ + public static FlowExecutionHolder getFlowExecutionHolder(FacesContext context) { + return (FlowExecutionHolder)context.getExternalContext().getRequestMap().get(getFlowExecutionHolderKey()); + } + + /** + * Sets the current flow execution holder for the given faces context. + * @param holder the flow execution holder + * @param context faces context + */ + public static void setFlowExecutionHolder(FlowExecutionHolder holder, FacesContext context) { + context.getExternalContext().getRequestMap().put(getFlowExecutionHolderKey(), holder); + } + + private static String getFlowExecutionHolderKey() { + return FlowExecutionHolder.class.getName(); + } + + public static boolean isFlowExecutionRestored(FacesContext context) { + return getFlowExecutionHolder(context) != null; + } + + public static boolean isFlowExecutionChanged(FacesContext context) { + return isFlowExecutionRestored(context) && getFlowExecutionHolder(context).needsSave(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowFacesUtils.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowFacesUtils.java new file mode 100644 index 00000000..004ac852 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowFacesUtils.java @@ -0,0 +1,88 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import javax.faces.context.FacesContext; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.web.jsf.FacesContextUtils; +import org.springframework.webflow.conversation.impl.SessionBindingConversationManager; +import org.springframework.webflow.definition.registry.FlowDefinitionLocator; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.engine.impl.FlowExecutionImplStateRestorer; +import org.springframework.webflow.execution.FlowExecutionFactory; +import org.springframework.webflow.execution.repository.FlowExecutionRepository; +import org.springframework.webflow.execution.repository.support.SimpleFlowExecutionRepository; + +/** + * Trivial helper utility class for SWF within a JSF environment. + * + * @author Keith Donald + */ +public class FlowFacesUtils { + + private static final String REPOSITORY_BEAN_NAME = "flowExecutionRepository"; + + private static final String LOCATOR_BEAN_NAME = "flowDefinitionLocator"; + + private static final String FACTORY_BEAN_NAME = "flowExecutionFactory"; + + private static SimpleFlowExecutionRepository defaultRepository; + + private static FlowExecutionImplFactory defaultFactory; + + public static FlowDefinitionLocator getDefinitionLocator(FacesContext context) { + ApplicationContext ac = FacesContextUtils.getRequiredWebApplicationContext(context); + try { + return (FlowDefinitionLocator)ac.getBean(LOCATOR_BEAN_NAME, FlowDefinitionLocator.class); + } + catch (NoSuchBeanDefinitionException e) { + String message = "No bean definition with id '" + LOCATOR_BEAN_NAME + + "' could be found; to use Spring Web Flow with JSF you must " + + "configure your context with a FlowDefinitionLocator " + + "exposing a registry of flow definitions."; + throw new JsfFlowConfigurationException(message, e); + } + } + + public synchronized static FlowExecutionRepository getExecutionRepository(FacesContext context) { + ApplicationContext ac = FacesContextUtils.getRequiredWebApplicationContext(context); + if (ac.containsBean(REPOSITORY_BEAN_NAME)) { + return (FlowExecutionRepository)ac.getBean(REPOSITORY_BEAN_NAME, FlowExecutionRepository.class); + } + else { + if (defaultRepository == null) { + defaultRepository = new SimpleFlowExecutionRepository(new FlowExecutionImplStateRestorer( + getDefinitionLocator(context)), new SessionBindingConversationManager()); + } + return defaultRepository; + } + } + + public synchronized static FlowExecutionFactory getExecutionFactory(FacesContext context) { + ApplicationContext ac = FacesContextUtils.getRequiredWebApplicationContext(context); + if (ac.containsBean(FACTORY_BEAN_NAME)) { + return (FlowExecutionFactory)ac.getBean(FACTORY_BEAN_NAME, FlowExecutionFactory.class); + } + else { + if (defaultFactory == null) { + defaultFactory = new FlowExecutionImplFactory(); + } + return defaultFactory; + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowNavigationHandler.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowNavigationHandler.java new file mode 100644 index 00000000..72c31e99 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowNavigationHandler.java @@ -0,0 +1,168 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import javax.faces.application.NavigationHandler; +import javax.faces.context.FacesContext; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.web.jsf.DecoratingNavigationHandler; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.registry.FlowDefinitionLocator; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.FlowExecutionFactory; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.executor.support.FlowExecutorArgumentExtractor; + +/** + * An implementation of a JSF NavigationHandler that provides + * integration with Spring Web Flow. It delegates handling to the standard + * NavigationHandler implementation when a navigation request does not pertain + * to a flow execution. + *

    + * Specifically, the following navigation handler algorithm is implemented: + *

      + *
    • If a flow execution is not currently in progress: + *
        + *
      • If the specified logical outcome is of the form + * flowId:xxx, look up the corresponding + * {@link org.springframework.webflow.engine.Flow} definition with that id and + * launch a new flow execution in the starting state. Expose information to + * indicate that this flow is in progress and render the starting + * {@link ViewSelection}.
      • + *
      • If the specified logical outcome is not of the form + * flowId:xxx, simply delegate to the standard + * NavigationHandler implementation and return.
      • + *
      + *
    • + *
    • If a flow execution is currently in progress: + *
        + *
      • Load the reference to the current in-progress flow execution using the + * submitted _flowExecutionKey parameter.
      • + *
      • Resume the flow execution by signaling what action outcome (aka event) + * the user took in the current state. + *
      • Once state event processing to complete, render the + * ViewSelection returned.
      • + *
      + *
    • + *
    + * + * @author Craig McClanahan + * @author Colin Sampaleanu + * @author Keith Donald + */ +public class FlowNavigationHandler extends DecoratingNavigationHandler { + + /** + * Logger, usable by subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * A helper for extracting parameters needed by this flow navigation + * handler. + */ + private FlowExecutorArgumentExtractor argumentExtractor = new FlowNavigationHandlerArgumentExtractor(); + + /** + * Create a new {@link FlowNavigationHandler} using the default constructor. + */ + public FlowNavigationHandler() { + super(); + } + + /** + * Create a new {@link FlowNavigationHandler}, wrapping the specified + * standard navigation handler implementation. + * @param originalNavigationHandler Standard NavigationHandler + * we are wrapping + */ + public FlowNavigationHandler(NavigationHandler originalNavigationHandler) { + super(originalNavigationHandler); + } + + /** + * Returns the argument extractor used by this navigation handler. + */ + public FlowExecutorArgumentExtractor getArgumentExtractor() { + return argumentExtractor; + } + + /** + * Sets the argument extractor to use. + */ + public void setArgumentExtractor(FlowExecutorArgumentExtractor argumentExtractor) { + this.argumentExtractor = argumentExtractor; + } + + public void handleNavigation(FacesContext facesContext, String fromAction, String outcome, + NavigationHandler originalNavigationHandler) { + JsfExternalContext context = new JsfExternalContext(facesContext, fromAction, outcome); + if (FlowExecutionHolderUtils.isFlowExecutionRestored(facesContext)) { + // the flow execution has been restored, now see if we need to + // signal an event against it + if (argumentExtractor.isEventIdPresent(context)) { + // a flow execution has been restored, signal an event in it + String eventId = argumentExtractor.extractEventId(context); + FlowExecutionHolder holder = FlowExecutionHolderUtils.getFlowExecutionHolder(facesContext); + ViewSelection selectedView = holder.getFlowExecution().signalEvent(eventId, context); + holder.setViewSelection(selectedView); + holder.markNeedsSave(); + } + } + else { + // no flow execution exists, see if we need to launch one if the + // flow id is present + if (argumentExtractor.isFlowIdPresent(context)) { + // a flow execution launch has been requested, start it + String flowId = argumentExtractor.extractFlowId(context); + FlowDefinition flowDefinition = getLocator(context).getFlowDefinition(flowId); + FlowExecution flowExecution = getFactory(context).createFlowExecution(flowDefinition); + FlowExecutionHolder holder = new FlowExecutionHolder(flowExecution); + FlowExecutionHolderUtils.setFlowExecutionHolder(holder, facesContext); + ViewSelection selectedView = flowExecution.start(createInput(flowExecution, context), context); + holder.setViewSelection(selectedView); + holder.markNeedsSave(); + } + else { + // no flow id submitted, proceed with std navigation + originalNavigationHandler.handleNavigation(facesContext, fromAction, outcome); + } + } + } + + private FlowDefinitionLocator getLocator(JsfExternalContext context) { + return FlowFacesUtils.getDefinitionLocator(context.getFacesContext()); + } + + private FlowExecutionFactory getFactory(JsfExternalContext context) { + return FlowFacesUtils.getExecutionFactory(context.getFacesContext()); + } + + /** + * Factory method that creates the input attribute map for a newly created + * {@link FlowExecution}. TODO - add support for input mappings here + * @param flowExecution the new flow execution (yet to be started) + * @param context the external context + * @return the input map + */ + protected MutableAttributeMap createInput(FlowExecution flowExecution, ExternalContext context) { + return null; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowNavigationHandlerArgumentExtractor.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowNavigationHandlerArgumentExtractor.java new file mode 100644 index 00000000..0cd1b445 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowNavigationHandlerArgumentExtractor.java @@ -0,0 +1,109 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import org.springframework.util.StringUtils; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.executor.support.FlowExecutorArgumentExtractionException; +import org.springframework.webflow.executor.support.FlowExecutorArgumentExtractor; +import org.springframework.webflow.executor.support.RequestParameterFlowExecutorArgumentHandler; + +/** + * An {@link FlowExecutorArgumentExtractor} that is aware of JSF + * outcomes that communicate requests to launch flow executions and + * signal event in existing flow executions. + * + * @author Keith Donald + */ +public class FlowNavigationHandlerArgumentExtractor extends RequestParameterFlowExecutorArgumentHandler { + + /* + * Implementation note: subclasses an FlowExecutorArgumentHandler but is really + * just a FlowExecutorArgumentExtractor. + */ + + /** + * The default prefix of a outcome string that indicates a new flow should be launched. + */ + private static final String FLOW_ID_PREFIX = "flowId:"; + + private String flowIdPrefix = FLOW_ID_PREFIX; + + /** + * Returns the configured prefix for outcome strings that indicate a new flow should be launched. + */ + public String getFlowIdPrefix() { + return flowIdPrefix; + } + + /** + * Sets the prefix of a outcome string that indicates a new flow should be launched. + */ + public void setFlowIdPrefix(String flowIdPrefix) { + this.flowIdPrefix = flowIdPrefix; + } + + public boolean isEventIdPresent(ExternalContext context) { + return StringUtils.hasText(getOutcome(context)) || super.isEventIdPresent(context); + } + + // overidden to return the eventId from the action outcome string. + public String extractEventId(ExternalContext context) throws FlowExecutorArgumentExtractionException { + String outcome = getOutcome(context); + if (StringUtils.hasText(outcome)) { + return outcome; + } + else { + return super.extractEventId(context); + } + } + + public boolean isFlowIdPresent(ExternalContext context) throws FlowExecutorArgumentExtractionException { + String outcome = getOutcome(context); + if (outcome != null && outcome.startsWith(getFlowIdPrefix())) { + return true; + } + else { + return super.isFlowIdPresent(context); + } + } + + // overidden to return the flowId from a JSF outcome in format flowId:${flowId} + public String extractFlowId(ExternalContext context) throws FlowExecutorArgumentExtractionException { + String outcome = getOutcome(context); + if (StringUtils.hasText(outcome)) { + int index = outcome.indexOf(getFlowIdPrefix()); + if (index == -1) { + throw new FlowExecutorArgumentExtractionException( + "Unable to extract flow id; make sure the JSF outcome is prefixed with '" + getFlowIdPrefix() + + "' to launch a new flow execution"); + } + String flowId = outcome.substring(getFlowIdPrefix().length()); + if (!StringUtils.hasText(flowId)) { + throw new FlowExecutorArgumentExtractionException( + "Unable to extract flow id; make sure the flow id is provided in the outcome string"); + } + return flowId; + } + else { + return super.extractFlowId(context); + } + } + + private String getOutcome(ExternalContext context) { + return ((JsfExternalContext)context).getOutcome(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowPhaseListener.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowPhaseListener.java new file mode 100644 index 00000000..eead27e7 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowPhaseListener.java @@ -0,0 +1,353 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import java.io.IOException; +import java.util.Iterator; +import java.util.Map; + +import javax.faces.application.ViewHandler; +import javax.faces.component.UIViewRoot; +import javax.faces.context.FacesContext; +import javax.faces.event.PhaseEvent; +import javax.faces.event.PhaseId; +import javax.faces.event.PhaseListener; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.context.ExternalContextHolder; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.registry.FlowDefinitionLocator; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.FlowExecutionFactory; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.repository.FlowExecutionKey; +import org.springframework.webflow.execution.repository.FlowExecutionRepository; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.execution.support.ExternalRedirect; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; +import org.springframework.webflow.execution.support.FlowExecutionRedirect; +import org.springframework.webflow.executor.support.FlowExecutorArgumentHandler; +import org.springframework.webflow.executor.support.RequestParameterFlowExecutorArgumentHandler; + +/** + * JSF phase listener that is responsible for managing a {@link FlowExecution} + * object representing an active user conversation so that other JSF artifacts + * that execute in different phases of the JSF lifecycle may have access to it. + *

    + * This phase listener implements the following algorithm: + *

      + *
    • On BEFORE_RESTORE_VIEW, restore the {@link FlowExecution} the user is + * participating in if a call to + * {@link FlowExecutorArgumentHandler#extractFlowExecutionKey(ExternalContext)} + * returns a submitted flow execution identifier. Place the restored flow + * execution in a holder that other JSF artifacts such as VariableResolvers, + * PropertyResolvers, and NavigationHandlers may access during the request + * lifecycle. + *
    • On BEFORE_RENDER_RESPONSE, if a flow execution was restored in the + * RESTORE_VIEW phase generate a new key for identifying the updated execution + * within a the selected {@link FlowExecutionRepository}. Expose managed flow + * execution attributes to the views before rendering. + *
    • On AFTER_RENDER_RESPONSE, if a flow execution was restored in the + * RESTORE_VIEW phase save the updated execution to the repository + * using the new key generated in the BEFORE_RENDER_RESPONSE phase. + *
    + * + * @author Colin Sampaleanu + * @author Keith Donald + */ +public class FlowPhaseListener implements PhaseListener { + + /** + * Logger, usable by subclasses. + */ + protected final Log logger = LogFactory.getLog(getClass()); + + /** + * A helper for handling arguments needed by this phase listener. + */ + private FlowExecutorArgumentHandler argumentHandler = new RequestParameterFlowExecutorArgumentHandler(); + + /** + * Resolves selected Web Flow view names to JSF view ids. + */ + private ViewIdMapper viewIdMapper = new DefaultViewIdMapper(); + + /** + * Returns the argument handler used by this phase listener. + */ + public FlowExecutorArgumentHandler getArgumentHandler() { + return argumentHandler; + } + + /** + * Sets the argument handler to use. + */ + public void setArgumentHandler(FlowExecutorArgumentHandler argumentHandler) { + this.argumentHandler = argumentHandler; + } + + /** + * Returns the JSF view id resolver used by this phase listener. + */ + public ViewIdMapper getViewIdMapper() { + return viewIdMapper; + } + + /** + * Sets the JSF view id mapper used by this phase listener. + */ + public void setViewIdMapper(ViewIdMapper viewIdMapper) { + this.viewIdMapper = viewIdMapper; + } + + public PhaseId getPhaseId() { + return PhaseId.ANY_PHASE; + } + + public void beforePhase(PhaseEvent event) { + if (event.getPhaseId() == PhaseId.RESTORE_VIEW) { + ExternalContextHolder.setExternalContext(new JsfExternalContext(event.getFacesContext())); + restoreFlowExecution(event.getFacesContext()); + } + else if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) { + if (FlowExecutionHolderUtils.isFlowExecutionRestored(event.getFacesContext())) { + prepareResponse(getCurrentContext(), FlowExecutionHolderUtils.getFlowExecutionHolder(event + .getFacesContext())); + } + } + } + + public void afterPhase(PhaseEvent event) { + if (event.getPhaseId() == PhaseId.RENDER_RESPONSE) { + try { + if (FlowExecutionHolderUtils.isFlowExecutionChanged(event.getFacesContext())) { + saveFlowExecution(getCurrentContext(), FlowExecutionHolderUtils.getFlowExecutionHolder(event + .getFacesContext())); + } + } + finally { + ExternalContextHolder.setExternalContext(null); + } + } + } + + private JsfExternalContext getCurrentContext() { + return (JsfExternalContext) ExternalContextHolder.getExternalContext(); + } + + protected void restoreFlowExecution(FacesContext facesContext) { + JsfExternalContext context = new JsfExternalContext(facesContext); + if (argumentHandler.isFlowExecutionKeyPresent(context)) { + // restore flow execution from repository so it will be + // available to variable/property resolvers and the flow + // navigation handler (this could happen as part of a submission or + // flow execution redirect) + FlowExecutionRepository repository = getRepository(context); + FlowExecutionKey flowExecutionKey = repository.parseFlowExecutionKey(argumentHandler + .extractFlowExecutionKey(context)); + FlowExecution flowExecution = repository.getFlowExecution(flowExecutionKey); + if (logger.isDebugEnabled()) { + logger.debug("Loaded existing flow execution from repository with id '" + flowExecutionKey + "'"); + } + FlowExecutionHolderUtils.setFlowExecutionHolder(new FlowExecutionHolder(flowExecutionKey, flowExecution), + facesContext); + } + else if (argumentHandler.isFlowIdPresent(context)) { + // launch a new flow execution (this could happen as part of a flow + // redirect) + String flowId = argumentHandler.extractFlowId(context); + FlowDefinition flowDefinition = getLocator(context).getFlowDefinition(flowId); + FlowExecution flowExecution = getFactory(context).createFlowExecution(flowDefinition); + FlowExecutionHolder holder = new FlowExecutionHolder(flowExecution); + FlowExecutionHolderUtils.setFlowExecutionHolder(holder, facesContext); + ViewSelection selectedView = flowExecution.start(createInput(flowExecution, context), context); + if (logger.isDebugEnabled()) { + logger.debug("Started new flow execution"); + } + holder.setViewSelection(selectedView); + holder.markNeedsSave(); + } + } + + /** + * Factory method that creates the input attribute map for a newly created + * {@link FlowExecution}. TODO - add support for input mappings here + * @param flowExecution the new flow execution (yet to be started) + * @param context the external context + * @return the input map + */ + protected LocalAttributeMap createInput(FlowExecution flowExecution, ExternalContext context) { + return null; + } + + protected void prepareResponse(JsfExternalContext context, FlowExecutionHolder holder) { + if (holder.needsSave()) { + generateKey(context, holder); + } + ViewSelection selectedView = holder.getViewSelection(); + if (selectedView == null) { + selectedView = holder.getFlowExecution().refresh(context); + holder.setViewSelection(selectedView); + } + if (selectedView instanceof ApplicationView) { + prepareApplicationView(context.getFacesContext(), holder); + } + else if (selectedView instanceof FlowExecutionRedirect) { + String url = argumentHandler.createFlowExecutionUrl(holder.getFlowExecutionKey().toString(), holder + .getFlowExecution(), context); + sendRedirect(url, context); + } + else if (selectedView instanceof ExternalRedirect) { + String flowExecutionKey = holder.getFlowExecution().isActive() ? holder.getFlowExecutionKey().toString() + : null; + String url = argumentHandler.createExternalUrl((ExternalRedirect) holder.getViewSelection(), + flowExecutionKey, context); + sendRedirect(url, context); + } + else if (selectedView instanceof FlowDefinitionRedirect) { + String url = argumentHandler.createFlowDefinitionUrl((FlowDefinitionRedirect) holder.getViewSelection(), + context); + sendRedirect(url, context); + } + } + + protected void prepareApplicationView(FacesContext facesContext, FlowExecutionHolder holder) { + ApplicationView forward = (ApplicationView) holder.getViewSelection(); + if (forward != null) { + putInto(facesContext.getExternalContext().getRequestMap(), forward.getModel()); + updateViewRoot(facesContext, viewIdMapper.mapViewId(forward.getViewName())); + } + Map requestMap = facesContext.getExternalContext().getRequestMap(); + argumentHandler.exposeFlowExecutionContext(holder.getFlowExecutionKey().toString(), holder.getFlowExecution(), + requestMap); + } + + private void updateViewRoot(FacesContext facesContext, String viewId) { + UIViewRoot viewRoot = facesContext.getViewRoot(); + if (viewRoot == null || hasViewChanged(viewRoot, viewId)) { + // create the specified view so that it can be rendered + if (logger.isDebugEnabled()) { + logger.debug("Creating new view with id '" + viewId + "' from previous view with id '" + + viewRoot.getViewId() + "'"); + } + ViewHandler handler = facesContext.getApplication().getViewHandler(); + UIViewRoot view = handler.createView(facesContext, viewId); + facesContext.setViewRoot(view); + } + } + + private boolean hasViewChanged(UIViewRoot viewRoot, String viewId) { + return !viewRoot.getViewId().equals(viewId); + } + + private void generateKey(JsfExternalContext context, FlowExecutionHolder holder) { + FlowExecution flowExecution = holder.getFlowExecution(); + if (flowExecution.isActive()) { + // generate new continuation key for the flow execution + // before rendering the response + FlowExecutionKey flowExecutionKey = holder.getFlowExecutionKey(); + FlowExecutionRepository repository = getRepository(context); + if (flowExecutionKey == null) { + // it is an new conversation, generate a brand new key + flowExecutionKey = repository.generateKey(flowExecution); + } + else { + // it is an existing conversaiton, use same conversation id, + // generate a new continuation id + flowExecutionKey = repository.getNextKey(flowExecution, flowExecutionKey); + } + holder.setFlowExecutionKey(flowExecutionKey); + } + } + + protected void saveFlowExecution(JsfExternalContext context, FlowExecutionHolder holder) { + FlowExecution flowExecution = holder.getFlowExecution(); + FlowExecutionRepository repository = getRepository(context); + if (flowExecution.isActive()) { + // save the flow execution out to the repository + if (logger.isDebugEnabled()) { + logger.debug("Saving continuation to repository with key " + holder.getFlowExecutionKey()); + } + repository.putFlowExecution(holder.getFlowExecutionKey(), flowExecution); + } + else { + if (holder.getFlowExecutionKey() != null) { + // remove the flow execution from the repository + if (logger.isDebugEnabled()) { + logger.debug("Removing execution in repository with key '" + holder.getFlowExecutionKey() + "'"); + } + repository.removeFlowExecution(holder.getFlowExecutionKey()); + } + } + } + + /** + * Utility method needed needed only because we can not rely on JSF + * RequestMap supporting Map's putAll method. Tries putAll, falls back to + * individual adds + * @param targetMap the target map to add the model data to + * @param map the model data to add to the target map + */ + private void putInto(Map targetMap, Map map) { + try { + targetMap.putAll(map); + } + catch (UnsupportedOperationException e) { + // work around nasty MyFaces bug where it's RequestMap doesn't + // support putAll remove after it's fixed in MyFaces + Iterator it = map.entrySet().iterator(); + while (it.hasNext()) { + Map.Entry entry = (Map.Entry) it.next(); + targetMap.put(entry.getKey(), entry.getValue()); + } + } + } + + private void sendRedirect(String url, JsfExternalContext context) { + try { + context.getFacesContext().getExternalContext().redirect(url); + context.getFacesContext().responseComplete(); + } + catch (IOException e) { + throw new IllegalArgumentException("Could not send redirect to " + url); + } + } + + /** + * Standard default view id resolver which uses the web flow view name as + * the jsf view id + */ + public static class DefaultViewIdMapper implements ViewIdMapper { + public String mapViewId(String viewName) { + return viewName; + } + } + + private FlowDefinitionLocator getLocator(JsfExternalContext context) { + return FlowFacesUtils.getDefinitionLocator(context.getFacesContext()); + } + + private FlowExecutionFactory getFactory(JsfExternalContext context) { + return FlowFacesUtils.getExecutionFactory(context.getFacesContext()); + } + + private FlowExecutionRepository getRepository(JsfExternalContext context) { + return FlowFacesUtils.getExecutionRepository(context.getFacesContext()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowPropertyResolver.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowPropertyResolver.java new file mode 100644 index 00000000..c7d8b23b --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowPropertyResolver.java @@ -0,0 +1,172 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import javax.faces.context.FacesContext; +import javax.faces.el.EvaluationException; +import javax.faces.el.PropertyNotFoundException; +import javax.faces.el.PropertyResolver; +import javax.faces.el.ReferenceSyntaxException; + +import org.springframework.util.Assert; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.jsf.FacesContextUtils; +import org.springframework.webflow.execution.FlowExecution; + +/** + * Custom property resolve that resolves active flow session scope properties + * from a thread bound flow execution. + *

    + * TODO: this class probably needs to delegate to a strategy object pulled out + * of the appcontext, to provide ability to override and configure, as JSF + * provides no other way to customize and configure this instance. + * + * @author Colin Sampaleanu + */ +public class FlowPropertyResolver extends PropertyResolver { + + /** + * The standard property resolver to delegate to if this one doesn't apply. + */ + private final PropertyResolver resolverDelegate; + + /** + * Create a new PropertyResolver, using the given original PropertyResolver. + *

    + * A JSF implementation will automatically pass its original resolver into + * the constructor of a configured resolver, provided that there is a + * corresponding constructor argument. + * + * @param resolverDelegate the original VariableResolver + */ + public FlowPropertyResolver(PropertyResolver resolverDelegate) { + this.resolverDelegate = resolverDelegate; + } + + public Class getType(Object base, int index) throws EvaluationException, PropertyNotFoundException { + if (!(base instanceof FlowExecution)) { + return resolverDelegate.getType(base, index); + } + else { + // can't access flow scope by index, so can't determine type. Return + // null per JSF spec + return null; + } + } + + public Class getType(Object base, Object property) throws EvaluationException, PropertyNotFoundException { + if (!(base instanceof FlowExecution)) { + return resolverDelegate.getType(base, property); + } + if (property == null) { + throw new PropertyNotFoundException("Unable to get value from Flow, as property (key) is null"); + } + if (!(property instanceof String)) { + throw new PropertyNotFoundException("Unable to get value from Flow map, as key is non-String"); + } + FlowExecution execution = (FlowExecution)base; + // we want to access flow scope of the active session (conversation) + Object value = execution.getActiveSession().getScope().get((String)property); + // note that MyFaces returns Object.class for a null value here, but + // as I read the JSF spec, null should be returned when the object + // type can not be determined this certainly seems to be the case + // for a map value which doesn' even exist + return (value == null) ? null : value.getClass(); + } + + public Object getValue(Object base, int index) throws EvaluationException, PropertyNotFoundException { + if (!(base instanceof FlowExecution)) { + return resolverDelegate.getValue(base, index); + } + else { + throw new ReferenceSyntaxException("Cannot apply an index value to Flow map"); + } + } + + public Object getValue(Object base, Object property) throws EvaluationException, PropertyNotFoundException { + if (!(base instanceof FlowExecution)) { + return resolverDelegate.getValue(base, property); + } + if (!(property instanceof String)) { + throw new PropertyNotFoundException("Unable to get value from Flow map, as key is non-String"); + } + FlowExecution execution = (FlowExecution)base; + String attributeName = (String)property; + Object value = execution.getActiveSession().getScope().get(attributeName); + if (value == null) { + FacesContext context = FacesContext.getCurrentInstance(); + Assert.notNull(context, "FacesContext must exist during property resolution stage"); + WebApplicationContext wac = getWebApplicationContext(context); + if (wac.containsBean(attributeName)) { + // note: this resolver doesn't care, but this should normally be + // either a stateless singleton bean, or a stateful/stateless + // prototype + value = wac.getBean(attributeName); + execution.getActiveSession().getScope().put(attributeName, value); + } + } + return value; + } + + public boolean isReadOnly(Object base, int index) throws EvaluationException, PropertyNotFoundException { + if (!(base instanceof FlowExecution)) { + return resolverDelegate.isReadOnly(base, index); + } + return false; + } + + public boolean isReadOnly(Object base, Object property) throws EvaluationException, PropertyNotFoundException { + if (!(base instanceof FlowExecution)) { + return resolverDelegate.isReadOnly(base, property); + } + return false; + } + + public void setValue(Object base, int index, Object value) throws EvaluationException, PropertyNotFoundException { + if (!(base instanceof FlowExecution)) { + resolverDelegate.setValue(base, index, value); + } + throw new ReferenceSyntaxException("Can not apply an index value to Flow map"); + } + + public void setValue(Object base, Object property, Object value) throws EvaluationException, + PropertyNotFoundException { + if (!(base instanceof FlowExecution)) { + resolverDelegate.setValue(base, property, value); + return; + } + if (property == null || !(property instanceof String) + || (property instanceof String && ((String)property).length() == 0)) { + throw new PropertyNotFoundException( + "Attempt to set Flow attribute with null name, empty name, or non-String name"); + } + FlowExecution execution = (FlowExecution)base; + execution.getActiveSession().getScope().put((String)property, value); + } + + /** + * Retrieve the web application context to delegate bean name resolution to. + *

    + * Default implementation delegates to FacesContextUtils. + * + * @param facesContext the current JSF context + * @return the Spring web application context (never null) + * @see FacesContextUtils#getRequiredWebApplicationContext + */ + protected WebApplicationContext getWebApplicationContext(FacesContext facesContext) { + return FacesContextUtils.getRequiredWebApplicationContext(facesContext); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowVariableResolver.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowVariableResolver.java new file mode 100644 index 00000000..61d2faa1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/FlowVariableResolver.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import javax.faces.context.FacesContext; +import javax.faces.el.EvaluationException; +import javax.faces.el.VariableResolver; + +/** + * Custom variable resolver that resolves to a thread-bound FlowExecution object + * for binding expressions prefixed with {@link #FLOW_SCOPE_VARIABLE}. For instance + * "flowScope.myBean.myProperty". + * + * @author Colin Sampaleanu + */ +public class FlowVariableResolver extends VariableResolver { + + /** + * Name of the exposed flow scope variable ("flowScope"). + */ + public static final String FLOW_SCOPE_VARIABLE = "flowScope"; + + /** + * The standard variable resolver to delegate to if this one doesn't apply. + */ + private VariableResolver resolverDelegate; + + /** + * Create a new FlowVariableResolver, using the given original + * VariableResolver. + *

    + * A JSF implementation will automatically pass its original resolver into + * the constructor of a configured resolver, provided that there is a + * corresponding constructor argument. + * + * @param resolverDelegate the original VariableResolver + */ + public FlowVariableResolver(VariableResolver resolverDelegate) { + this.resolverDelegate = resolverDelegate; + } + + /** + * Return the original VariableResolver that this resolver delegates to. + */ + protected final VariableResolver getResolverDelegate() { + return resolverDelegate; + } + + /** + * Check for the special "flow" variable first, then delegate to the + * original VariableResolver. + */ + public Object resolveVariable(FacesContext context, String name) throws EvaluationException { + if (!FLOW_SCOPE_VARIABLE.equals(name)) { + return resolverDelegate.resolveVariable(context, name); + } + else { + FlowExecutionHolder holder = FlowExecutionHolderUtils.getFlowExecutionHolder(context); + if (holder == null) + throw new EvaluationException( + "'flowScope' variable prefix specified, but a FlowExecution is not bound to current thread context as it should be"); + return holder.getFlowExecution(); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/JsfExternalContext.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/JsfExternalContext.java new file mode 100644 index 00000000..c9bd295c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/JsfExternalContext.java @@ -0,0 +1,158 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import javax.faces.context.FacesContext; + +import org.springframework.binding.collection.SharedMapDecorator; +import org.springframework.core.style.ToStringCreator; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.LocalParameterMap; +import org.springframework.webflow.core.collection.LocalSharedAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.core.collection.ParameterMap; +import org.springframework.webflow.core.collection.SharedAttributeMap; + +/** + * Provides contextual information about a JSF environment that has interacted + * with SWF. + * + * @author Keith Donald + */ +public class JsfExternalContext implements ExternalContext { + + /** + * The JSF Faces context. + */ + private FacesContext facesContext; + + /** + * The id of the action or "command button" that fired. + */ + private String actionId; + + /** + * The action outcome. + */ + private String outcome; + + /** + * Creates a JSF External Context. + * @param facesContext the JSF faces context + */ + public JsfExternalContext(FacesContext facesContext) { + this.facesContext = facesContext; + } + + /** + * Creates a JSF External Context. + * @param facesContext the JSF faces context. + * @param actionId the action that fired + * @param outcome the action outcome + */ + public JsfExternalContext(FacesContext facesContext, String actionId, String outcome) { + this.facesContext = facesContext; + this.actionId = actionId; + this.outcome = outcome; + } + + public String getContextPath() { + return facesContext.getExternalContext().getRequestContextPath(); + } + + public String getDispatcherPath() { + return facesContext.getExternalContext().getRequestServletPath(); + } + + public String getRequestPathInfo() { + return facesContext.getExternalContext().getRequestPathInfo(); + } + + public ParameterMap getRequestParameterMap() { + return new LocalParameterMap(facesContext.getExternalContext().getRequestParameterMap()); + } + + public MutableAttributeMap getRequestMap() { + return new LocalAttributeMap(facesContext.getExternalContext().getRequestMap()); + } + + public SharedAttributeMap getSessionMap() { + return new LocalSharedAttributeMap(new SessionSharedMap(facesContext)); + } + + public SharedAttributeMap getGlobalSessionMap() { + return getSessionMap(); + } + + public SharedAttributeMap getApplicationMap() { + return new LocalSharedAttributeMap(new ApplicationSharedMap(facesContext)); + } + + /** + * Returns the JSF FacesContext. + */ + public FacesContext getFacesContext() { + return facesContext; + } + + /** + * Returns the action identifier. + */ + public String getActionId() { + return actionId; + } + + /** + * Returns the action outcome. + */ + public String getOutcome() { + return outcome; + } + + private static class SessionSharedMap extends SharedMapDecorator { + + private FacesContext facesContext; + + public SessionSharedMap(FacesContext facesContext) { + super(facesContext.getExternalContext().getSessionMap()); + this.facesContext = facesContext; + } + + public Object getMutex() { + return facesContext.getExternalContext().getSession(false); + } + } + + private static class ApplicationSharedMap extends SharedMapDecorator { + + private FacesContext facesContext; + + public ApplicationSharedMap(FacesContext facesContext) { + super(facesContext.getExternalContext().getApplicationMap()); + this.facesContext = facesContext; + } + + public Object getMutex() { + return facesContext.getExternalContext().getContext(); + } + } + + public String toString() { + return new ToStringCreator(this).append("actionId", actionId).append("outcome", outcome).append("facesContext", + facesContext).toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/JsfFlowConfigurationException.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/JsfFlowConfigurationException.java new file mode 100644 index 00000000..8d3f337d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/JsfFlowConfigurationException.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import org.springframework.webflow.core.FlowException; + +/** + * Thrown when there is a configuration error with SWF within a JSF environment. + * + * @author Keith Donald + */ +public class JsfFlowConfigurationException extends FlowException { + + public JsfFlowConfigurationException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/ViewIdMapper.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/ViewIdMapper.java new file mode 100644 index 00000000..e9c55e22 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/ViewIdMapper.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +/** + * Interface to be implemented by objects that can map Web Flow view names to + * JSF view identifiers. + * + * @author Colin Sampaleanu + */ +public interface ViewIdMapper { + + /** + * Map the given Web Flow view name to a JSF view id. + * @param viewName name of the view to map + * @return the corresponding JSF view id + */ + public String mapViewId(String viewName); + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/package.html b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/package.html new file mode 100644 index 00000000..72e0acd3 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/jsf/package.html @@ -0,0 +1,5 @@ + + +

    The integration layer between Spring Web Flow and Java Server Faces (JSF).

    + + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/mvc/FlowController.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/mvc/FlowController.java new file mode 100644 index 00000000..b255227a --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/mvc/FlowController.java @@ -0,0 +1,226 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.mvc; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.mvc.AbstractController; +import org.springframework.web.servlet.mvc.Controller; +import org.springframework.web.servlet.view.RedirectView; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.context.servlet.ServletExternalContext; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.execution.support.ExternalRedirect; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; +import org.springframework.webflow.executor.FlowExecutor; +import org.springframework.webflow.executor.ResponseInstruction; +import org.springframework.webflow.executor.support.FlowExecutorArgumentHandler; +import org.springframework.webflow.executor.support.FlowRequestHandler; +import org.springframework.webflow.executor.support.RequestParameterFlowExecutorArgumentHandler; +import org.springframework.webflow.executor.support.RequestPathFlowExecutorArgumentHandler; + +/** + * Point of integration between Spring Web MVC and Spring Web Flow: a + * {@link Controller} that routes incoming requests to one or more managed flow + * executions. + *

    + * Requests into the web flow system are handled by a {@link FlowExecutor}, + * which this class delegates to using a {@link FlowRequestHandler} helper. + * Consult the JavaDoc of that class for more information on how requests are + * processed. + *

    + * Note: a single FlowController may execute all flows of your application. + *

      + *
    • By default, to have this controller launch a new flow execution + * (conversation), have the client send a + * {@link FlowExecutorArgumentHandler#getFlowIdArgumentName()} request + * parameter indicating the flow definition to launch. + *
    • To have this controller participate in an existing flow execution + * (conversation), have the client send a + * {@link FlowExecutorArgumentHandler#getFlowExecutionKeyArgumentName()} + * request parameter identifying the conversation to participate in. + * See the flow-launcher sample application for examples of the + * various strategies for launching and resuming flow executions. + *
    + *

    + * Usage example: + *

    + *     <!--
    + *         Exposes flows for execution at a single request URL.
    + *         The id of a flow to launch should be passed in by clients using
    + *         the "_flowId" request parameter:
    + *         e.g. /app.htm?_flowId=flow1
    + *     -->
    + *     <bean name="/app.htm" class="org.springframework.webflow.executor.mvc.FlowController">
    + *         <property name="flowExecutor" ref="flowExecutor"/>
    + *     </bean>
    + * 
    + *

    + * It is also possible to customize the {@link FlowExecutorArgumentHandler} + * strategy to allow for different types of controller parameterization, for + * example perhaps in conjunction with a REST-style request mapper (see + * {@link RequestPathFlowExecutorArgumentHandler}). + * + * @see org.springframework.webflow.executor.FlowExecutor + * @see org.springframework.webflow.executor.support.FlowRequestHandler + * @see org.springframework.webflow.executor.support.FlowExecutorArgumentHandler + * + * @author Erwin Vervaet + * @author Keith Donald + */ +public class FlowController extends AbstractController implements InitializingBean { + + /** + * The facade for executing flows (launching new executions, and resuming + * existing executions). + */ + private FlowExecutor flowExecutor; + + /** + * The strategy for handling flow executor parameters. + */ + private FlowExecutorArgumentHandler argumentHandler = new RequestParameterFlowExecutorArgumentHandler(); + + /** + * Create a new flow controller. Allows bean style usage. + * @see #setFlowExecutor(FlowExecutor) + * @see #setArgumentHandler(FlowExecutorArgumentHandler) + */ + public FlowController() { + // set the cache seconds property to 0 so no pages are cached by default + // for flows. + setCacheSeconds(0); + } + + /** + * Returns the flow executor used by this controller. + * @return the flow executor + */ + public FlowExecutor getFlowExecutor() { + return flowExecutor; + } + + /** + * Sets the flow executor to use; setting this property is required. + * @param flowExecutor the fully configured flow executor to use + */ + public void setFlowExecutor(FlowExecutor flowExecutor) { + this.flowExecutor = flowExecutor; + } + + /** + * Returns the flow executor argument handler used by this controller. + * Defaults to {@link RequestParameterFlowExecutorArgumentHandler}. + * @return the argument handler + */ + public FlowExecutorArgumentHandler getArgumentHandler() { + return argumentHandler; + } + + /** + * Sets the flow executor argument handler to use. The default is + * {@link RequestParameterFlowExecutorArgumentHandler}. + * @param argumentHandler the fully configured argument handler + */ + public void setArgumentHandler(FlowExecutorArgumentHandler argumentHandler) { + this.argumentHandler = argumentHandler; + } + + /** + * Sets the identifier of the default flow to launch if no flowId argument + * can be extracted by the configured {@link FlowExecutorArgumentHandler} + * during request processing. + *

    + * This is a convenience method that sets the default flow id of the + * controller's argument handler. Don't use this when using + * {@link #setArgumentHandler(FlowExecutorArgumentHandler)}. + */ + public void setDefaultFlowId(String defaultFlowId) { + this.argumentHandler.setDefaultFlowId(defaultFlowId); + } + + public void afterPropertiesSet() { + Assert.notNull(flowExecutor, "The flow executor property is required"); + Assert.notNull(argumentHandler, "The argument handler property is required"); + } + + protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) + throws Exception { + ServletExternalContext context = new ServletExternalContext(getServletContext(), request, response); + ResponseInstruction responseInstruction = createRequestHandler().handleFlowRequest(context); + return toModelAndView(responseInstruction, context); + } + + /** + * Factory method that creates a new helper for processing a request into + * this flow controller. The handler is a basic template encapsulating + * reusable flow execution request handling workflow. + * This implementation just creates a new {@link FlowRequestHandler}. + * @return the controller helper + */ + protected FlowRequestHandler createRequestHandler() { + return new FlowRequestHandler(getFlowExecutor(), getArgumentHandler()); + } + + /** + * Create a ModelAndView object based on the information in the selected + * response instruction. Subclasses can override this to return a + * specialized ModelAndView or to do custom processing on it. + * @param response instruction the response instruction to convert + * @return a new ModelAndView object + */ + protected ModelAndView toModelAndView(ResponseInstruction response, ExternalContext context) { + if (response.isApplicationView()) { + // forward to a view as part of an active conversation + ApplicationView view = (ApplicationView)response.getViewSelection(); + Map model = new HashMap(view.getModel()); + argumentHandler.exposeFlowExecutionContext( + response.getFlowExecutionKey(), response.getFlowExecutionContext(), model); + return new ModelAndView(view.getViewName(), model); + } + else if (response.isFlowDefinitionRedirect()) { + // restart the flow by redirecting to flow launch URL + String flowUrl = argumentHandler.createFlowDefinitionUrl((FlowDefinitionRedirect)response.getViewSelection(), context); + return new ModelAndView(new RedirectView(flowUrl)); + } + else if (response.isFlowExecutionRedirect()) { + // redirect to active flow execution URL + String flowExecutionUrl = argumentHandler.createFlowExecutionUrl( + response.getFlowExecutionKey(), response.getFlowExecutionContext(), context); + return new ModelAndView(new RedirectView(flowExecutionUrl)); + } + else if (response.isExternalRedirect()) { + // redirect to external URL + ExternalRedirect redirect = (ExternalRedirect)response.getViewSelection(); + String externalUrl = argumentHandler.createExternalUrl(redirect, response.getFlowExecutionKey(), context); + return new ModelAndView(new RedirectView(externalUrl)); + } + else if (response.isNull()) { + // no response to issue + return null; + } + else { + throw new IllegalArgumentException("Don't know how to handle response instruction " + response); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/mvc/PortletFlowController.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/mvc/PortletFlowController.java new file mode 100644 index 00000000..bf03a9de --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/mvc/PortletFlowController.java @@ -0,0 +1,289 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.mvc; + +import java.util.HashMap; +import java.util.Map; + +import javax.portlet.ActionRequest; +import javax.portlet.ActionResponse; +import javax.portlet.PortletRequest; +import javax.portlet.PortletSession; +import javax.portlet.RenderRequest; +import javax.portlet.RenderResponse; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.Assert; +import org.springframework.web.portlet.ModelAndView; +import org.springframework.web.portlet.mvc.AbstractController; +import org.springframework.web.portlet.mvc.Controller; +import org.springframework.webflow.context.portlet.PortletExternalContext; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.execution.support.ExternalRedirect; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; +import org.springframework.webflow.executor.FlowExecutor; +import org.springframework.webflow.executor.ResponseInstruction; +import org.springframework.webflow.executor.support.FlowExecutorArgumentHandler; +import org.springframework.webflow.executor.support.RequestParameterFlowExecutorArgumentHandler; + +/** + * Point of integration between Spring Portlet MVC and Spring Web Flow: a + * {@link Controller} that routes incoming portlet requests to one or more + * managed flow executions. + *

    + * Requests into the web flow system are handled by a {@link FlowExecutor}, + * which this class delegates to. Consult the JavaDoc of that class for more + * information on how requests are processed. + *

    + * Note: a single PortletFlowController may execute all flows + * within your application. See the phonebook-portlet sample + * application for examples of the various strategies for launching and resuming + * flow executions in a Portlet environment. + *

    + * It is also possible to customize the {@link FlowExecutorArgumentHandler} + * strategy to allow for different types of controller parameterization, for + * example perhaps in conjunction with a REST-style request mapper. + * + * @see org.springframework.webflow.executor.FlowExecutor + * @see org.springframework.webflow.executor.support.FlowExecutorArgumentHandler + * + * @author Keith Donald + * @author Erwin Vervaet + * @author J.Enrique Ruiz + * @author César Ordiñana + */ +public class PortletFlowController extends AbstractController implements InitializingBean { + + /** + * Name of the attribute under which the response instruction will be stored + * in the session. + */ + private static final String RESPONSE_INSTRUCTION_SESSION_ATTRIBUTE = "actionRequest.responseInstruction"; + + /** + * Delegate for executing flow executions (launching new executions, and + * resuming existing executions). + */ + private FlowExecutor flowExecutor; + + /** + * Delegate for handler flow executor arguments. + */ + private FlowExecutorArgumentHandler argumentHandler = new RequestParameterFlowExecutorArgumentHandler(); + + /** + * Create a new portlet flow controller. Allows for bean style usage. + * @see #setFlowExecutor(FlowExecutor) + * @see #setArgumentHandler(FlowExecutorArgumentHandler) + */ + public PortletFlowController() { + // set the cache seconds property to 0 so no pages are cached by default + // for flows + setCacheSeconds(0); + // this controller stores ResponseInstruction objects in the session, so + // we need to ensure we do this in an orderly manner + // see exposeToRenderPhase() and extractActionResponseInstruction() + setSynchronizeOnSession(true); + } + + /** + * Returns the flow executor used by this controller. + * @return the flow executor + */ + public FlowExecutor getFlowExecutor() { + return flowExecutor; + } + + /** + * Configures the flow executor implementation to use. Required. + * @param flowExecutor the fully configured flow executor + */ + public void setFlowExecutor(FlowExecutor flowExecutor) { + this.flowExecutor = flowExecutor; + } + + /** + * Returns the flow executor argument handler used by this controller. + * @return the argument handler + */ + public FlowExecutorArgumentHandler getArgumentHandler() { + return argumentHandler; + } + + /** + * Sets the flow executor argument handler to use. + * @param argumentHandler the fully configured argument handler + */ + public void setArgumentHandler(FlowExecutorArgumentHandler argumentHandler) { + this.argumentHandler = argumentHandler; + } + + /** + * Sets the identifier of the default flow to launch if no flowId argument + * can be extracted by the configured {@link FlowExecutorArgumentHandler} + * during render request processing. + *

    + * This is a convenience method that sets the default flow id of the + * controller's argument handler. Don't use this when using + * {@link #setArgumentHandler(FlowExecutorArgumentHandler)}. + */ + public void setDefaultFlowId(String defaultFlowId) { + argumentHandler.setDefaultFlowId(defaultFlowId); + } + + public void afterPropertiesSet() { + Assert.notNull(flowExecutor, "The flow executor property is required"); + Assert.notNull(argumentHandler, "The argument handler property is required"); + } + + protected ModelAndView handleRenderRequestInternal(RenderRequest request, RenderResponse response) throws Exception { + PortletExternalContext context = new PortletExternalContext(getPortletContext(), request, response); + if (argumentHandler.isFlowExecutionKeyPresent(context)) { + // flowExecutionKey render param present: this is a request to + // render an active flow execution -- extract its key + String flowExecutionKey = argumentHandler.extractFlowExecutionKey(context); + // look for a cached response instruction in the session put there + // by the action request phase as part of an "active view" forward + ResponseInstruction responseInstruction = extractActionResponseInstruction(request); + if (responseInstruction == null) { + // no response instruction found, simply refresh the current + // view state of the flow execution + return toModelAndView(flowExecutor.refresh(flowExecutionKey, context)); + } + else { + // found: convert it to model and view for rendering + return toModelAndView(responseInstruction); + } + } + else { + // this is either a "launch" flow request or a "confirmation view" + // render request -- look for the cached "confirmation view" + // response instruction + ResponseInstruction responseInstruction = extractActionResponseInstruction(request); + if (responseInstruction == null) { + // no response instruction found in session - launch a new flow + // execution + String flowId = argumentHandler.extractFlowId(context); + return toModelAndView(flowExecutor.launch(flowId, context)); + } + else { + // found: convert it to model and view for rendering + return toModelAndView(responseInstruction); + } + } + } + + protected void handleActionRequestInternal(ActionRequest request, ActionResponse response) throws Exception { + PortletExternalContext context = new PortletExternalContext(getPortletContext(), request, response); + String flowExecutionKey = argumentHandler.extractFlowExecutionKey(context); + String eventId = argumentHandler.extractEventId(context); + // signal the event against the flow execution, returning the next + // response instruction + ResponseInstruction responseInstruction = flowExecutor.resume(flowExecutionKey, eventId, context); + if (responseInstruction.isApplicationView()) { + // response instruction is a forward to an "application view" + if (responseInstruction.isActiveView()) { + // is an "active" forward from a view-state (not end-state) -- + // set the flow execution key render parameter to support + // browser refresh + response.setRenderParameter( + argumentHandler.getFlowExecutionKeyArgumentName(), + responseInstruction.getFlowExecutionKey()); + } + // cache response instruction for access during render phase of this + // portlet + exposeToRenderPhase(responseInstruction, request); + } + else if (responseInstruction.isFlowExecutionRedirect()) { + // is a flow execution redirect: simply expose key parameter to + // support refresh during render phase + response.setRenderParameter( + argumentHandler.getFlowExecutionKeyArgumentName(), + responseInstruction.getFlowExecutionKey()); + } + else if (responseInstruction.isFlowDefinitionRedirect()) { + // set flow id render parameter to request that a new flow be + // launched within this portlet + FlowDefinitionRedirect redirect = (FlowDefinitionRedirect)responseInstruction.getViewSelection(); + response.setRenderParameters(redirect.getExecutionInput()); + response.setRenderParameter(argumentHandler.getFlowIdArgumentName(), redirect.getFlowDefinitionId()); + } + else if (responseInstruction.isExternalRedirect()) { + // issue the redirect to the external URL + ExternalRedirect redirect = (ExternalRedirect)responseInstruction.getViewSelection(); + String url = argumentHandler.createExternalUrl(redirect, flowExecutionKey, context); + response.sendRedirect(url); + } + else { + throw new IllegalArgumentException("Don't know how to handle response instruction " + responseInstruction); + } + } + + // helpers + + /** + * Expose given response instruction to the render phase by putting it in + * the session. + */ + private void exposeToRenderPhase(ResponseInstruction responseInstruction, ActionRequest request) { + PortletSession session = request.getPortletSession(false); + Assert.notNull(session, "A PortletSession is required"); + session.setAttribute(RESPONSE_INSTRUCTION_SESSION_ATTRIBUTE, responseInstruction); + } + + /** + * Extract a response instruction stored in the session during the action + * phase by {@link #exposeToRenderPhase(ResponseInstruction, ActionRequest)}. + * If a response instruction is found, it will be removed from the session. + * @param request the portlet request + * @return the response instructions found in the session or null if not + * found + */ + private ResponseInstruction extractActionResponseInstruction(PortletRequest request) { + PortletSession session = request.getPortletSession(false); + ResponseInstruction response = null; + if (session != null) { + response = (ResponseInstruction)session.getAttribute(RESPONSE_INSTRUCTION_SESSION_ATTRIBUTE); + if (response != null) { + // remove it + session.removeAttribute(RESPONSE_INSTRUCTION_SESSION_ATTRIBUTE); + } + } + return response; + } + + /** + * Convert given response instruction into a Spring Portlet MVC model and + * view. + */ + protected ModelAndView toModelAndView(ResponseInstruction response) { + if (response.isApplicationView()) { + // forward to a view as part of an active conversation + ApplicationView forward = (ApplicationView)response.getViewSelection(); + Map model = new HashMap(forward.getModel()); + argumentHandler.exposeFlowExecutionContext( + response.getFlowExecutionKey(), response.getFlowExecutionContext(), model); + return new ModelAndView(forward.getViewName(), model); + } + else if (response.isNull()) { + // no response to issue + return null; + } + else { + throw new IllegalArgumentException("Don't know how to handle response instruction " + response); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/mvc/package.html b/spring-webflow/src/main/java/org/springframework/webflow/executor/mvc/package.html new file mode 100644 index 00000000..90ed9ca5 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/mvc/package.html @@ -0,0 +1,5 @@ + + +The integration layer between Spring Web Flow the Spring (Portlet) MVC framework. + + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/package.html b/spring-webflow/src/main/java/org/springframework/webflow/executor/package.html new file mode 100644 index 00000000..863d8a4c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/package.html @@ -0,0 +1,13 @@ + + +

    +High-level executors for driving the execution of flow definitions. +

    +

    +The central concept defined by this package is the +{@link org.springframework.webflow.executor.FlowExecutor}, representing +a facade or entry-point for driving the execution of flows. This interface acts +as the system boundary into Spring Web Flow most calling systems interact with. +

    + + \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/struts/FlowAction.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/struts/FlowAction.java new file mode 100644 index 00000000..0087ee21 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/struts/FlowAction.java @@ -0,0 +1,318 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.struts; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.struts.action.ActionForm; +import org.apache.struts.action.ActionForward; +import org.apache.struts.action.ActionMapping; +import org.springframework.validation.Errors; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.struts.ActionSupport; +import org.springframework.web.struts.DelegatingActionProxy; +import org.springframework.web.struts.SpringBindingActionForm; +import org.springframework.web.util.WebUtils; +import org.springframework.webflow.action.FormObjectAccessor; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.execution.support.ExternalRedirect; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; +import org.springframework.webflow.executor.FlowExecutor; +import org.springframework.webflow.executor.ResponseInstruction; +import org.springframework.webflow.executor.support.FlowExecutorArgumentHandler; +import org.springframework.webflow.executor.support.FlowRequestHandler; +import org.springframework.webflow.executor.support.RequestParameterFlowExecutorArgumentHandler; + +/** + * Point of integration between Struts and Spring Web Flow: a Struts Action that + * acts a front controller entry point into the web flow system. A single + * FlowAction may launch any new FlowExecution. In addition, a single Flow + * Action may signal events in any existing/restored FlowExecutions. + *

    + * Requests are managed by and delegated to a {@link FlowExecutor}, which this + * class delegates to using a {@link FlowRequestHandler} (allowing reuse of + * common front flow controller logic in other environments). Consult the + * JavaDoc of those classes for more information on how requests are processed. + *

    + *

  • By default, to have this controller launch a new flow execution + * (conversation), have the client send a + * {@link FlowExecutorArgumentHandler#getFlowIdArgumentName()} request + * parameter indicating the flow definition to launch. + *
  • To have this controller participate in an existing flow execution + * (conversation), have the client send a + * {@link FlowExecutorArgumentHandler#getFlowExecutionKeyArgumentName()} + * request parameter identifying the conversation to participate in. + *

    + * On each request received by this action, a {@link StrutsExternalContext} + * object is created as input to the web flow system. This external source event + * provides access to the action form, action mapping, and other Struts-specific + * constructs. + *

    + * This class also is aware of the {@link SpringBindingActionForm} adapter, + * which adapts Spring's data binding infrastructure (based on POJO binding, a + * standard Errors interface, and property editor type conversion) to the Struts + * action form model. This option gives backend web-tier developers full support + * for POJO-based binding with minimal hassel, while still providing consistency + * to view developers who already have a lot of experience with Struts for + * markup and request dispatching. + *

    + * Below is an example struts-config.xml configuration for a + * FlowAction: + * + *

    + *     <action path="/userRegistration"
    + *         type="org.springframework.webflow.executor.struts.FlowAction"
    + *         name="springBindingActionForm" scope="request">
    + *     </action>
    + * 
    + * + * This example maps the logical request URL /userRegistration.do + * as a Flow controller (FlowAction). It is expected that flows + * to launch be provided in a dynamic fashion by the views (allowing this single + * FlowAction to manage any number of flow executions). A Spring + * binding action form instance is set in request scope, acting as an adapter + * enabling POJO-based binding and validation with Spring. + *

    + * Other notes regarding Struts/Spring Web Flow integration: + *

      + *
    • Logical view names returned when ViewStates and + * EndStates are entered are mapped to physical view templates + * using standard Struts action forwards (typically global forwards).
    • + *
    • Use of the SpringBindingActionForm requires no special + * setup in struts-config.xml: simply declare a form bean in + * request scope of the class + * org.springframework.web.struts.SpringBindingActionForm and use + * it with your FlowAction.
    • + *
    • This class depends on a {@link FlowExecutor} instance to be configured. + * If relying on Spring's {@link DelegatingActionProxy} (which is recommended), + * a FlowExecutor reference can simply be injected using standard Spring + * dependency injection techniques. If you are not using the proxy-based + * approach, this class will attempt a root context lookup on initialization, + * first querying for a bean of instance {@link FlowExecutor} named + * {@link #FLOW_EXECUTOR_BEAN_NAME}.
    • + *
    • The + * {@link org.springframework.webflow.executor.support.FlowExecutorArgumentHandler} + * used by the FlowAction can be configured in the root context using a bean of + * name {@link #FLOW_EXECUTOR_ARGUMENT_HANDLER_BEAN_NAME}. If not explicitly + * specified it will default to a normal + * {@link org.springframework.webflow.executor.support.RequestParameterFlowExecutorArgumentHandler} + * with standard configuration.
    • + *
    + *

    + * The benefits here are considerable: developers now have a powerful web flow + * capability integrated with Struts, with a consistent-approach to POJO-based + * binding and validation that addresses the proliferation of + * ActionForm classes found in traditional Struts-based apps. + * + * @see org.springframework.webflow.executor.FlowExecutor + * @see org.springframework.webflow.executor.support.FlowRequestHandler + * @see org.springframework.web.struts.SpringBindingActionForm + * @see org.springframework.web.struts.DelegatingActionProxy + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class FlowAction extends ActionSupport { + + /** + * The flow executor will be retreived from the application context using + * this bean name if no executor is explicitly set. ("flowExecutor") + */ + protected static final String FLOW_EXECUTOR_BEAN_NAME = "flowExecutor"; + + /** + * The flow executor argument handler will be retreived from the + * application context using this bean name if no argument handler is + * explicitly set. ("argumentHandler") + */ + protected static final String FLOW_EXECUTOR_ARGUMENT_HANDLER_BEAN_NAME = "argumentHandler"; + + /** + * The service responsible for launching and signaling Struts-originating + * events in flow executions. + */ + private FlowExecutor flowExecutor; + + /** + * Delegate to handle flow executor arguments. + */ + private FlowExecutorArgumentHandler argumentHandler; + + /** + * Returns the flow executor used by this controller. + * @return the flow executor + */ + public FlowExecutor getFlowExecutor() { + return flowExecutor; + } + + /** + * Configures the flow executor implementation to use. Required. + * @param flowExecutor the fully configured flow executor + */ + public void setFlowExecutor(FlowExecutor flowExecutor) { + this.flowExecutor = flowExecutor; + } + + /** + * Returns the flow executor argument handler used by this controller. + * @return the argument handler + */ + public FlowExecutorArgumentHandler getArgumentHandler() { + return argumentHandler; + } + + /** + * Sets the flow executor argument handler to use. + * @param argumentHandler the fully configured argument handler + */ + public void setArgumentHandler(FlowExecutorArgumentHandler argumentHandler) { + this.argumentHandler = argumentHandler; + } + + protected void onInit() { + WebApplicationContext context = getWebApplicationContext(); + if (getFlowExecutor() == null) { + if (context.containsBean(FLOW_EXECUTOR_BEAN_NAME)) { + setFlowExecutor((FlowExecutor)context.getBean(FLOW_EXECUTOR_BEAN_NAME, FlowExecutor.class)); + } + else { + throw new IllegalStateException("No '" + FLOW_EXECUTOR_BEAN_NAME + + "' bean definition could be found; to use Spring Web Flow with Struts you must " + + "configure this FlowAction with a FlowExecutor"); + } + } + if (getArgumentHandler() == null) { + if (context.containsBean(FLOW_EXECUTOR_ARGUMENT_HANDLER_BEAN_NAME)) { + setArgumentHandler((FlowExecutorArgumentHandler)context.getBean( + FLOW_EXECUTOR_ARGUMENT_HANDLER_BEAN_NAME, FlowExecutorArgumentHandler.class)); + } + else { + // default + argumentHandler = new RequestParameterFlowExecutorArgumentHandler(); + } + } + } + + public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, + HttpServletResponse response) throws Exception { + ExternalContext context = new StrutsExternalContext(mapping, form, getServletContext(), request, response); + ResponseInstruction responseInstruction = createRequestHandler().handleFlowRequest(context); + return toActionForward(responseInstruction, mapping, form, request, response, context); + } + + /** + * Factory method that creates a new helper for processing a request into + * this flow controller. + * @return the controller helper + */ + protected FlowRequestHandler createRequestHandler() { + return new FlowRequestHandler(getFlowExecutor(), getArgumentHandler()); + } + + /** + * Return a Struts ActionForward given a ResponseInstruction. Adds all + * attributes from the ResponseInstruction as request attributes. + */ + protected ActionForward toActionForward(ResponseInstruction response, ActionMapping mapping, ActionForm form, + HttpServletRequest request, HttpServletResponse httpResponse, ExternalContext context) throws Exception { + if (response.isApplicationView()) { + // forward to a view as part of an active conversation + ApplicationView forward = (ApplicationView)response.getViewSelection(); + Map model = new HashMap(forward.getModel()); + argumentHandler.exposeFlowExecutionContext( + response.getFlowExecutionKey(), response.getFlowExecutionContext(), model); + WebUtils.exposeRequestAttributes(request, model); + if (form instanceof SpringBindingActionForm) { + SpringBindingActionForm bindingForm = (SpringBindingActionForm)form; + // expose the form object and associated errors as the + // "current form object" in the request + Errors currentErrors = (Errors)model.get(FormObjectAccessor.getCurrentFormErrorsName()); + bindingForm.expose(currentErrors, request); + } + return findForward(forward, mapping); + + } + else if (response.isFlowExecutionRedirect()) { + // redirect to active flow execution URL + String flowExecutionUrl = argumentHandler.createFlowExecutionUrl( + response.getFlowExecutionKey(), response.getFlowExecutionContext(), context); + return createRedirectForward(flowExecutionUrl, httpResponse); + } + else if (response.isFlowDefinitionRedirect()) { + // restart the flow by redirecting to flow launch URL + String flowUrl = argumentHandler.createFlowDefinitionUrl( + (FlowDefinitionRedirect)response.getViewSelection(), context); + return createRedirectForward(flowUrl, httpResponse); + } + else if (response.isExternalRedirect()) { + // redirect to external URL + String externalUrl = argumentHandler.createExternalUrl( + (ExternalRedirect)response.getViewSelection(), response.getFlowExecutionKey(), context); + return createRedirectForward(externalUrl, httpResponse); + } + else if (response.isNull()) { + // no response to issue + return null; + } + else { + throw new IllegalArgumentException("Don't know how to handle response instruction " + response); + } + } + + /** + * Handles a redirect. This implementation simply calls sendRedirect on the + * response object. + * @param url the url to redirect to + * @param response the http response + * @return the redirect forward, this implementation returns null + * @throws Exception an excpetion occured processing the redirect + * @see HttpServletResponse#sendRedirect(java.lang.String) + */ + protected ActionForward createRedirectForward(String url, HttpServletResponse response) throws Exception { + response.sendRedirect(url); + return null; + } + + /** + * Find an action forward for given application view. If no suitable forward + * is found in the action mapping using the view name as a key, this method + * will create a new action forward using the view name. + * @param forward the application view to find a forward for + * @param mapping the action mapping to use + * @return the action forward, never null + */ + protected ActionForward findForward(ApplicationView forward, ActionMapping mapping) { + // note that this method is always creating a new ActionForward to make + // sure that the redirect flag is false -- redirect is controlled by SWF + // itself, not Struts + ActionForward actionForward = mapping.findForward(forward.getViewName()); + if (actionForward != null) { + // the 1.2.1 copy constructor would ideally be better to + // use, but it is not Struts 1.1 compatible + actionForward = new ActionForward(actionForward.getName(), actionForward.getPath(), false); + } + else { + actionForward = new ActionForward(forward.getViewName(), false); + } + return actionForward; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/struts/StrutsExternalContext.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/struts/StrutsExternalContext.java new file mode 100644 index 00000000..a7bc8f60 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/struts/StrutsExternalContext.java @@ -0,0 +1,72 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.struts; + +import javax.servlet.ServletContext; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.apache.struts.action.ActionForm; +import org.apache.struts.action.ActionMapping; +import org.springframework.webflow.context.servlet.ServletExternalContext; + +/** + * Provides consistent access to a Struts environment from within the Spring Web + * Flow system. Represents the context of a request into SWF from Struts. + * + * @author Keith Donald + */ +public class StrutsExternalContext extends ServletExternalContext { + + /** + * The Struts action mapping associated with this request. + */ + private ActionMapping actionMapping; + + /** + * The Struts action form associated with this request. + */ + private ActionForm actionForm; + + /** + * Creates a new Struts external context. + * @param mapping the action mapping + * @param form the action form + * @param context the servlet context + * @param request the request + * @param response the response + */ + public StrutsExternalContext(ActionMapping mapping, ActionForm form, ServletContext context, + HttpServletRequest request, HttpServletResponse response) { + super(context, request, response); + this.actionMapping = mapping; + this.actionForm = form; + } + + /** + * Returns the action form. + */ + public ActionForm getActionForm() { + return actionForm; + } + + /** + * Returns the action mapping. + */ + public ActionMapping getActionMapping() { + return actionMapping; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/struts/package.html b/spring-webflow/src/main/java/org/springframework/webflow/executor/struts/package.html new file mode 100644 index 00000000..5d3b0642 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/struts/package.html @@ -0,0 +1,5 @@ + + +The integration layer between Spring Web Flow and Struts 1.x. + + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowExecutorArgumentExposer.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowExecutorArgumentExposer.java new file mode 100644 index 00000000..f77dad3a --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowExecutorArgumentExposer.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.support; + +import java.util.Map; + +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.execution.FlowExecutionContext; +import org.springframework.webflow.execution.support.ExternalRedirect; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; +import org.springframework.webflow.execution.support.FlowExecutionRedirect; +import org.springframework.webflow.executor.FlowExecutor; + +/** + * Helper strategy that can expose {@link FlowExecutor} method arguments in + * a response (view) so that subsequent requests resulting from the response + * can have those arguments extracted again, typically using a + * {@link FlowExecutorArgumentExtractor}. + *

    + * Arguments can either be exposed in the model of a view that will be + * rendered or in a URL that will be used to trigger a new request into + * Spring Web Flow, for instance using a redirect. + * + * @author Erwin Vervaet + */ +public interface FlowExecutorArgumentExposer { + + /** + * Expose the flow execution context and it's key in given model map. + * @param flowExecutionKey the flow execution key (may be null if the + * conversation has ended) + * @param context the flow execution context + * @param model the model map + */ + public void exposeFlowExecutionContext(String flowExecutionKey, FlowExecutionContext context, Map model); + + /** + * Create a URL that when redirected to launches a entirely new execution of + * a flow definition (starts a new conversation). Used to support the restart flow + * and redirect to flow use cases. + * @param flowDefinitionRedirect the flow definition redirect view selection + * @param context the external context + * @return the relative flow URL path to redirect to + */ + public String createFlowDefinitionUrl(FlowDefinitionRedirect flowDefinitionRedirect, ExternalContext context); + + /** + * Create a URL path that when redirected to renders the current (or + * last) view selection made by the flow execution identified by the flow + * execution key. Used to support the flow execution redirect use + * case. + * @param flowExecutionKey the flow execution key + * @param flowExecution the flow execution + * @param context the external context + * @return the relative conversation URL path + * @see FlowExecutionRedirect + */ + public String createFlowExecutionUrl(String flowExecutionKey, FlowExecutionContext flowExecution, + ExternalContext context); + + /** + * Create a URL path that when redirected to communicates with an external + * system outside of Spring Web Flow. + * @param redirect the external redirect request + * @param flowExecutionKey the flow execution key to send through the + * redirect (optional) + * @param context the external context + * @return the external URL + */ + public String createExternalUrl(ExternalRedirect redirect, String flowExecutionKey, ExternalContext context); + +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowExecutorArgumentExtractionException.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowExecutorArgumentExtractionException.java new file mode 100644 index 00000000..57f18bc3 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowExecutorArgumentExtractionException.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.support; + +import org.springframework.webflow.core.FlowException; + +/** + * An exception thrown by a flow executor argument extractor when an + * argument could not be extracted. + * + * @see org.springframework.webflow.executor.support.FlowExecutorArgumentExtractor + * + * @author Keith Donald + */ +public class FlowExecutorArgumentExtractionException extends FlowException { + + /** + * Creates a new argument extraction exception. + * @param msg a descriptive message + */ + public FlowExecutorArgumentExtractionException(String msg) { + super(msg); + } + + /** + * Creates a new argument extraction exception. + * @param msg a descriptive message + * @param cause the cause + */ + public FlowExecutorArgumentExtractionException(String msg, Throwable cause) { + super(msg, cause); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowExecutorArgumentExtractor.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowExecutorArgumentExtractor.java new file mode 100644 index 00000000..73ba42e1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowExecutorArgumentExtractor.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.support; + +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.execution.repository.FlowExecutionKey; +import org.springframework.webflow.executor.FlowExecutor; + +/** + * A helper strategy used by the {@link FlowRequestHandler} to extract + * {@link FlowExecutor} method arguments from a request initiated by + * an {@link ExternalContext}. The extracted arguments were typically + * exposed in the previous response (the response that resulted in + * a new request) using a {@link FlowExecutorArgumentExposer}. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public interface FlowExecutorArgumentExtractor { + + /** + * Returns true if the flow id is extractable from the context. + * @param context the context in which a external user event occured + * @return true if extractable, false if not + */ + public boolean isFlowIdPresent(ExternalContext context); + + /** + * Extracts the flow id from the external context. + * @param context the context in which a external user event occured + * @return the extracted flow id + * @throws FlowExecutorArgumentExtractionException if the flow id could not + * be extracted + */ + public String extractFlowId(ExternalContext context) throws FlowExecutorArgumentExtractionException; + + /** + * Returns true if the flow execution key is extractable from the context. + * @param context the context in which a external user event occured + * @return true if extractable, false if not + */ + public boolean isFlowExecutionKeyPresent(ExternalContext context); + + /** + * Extract the flow execution key from the external context. + * @param context the context in which the external user event occured + * @return the obtained flow execution key + * @throws FlowExecutorArgumentExtractionException if the flow execution key + * could not be extracted + */ + public String extractFlowExecutionKey(ExternalContext context) throws FlowExecutorArgumentExtractionException; + + /** + * Returns true if the event id is extractable from the context. + * @param context the context in which a external user event occured + * @return true if extractable, false if not + */ + public boolean isEventIdPresent(ExternalContext context); + + /** + * Extract the flow execution event id from the external context. + *

    + * This method should only be called if a {@link FlowExecutionKey} was + * successfully extracted, indicating a request to resume a flow execution. + * @param context the context in which a external user event occured + * @return the event id + * @throws FlowExecutorArgumentExtractionException if the event id could not + * be extracted + */ + public String extractEventId(ExternalContext context) throws FlowExecutorArgumentExtractionException; + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowExecutorArgumentHandler.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowExecutorArgumentHandler.java new file mode 100644 index 00000000..16320810 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowExecutorArgumentHandler.java @@ -0,0 +1,366 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.support; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Map; + +import org.springframework.core.JdkVersion; +import org.springframework.util.StringUtils; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.execution.FlowExecutionContext; +import org.springframework.webflow.execution.support.ExternalRedirect; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; + +/** + * Abstract base class for objects handling + * {@link org.springframework.webflow.executor.FlowExecutor} arguments. This + * class combines the two argument handling responsabilities of ({@link FlowExecutorArgumentExtractor extraction} + * and {@link FlowExecutorArgumentExposer exposing}) and makes sure they are + * consistent, i.e. that exposed arguments can later be extracted again. + *

    + * All argument names are configurable. Common convenience functionality is also + * provided, e.g. a {@link #applyDefaultFlowId(String) default flow id}, + * {@link #encodeValue(Object) URL encoding} and dealing with + * {@link #makeRedirectUrlContextRelativeIfNecessary(String, ExternalContext) relative URLs}. + * Subclasses are responsible for taking these settings into account when + * implementing actual argument extraction and exposing behavior. + * + * @see FlowExecutorArgumentExtractor + * @see FlowExecutorArgumentExposer + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public abstract class FlowExecutorArgumentHandler implements FlowExecutorArgumentExtractor, FlowExecutorArgumentExposer { + + // data and behavior related to argument extraction + + /** + * By default clients can send the id of the flow definition to be launched + * using an argument with this name ("_flowId"). + */ + private static final String FLOW_ID_ARGUMENT_NAME = "_flowId"; + + /** + * By default clients can send the key of a flow execution to be resumed + * using an argument with this name ("_flowExecutionKey"). + */ + private static final String FLOW_EXECUTION_KEY_ARGUMENT_NAME = "_flowExecutionKey"; + + /** + * By default clients can send the event to be signaled in an argument with + * this name ("_eventId"). + */ + private static final String EVENT_ID_ARGUMENT_NAME = "_eventId"; + + /** + * Identifies a flow definition to launch a new execution for, defaults to + * {@link #FLOW_ID_ARGUMENT_NAME}. + */ + private String flowIdArgumentName = FLOW_ID_ARGUMENT_NAME; + + /** + * Input argument that identifies an existing flow execution to participate + * in, defaults to {@link #FLOW_EXECUTION_KEY_ARGUMENT_NAME}. + */ + private String flowExecutionKeyArgumentName = FLOW_EXECUTION_KEY_ARGUMENT_NAME; + + /** + * Identifies an event that occured in an existing flow execution, defaults + * to {@link #EVENT_ID_ARGUMENT_NAME}. + */ + private String eventIdArgumentName = EVENT_ID_ARGUMENT_NAME; + + /** + * The flow definition id to use if no flowId argument value can be + * extracted during the {@link #extractFlowId(ExternalContext)} operation. + * Default value is null. + */ + private String defaultFlowId; + + /** + * Returns the flow id argument name, used to request a flow to launch. + */ + public String getFlowIdArgumentName() { + return flowIdArgumentName; + } + + /** + * Sets the flow id argument name, used to request a flow to launch. + */ + public void setFlowIdArgumentName(String flowIdArgumentName) { + this.flowIdArgumentName = flowIdArgumentName; + } + + /** + * Returns the flow execution key argument name, used to request that an + * executing conversation resumes. + */ + public String getFlowExecutionKeyArgumentName() { + return flowExecutionKeyArgumentName; + } + + /** + * Sets the flow execution key argument name, used to request that an + * executing conversation resumes. + */ + public void setFlowExecutionKeyArgumentName(String flowExecutionKeyArgumentName) { + this.flowExecutionKeyArgumentName = flowExecutionKeyArgumentName; + } + + /** + * Returns the event id argument name, used to signal what user action + * happened within a paused flow execution. + */ + public String getEventIdArgumentName() { + return eventIdArgumentName; + } + + /** + * Sets the event id argument name, used to signal what user action happened + * within a paused flow execution. + */ + public void setEventIdArgumentName(String eventIdArgumentName) { + this.eventIdArgumentName = eventIdArgumentName; + } + + /** + * Returns the default flowId argument value. If no flow id argument + * is provided, the default acts as a fallback. Defaults to + * null. + */ + public String getDefaultFlowId() { + return defaultFlowId; + } + + /** + * Sets the default flowId argument value. + *

    + * This value will be used if no flowId argument value can be extracted from + * the request by the {@link #extractFlowId(ExternalContext)} operation. + */ + public void setDefaultFlowId(String defaultFlowId) { + this.defaultFlowId = defaultFlowId; + } + + // data and behavior for response issuance + + /** + * The string-encoded id of the flow execution will be exposed to the view + * in a model attribute with this name ("flowExecutionKey"). + */ + private static final String FLOW_EXECUTION_KEY_ATTRIBUTE = "flowExecutionKey"; + + /** + * The flow execution context itself will be exposed to the view in a model + * attribute with this name ("flowExecutionContext"). + */ + private static final String FLOW_EXECUTION_CONTEXT_ATTRIBUTE = "flowExecutionContext"; + + /** + * The default URL encoding scheme: UTF-8. + */ + private static final String DEFAULT_URL_ENCODING_SCHEME = "UTF-8"; + + /** + * Model attribute that identifies the flow execution participated in, + * defaults to {@link #FLOW_EXECUTION_KEY_ATTRIBUTE}. + */ + private String flowExecutionKeyAttributeName = FLOW_EXECUTION_KEY_ATTRIBUTE; + + /** + * Model attribute that provides state about the flow execution participated + * in, defaults to {@link #FLOW_EXECUTION_CONTEXT_ATTRIBUTE}. + */ + private String flowExecutionContextAttributeName = FLOW_EXECUTION_CONTEXT_ATTRIBUTE; + + /** + * The url encoding scheme to be used to encode URLs built by this argument + * handler. Defaults to {@link #DEFAULT_URL_ENCODING_SCHEME}. + */ + private String urlEncodingScheme = DEFAULT_URL_ENCODING_SCHEME; + + /** + * A flag indicating whether to interpret a redirect URL that starts with a + * slash ("/") as relative to the current ServletContext, i.e. as relative + * to the web application root, as opposed to absolute. Default is true. + */ + private boolean redirectContextRelative = true; + + /** + * Returns the flow execution key attribute name, used as a model attribute + * for identifying the executing flow being participated in. + */ + public String getFlowExecutionKeyAttributeName() { + return flowExecutionKeyAttributeName; + } + + /** + * Sets the flow execution key attribute name, used as a model attribute for + * identifying the current state of the executing flow being participated in + * (typically used by view templates during rendering). + */ + public void setFlowExecutionKeyAttributeName(String flowExecutionKeyAttributeName) { + this.flowExecutionKeyAttributeName = flowExecutionKeyAttributeName; + } + + /** + * Returns the flow execution context attribute name. + */ + public String getFlowExecutionContextAttributeName() { + return flowExecutionContextAttributeName; + } + + /** + * Sets the flow execution context attribute name. + */ + public void setFlowExecutionContextAttributeName(String flowExecutionContextAttributeName) { + this.flowExecutionContextAttributeName = flowExecutionContextAttributeName; + } + + /** + * Returns the url encoding scheme to be used to encode URLs built by this + * argument handler. Defaults to "UTF-8". + */ + public String getUrlEncodingScheme() { + return urlEncodingScheme; + } + + /** + * Set the url encoding scheme to be used to encode URLs built by this + * argument handler. Defaults to "UTF-8". + */ + public void setUrlEncodingScheme(String urlEncodingScheme) { + this.urlEncodingScheme = urlEncodingScheme; + } + + /** + * Set whether to interpret a given redirect URL that starts with a slash + * ("/") as relative to the current ServletContext, i.e. as relative to the + * web application root. + *

    + * Default is "true": A redirect URL that starts with a slash will be + * interpreted as relative to the web application root, i.e. the context + * path will be prepended to the URL. + */ + public void setRedirectContextRelative(boolean redirectContextRelative) { + this.redirectContextRelative = redirectContextRelative; + } + + /** + * Return whether to interpret a given redirect URL that starts with a slash + * ("/") as relative to the current ServletContext, i.e. as relative to the + * web application root. + */ + public boolean isRedirectContextRelative() { + return redirectContextRelative; + } + + public abstract boolean isFlowIdPresent(ExternalContext context); + + public abstract String extractFlowId(ExternalContext context) throws FlowExecutorArgumentExtractionException; + + public abstract boolean isFlowExecutionKeyPresent(ExternalContext context); + + public abstract String extractFlowExecutionKey(ExternalContext context) + throws FlowExecutorArgumentExtractionException; + + public abstract boolean isEventIdPresent(ExternalContext context); + + public abstract String extractEventId(ExternalContext context) throws FlowExecutorArgumentExtractionException; + + public void exposeFlowExecutionContext(String flowExecutionKey, FlowExecutionContext context, Map model) { + if (flowExecutionKey != null) { + model.put(getFlowExecutionKeyAttributeName(), flowExecutionKey); + } + model.put(getFlowExecutionContextAttributeName(), context); + } + + public abstract String createFlowDefinitionUrl(FlowDefinitionRedirect flowDefinitionRedirect, + ExternalContext context); + + public abstract String createFlowExecutionUrl(String flowExecutionKey, FlowExecutionContext flowExecution, + ExternalContext context); + + public abstract String createExternalUrl(ExternalRedirect redirect, String flowExecutionKey, ExternalContext context); + + // helpers for use in subclasses + + /** + * Apply the configured default flow id to given extracted flow id. + * @param extractedFlowId the extracted flow id, could be null if non was + * available in the external context + * @return the extracted flow id if not empty, the default flow id otherwise + * (which could still be null if not set) + * @see #getDefaultFlowId() + */ + protected String applyDefaultFlowId(String extractedFlowId) { + return StringUtils.hasText(extractedFlowId) ? extractedFlowId : getDefaultFlowId(); + } + + /** + * URL-encode the given input object with the configured encoding scheme. + * @param value the unencoded value + * @return the encoded output String + * @see #getUrlEncodingScheme() + */ + protected String encodeValue(Object value) { + return value != null ? urlEncode(value.toString()) : ""; + } + + /** + * Make given redirect URL context relative if necessary. If the URL starts + * with a slash ("/") it will be made relative to the current + * ServletContext, i.e. relative to the web application root. + * @param url the original URL + * @param context the external context + * @return the processed URL + * @see #isRedirectContextRelative() + */ + protected String makeRedirectUrlContextRelativeIfNecessary(String url, ExternalContext context) { + StringBuffer res = new StringBuffer(); + if (url.startsWith("/") && isRedirectContextRelative()) { + res.append(context.getContextPath()); + } + res.append(url); + return res.toString(); + } + + // internal helpers + + /** + * URL-encode the given input String with the configured encoding scheme. + *

    + * Default implementation uses URLEncoder.encode(input, enc) + * on JDK 1.4+, falling back to URLEncoder.encode(input) + * (which uses the platform default encoding) on JDK 1.3. + * @param input the unencoded input String + * @return the encoded output String + */ + private String urlEncode(String input) { + if (JdkVersion.getMajorJavaVersion() < JdkVersion.JAVA_14) { + return URLEncoder.encode(input); + } + try { + return URLEncoder.encode(input, getUrlEncodingScheme()); + } + catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException("Cannot encode URL " + input); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowRequestHandler.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowRequestHandler.java new file mode 100644 index 00000000..d78560f1 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/FlowRequestHandler.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.support; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.util.Assert; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.FlowException; +import org.springframework.webflow.executor.FlowExecutor; +import org.springframework.webflow.executor.ResponseInstruction; + +/** + * An immutable helper for flow controllers that encapsulates reusable workflow + * required to launch and resume flow executions using a {@link FlowExecutor}. + *

    + * The {@link #handleFlowRequest(ExternalContext)} method is the central helper + * operation and implements the following algorithm: + *

      + *
    1. Extract the flow execution id by calling + * {@link FlowExecutorArgumentExtractor#extractFlowExecutionKey(ExternalContext)}. + *
    2. If a valid flow execution id was extracted, signal an event in that + * existing execution to resume it. The event to signal is determined by calling + * the {@link FlowExecutorArgumentExtractor#extractEventId(ExternalContext)} + * method. If no event can be extracted, the existing execution will be refreshed. + *
    3. If no flow execution id was extracted, launch a new flow execution. The + * top-level flow definition for which an execution is created is determined by + * extracting the flow id using the method + * {@link FlowExecutorArgumentExtractor#extractFlowId(ExternalContext)}. If no + * valid flow id can be determined, an exception is thrown. + *
    + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class FlowRequestHandler { + + /** + * Logger. + */ + private static final Log logger = LogFactory.getLog(FlowRequestHandler.class); + + /** + * The flow executor this helper will coordinate with. + */ + private FlowExecutor flowExecutor; + + /** + * A helper for extracting arguments of flow executor operations + * from the external context. + */ + private FlowExecutorArgumentExtractor argumentExtractor; + + /** + * Creates a new flow controller helper. Will use the default + * {@link RequestParameterFlowExecutorArgumentHandler}. + * @param flowExecutor the flow execution manager to delegate to + */ + public FlowRequestHandler(FlowExecutor flowExecutor) { + this(flowExecutor, new RequestParameterFlowExecutorArgumentHandler()); + } + + /** + * Creates a new flow controller helper. + * @param flowExecutor the flow executor to delegate to + * @param argumentExtractor the flow executor argument extractor to use + */ + public FlowRequestHandler(FlowExecutor flowExecutor, FlowExecutorArgumentExtractor argumentExtractor) { + Assert.notNull(flowExecutor, "The flow executor is required"); + Assert.notNull(argumentExtractor, "The flow executor argument extractor is required"); + this.flowExecutor = flowExecutor; + this.argumentExtractor = argumentExtractor; + } + + /** + * Returns the flow executor used by this helper. + */ + public FlowExecutor getFlowExecutor() { + return flowExecutor; + } + + /** + * Returns the flow executor argument extractor used by this helper. + */ + public FlowExecutorArgumentExtractor getArgumentExtractor() { + return argumentExtractor; + } + + /** + * Handle a request into the Spring Web Flow system from an external system. + * @param context the external context in which the request occured + * @return the selected view that should be rendered as a response + */ + public ResponseInstruction handleFlowRequest(ExternalContext context) throws FlowException { + if (logger.isDebugEnabled()) { + logger.debug("Request initiated by " + context); + } + if (argumentExtractor.isFlowExecutionKeyPresent(context)) { + String flowExecutionKey = argumentExtractor.extractFlowExecutionKey(context); + if (argumentExtractor.isEventIdPresent(context)) { + String eventId = argumentExtractor.extractEventId(context); + ResponseInstruction response = flowExecutor.resume(flowExecutionKey, eventId, context); + if (logger.isDebugEnabled()) { + logger.debug("Returning [resume] " + response); + } + return response; + } + else { + ResponseInstruction response = flowExecutor.refresh(flowExecutionKey, context); + if (logger.isDebugEnabled()) { + logger.debug("Returning [refresh] " + response); + } + return response; + } + } + else { + String flowDefinitionId = argumentExtractor.extractFlowId(context); + ResponseInstruction response = flowExecutor.launch(flowDefinitionId, context); + if (logger.isDebugEnabled()) { + logger.debug("Returning [launch] " + response); + } + return response; + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/support/RequestParameterFlowExecutorArgumentHandler.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/RequestParameterFlowExecutorArgumentHandler.java new file mode 100644 index 00000000..fe2d3f97 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/RequestParameterFlowExecutorArgumentHandler.java @@ -0,0 +1,255 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.support; + +import java.util.Iterator; +import java.util.Map; + +import org.springframework.core.style.StylerUtils; +import org.springframework.util.StringUtils; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.collection.ParameterMap; +import org.springframework.webflow.execution.FlowExecutionContext; +import org.springframework.webflow.execution.support.ExternalRedirect; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; +import org.springframework.webflow.executor.FlowExecutor; + +/** + * Default {@link FlowExecutor} argument handler that extracts flow executor + * method arguments from the {@link ExternalContext#getRequestParameterMap()} + * and exposes arguments as URL encoded request parameters. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class RequestParameterFlowExecutorArgumentHandler extends FlowExecutorArgumentHandler { + + /** + * The default delimiter used when a parameter value is encoded as part of + * the name of a parameter, e.g. "_eventId_submit" ("_"). + *

    + * This form is typically used to support multiple HTML buttons on a form + * without resorting to Javascript to communicate the event that corresponds + * to a button. + */ + private static final String PARAMETER_VALUE_DELIMITER = "_"; + + /** + * The embedded parameter name/value delimiter, used to parse a parameter + * value when a value is embedded in a parameter name (e.g. + * "_eventId_submit"). Defaults to {@link #PARAMETER_VALUE_DELIMITER}. + */ + private String parameterValueDelimiter = PARAMETER_VALUE_DELIMITER; + + /** + * Returns the delimiter used to parse a parameter value when a value is + * embedded in a parameter name (e.g. "_eventId_submit"). Defaults to "_". + */ + public String getParameterValueDelimiter() { + return parameterValueDelimiter; + } + + /** + * Set the delimiter used to parse a parameter value when a value is + * embedded in a parameter name (e.g. "_eventId_submit"). + */ + public void setParameterValueDelimiter(String parameterValueDelimiter) { + this.parameterValueDelimiter = parameterValueDelimiter; + } + + public boolean isFlowIdPresent(ExternalContext context) { + return context.getRequestParameterMap().contains(getFlowIdArgumentName()); + } + + public String extractFlowId(ExternalContext context) throws FlowExecutorArgumentExtractionException { + String flowId = context.getRequestParameterMap().get(getFlowIdArgumentName()); + flowId = applyDefaultFlowId(flowId); + if (!StringUtils.hasText(flowId)) { + throw new FlowExecutorArgumentExtractionException( + "Unable to extract the flow definition id parameter: make sure the client provides the '" + + getFlowIdArgumentName() + + "' parameter as input or set the 'defaultFlowId' property; " + + "the parameters provided in this request are: " + + StylerUtils.style(context.getRequestParameterMap())); + } + return flowId; + } + + public boolean isFlowExecutionKeyPresent(ExternalContext context) { + return context.getRequestParameterMap().contains(getFlowExecutionKeyArgumentName()); + } + + public String extractFlowExecutionKey(ExternalContext context) throws FlowExecutorArgumentExtractionException { + String encodedKey = context.getRequestParameterMap().get(getFlowExecutionKeyArgumentName()); + if (!StringUtils.hasText(encodedKey)) { + throw new FlowExecutorArgumentExtractionException( + "Unable to extract the flow execution key parameter: make sure the client provides the '" + + getFlowExecutionKeyArgumentName() + + "' parameter as input; the parameters provided in this request are: " + + StylerUtils.style(context.getRequestParameterMap())); + } + return encodedKey; + } + + public boolean isEventIdPresent(ExternalContext context) { + return StringUtils.hasText(findParameter(getEventIdArgumentName(), context.getRequestParameterMap())); + } + + public String extractEventId(ExternalContext context) throws FlowExecutorArgumentExtractionException { + String eventId = findParameter(getEventIdArgumentName(), context.getRequestParameterMap()); + if (!StringUtils.hasText(eventId)) { + throw new FlowExecutorArgumentExtractionException( + "Unable to extract the event id parameter: make sure the client provides the '" + + getEventIdArgumentName() + "' parameter as input along with the '" + + getFlowExecutionKeyArgumentName() + + "' parameter; the parameters provided in this request are: " + + StylerUtils.style(context.getRequestParameterMap())); + } + return eventId; + } + + /** + * Obtain a named parameter from the request parameters. This method will try + * to obtain a parameter value using the following algorithm: + *

      + *
    1. Try to get the parameter value using just the given logical + * name. This handles parameters of the form logicalName = value. + * For normal parameters, e.g. submitted using a hidden HTML form field, + * this will return the requested value.
    2. + *
    3. Try to obtain the parameter value from the parameter name, where the + * parameter name in the request is of the form + * logicalName_value = xyz with "_" being the configured + * delimiter. This deals with parameter values submitted using an HTML form + * submit button.
    4. + *
    5. If the value obtained in the previous step has a ".x" or ".y" + * suffix, remove that. This handles cases where the value was submitted + * using an HTML form image button. In this case the parameter in the request + * would actually be of the form logicalName_value.x = 123. + *
    6. + *
    + * @param logicalParameterName the logical name of the request + * parameter + * @param parameters the available parameter map + * @return the value of the parameter, or null if the + * parameter does not exist in given request + */ + protected String findParameter(String logicalParameterName, ParameterMap parameters) { + // first try to get it as a normal name=value parameter + String value = parameters.get(logicalParameterName); + if (value != null) { + return value; + } + // if no value yet, try to get it as a name_value=xyz parameter + String prefix = logicalParameterName + getParameterValueDelimiter(); + Iterator paramNames = parameters.asMap().keySet().iterator(); + while (paramNames.hasNext()) { + String paramName = (String)paramNames.next(); + if (paramName.startsWith(prefix)) { + String strValue = paramName.substring(prefix.length()); + // support images buttons, which would submit parameters as + // name_value.x=123 + if (strValue.endsWith(".x") || strValue.endsWith(".y")) { + strValue = strValue.substring(0, strValue.length() - 2); + } + return strValue; + } + } + // we couldn't find the parameter value + return null; + } + + public String createFlowDefinitionUrl(FlowDefinitionRedirect flowDefinitionRedirect, ExternalContext context) { + StringBuffer url = new StringBuffer(); + appendFlowExecutorPath(url, context); + url.append('?'); + appendQueryParameter(url, getFlowIdArgumentName(), flowDefinitionRedirect.getFlowDefinitionId()); + if (!flowDefinitionRedirect.getExecutionInput().isEmpty()) { + url.append('&'); + } + appendQueryParameters(url, flowDefinitionRedirect.getExecutionInput()); + return url.toString(); + } + + public String createFlowExecutionUrl(String flowExecutionKey, FlowExecutionContext flowExecution, + ExternalContext context) { + StringBuffer url = new StringBuffer(); + appendFlowExecutorPath(url, context); + url.append('?'); + appendQueryParameter(url, getFlowExecutionKeyArgumentName(), flowExecutionKey); + return url.toString(); + } + + public String createExternalUrl(ExternalRedirect redirect, String flowExecutionKey, ExternalContext context) { + StringBuffer externalUrl = new StringBuffer(); + externalUrl.append(makeRedirectUrlContextRelativeIfNecessary(redirect.getUrl(), context)); + if (flowExecutionKey != null) { + boolean first = redirect.getUrl().indexOf('?') < 0; + if (first) { + externalUrl.append('?'); + } + else { + externalUrl.append('&'); + } + appendQueryParameter(externalUrl, getFlowExecutionKeyArgumentName(), flowExecutionKey); + } + return externalUrl.toString(); + } + + // helpers + + /** + * Append the URL path to the flow executor capable of accepting new + * requests. + * @param url the url buffer to append to + * @param context the context of this request + */ + protected void appendFlowExecutorPath(StringBuffer url, ExternalContext context) { + url.append(context.getContextPath()); + url.append(context.getDispatcherPath()); + if (context.getRequestPathInfo() != null) { + url.append(context.getRequestPathInfo()); + } + } + + /** + * Append query parameters to the redirect URL. Stringifies, URL-encodes and + * formats model attributes as query parameters. + * @param url the StringBuffer to append the parameters to + * @param parameters Map that contains attributes + */ + protected void appendQueryParameters(StringBuffer url, Map parameters) { + Iterator entries = parameters.entrySet().iterator(); + while (entries.hasNext()) { + Map.Entry entry = (Map.Entry)entries.next(); + appendQueryParameter(url, entry.getKey(), entry.getValue()); + if (entries.hasNext()) { + url.append('&'); + } + } + } + + /** + * Appends a single query parameter to a URL. + * @param url the target url to append to + * @param key the parameter name + * @param value the parameter value + */ + protected void appendQueryParameter(StringBuffer url, Object key, Object value) { + String encodedKey = encodeValue(key); + String encodedValue = encodeValue(value); + url.append(encodedKey).append('=').append(encodedValue); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/support/RequestPathFlowExecutorArgumentHandler.java b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/RequestPathFlowExecutorArgumentHandler.java new file mode 100644 index 00000000..1f053dc7 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/RequestPathFlowExecutorArgumentHandler.java @@ -0,0 +1,166 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.support; + +import org.springframework.util.StringUtils; +import org.springframework.web.util.WebUtils; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.context.servlet.ServletExternalContext; +import org.springframework.webflow.execution.FlowExecutionContext; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; + +/** + * Flow executor argument handler that extracts arguments from the request path + * and exposes them in the URL path. + *

    + * This allows for REST-style URLs to launch flows in the general format: + * http://${host}/${context path}/${dispatcher path}/${flowId}. + *

    + * For example, the URL + * http://localhost/springair/reservation/booking would launch a + * new execution of the booking flow, assuming a context path of + * /springair and a servlet mapping of /reservation/*. + *

    + * This also allows for URLs to resume flow executions in the format: + * http://${host}/${context path}/${dispatcher path}/${key delimiter}/${flowExecutionKey}. + *

    + * For example, the URL + * http://localhost/springair/reservation/k/ABC123XYZ would + * resume flow execution "ABC123XYZ". + *

    + * Note: this implementation only works with ExternalContext + * implementations that return valid + * {@link ExternalContext#getRequestPathInfo()} such as the + * {@link ServletExternalContext}. Furthermore, it assumes that the controller + * handling flow requests is not identified using request path information. + * For instance, mapping the dispatcher to "*.html" in web.xml would work since + * in this case the flow controller will be identified as part of the dispatcher + * name (e.g. "flows.html"). Mapping the dispatcher to "/html/*" in web.xml + * will not work since that would require the flow controller to be identified + * by the extra request path information (e.g. "/html/flows"). + * + * @author Keith Donald + */ +public class RequestPathFlowExecutorArgumentHandler extends RequestParameterFlowExecutorArgumentHandler { + + /** + * URL path seperator ("/"). + */ + private static final char PATH_SEPARATOR_CHARACTER = '/'; + + /** + * Default value of the flow execution key delimiter ("k"). + */ + private static final String KEY_DELIMITER = "k"; + + /** + * The delimiter that when present in the requestPathInfo indicates the + * flowExecutionKey follows in the URL. Defaults to {@link #KEY_DELIMITER}. + */ + private String keyDelimiter = KEY_DELIMITER; + + /** + * Returns the key delimiter. Defaults to "k". + * @return the key delimiter + */ + public String getKeyDelimiter() { + return keyDelimiter; + } + + /** + * Sets the delimiter that when present in the requestPathInfo indicates the + * flowExecutionKey follows in the URL. Defaults to "k". + * @param keyDelimiter the key delimiter + * @see #extractFlowExecutionKey(ExternalContext) + */ + public void setKeyDelimiter(String keyDelimiter) { + this.keyDelimiter = keyDelimiter; + } + + public boolean isFlowIdPresent(ExternalContext context) { + String requestPathInfo = getRequestPathInfo(context); + boolean hasFileName = StringUtils.hasText(WebUtils.extractFilenameFromUrlPath(requestPathInfo)); + return hasFileName || super.isFlowIdPresent(context); + } + + public String extractFlowId(ExternalContext context) { + String requestPathInfo = getRequestPathInfo(context); + String extractedFilename = WebUtils.extractFilenameFromUrlPath(requestPathInfo); + return StringUtils.hasText(extractedFilename) ? extractedFilename : super.extractFlowId(context); + } + + public boolean isFlowExecutionKeyPresent(ExternalContext context) { + String requestPathInfo = getRequestPathInfo(context); + return requestPathInfo.startsWith(keyPath()) || super.isFlowExecutionKeyPresent(context); + } + + public String extractFlowExecutionKey(ExternalContext context) throws FlowExecutorArgumentExtractionException { + String requestPathInfo = getRequestPathInfo(context); + int index = requestPathInfo.indexOf(keyPath()); + if (index != -1) { + return requestPathInfo.substring(index + keyPath().length()); + } + else { + return super.extractFlowExecutionKey(context); + } + } + + public String createFlowDefinitionUrl(FlowDefinitionRedirect flowDefinitionRedirect, ExternalContext context) { + StringBuffer flowUrl = new StringBuffer(); + appendFlowExecutorPath(flowUrl, context); + flowUrl.append(PATH_SEPARATOR_CHARACTER); + flowUrl.append(flowDefinitionRedirect.getFlowDefinitionId()); + if (!flowDefinitionRedirect.getExecutionInput().isEmpty()) { + flowUrl.append('?'); + appendQueryParameters(flowUrl, flowDefinitionRedirect.getExecutionInput()); + } + return flowUrl.toString(); + } + + public String createFlowExecutionUrl(String flowExecutionKey, FlowExecutionContext flowExecution, + ExternalContext context) { + StringBuffer flowExecutionUrl = new StringBuffer(); + appendFlowExecutorPath(flowExecutionUrl, context); + flowExecutionUrl.append(PATH_SEPARATOR_CHARACTER); + flowExecutionUrl.append(keyDelimiter); + flowExecutionUrl.append(PATH_SEPARATOR_CHARACTER); + flowExecutionUrl.append(flowExecutionKey); + return flowExecutionUrl.toString(); + } + + // internal helpers + + protected void appendFlowExecutorPath(StringBuffer url, ExternalContext context) { + url.append(context.getContextPath()); + url.append(context.getDispatcherPath()); + } + + /** + * Returns the request path info for given external context. Never returns + * null, an empty string is returned instead. + */ + private String getRequestPathInfo(ExternalContext context) { + String requestPathInfo = context.getRequestPathInfo(); + return requestPathInfo != null ? requestPathInfo : ""; + } + + /** + * Returns the flow execution key path in the request path info, e.g. "/k/". + */ + private String keyPath() { + return PATH_SEPARATOR_CHARACTER + keyDelimiter + PATH_SEPARATOR_CHARACTER; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/executor/support/package.html b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/package.html new file mode 100644 index 00000000..e54673e3 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/executor/support/package.html @@ -0,0 +1,5 @@ + + +Flow executor implementation support; includes helpers for driving the execution of flows. + + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/MockExternalContext.java b/spring-webflow/src/main/java/org/springframework/webflow/test/MockExternalContext.java new file mode 100644 index 00000000..883f9b0c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/MockExternalContext.java @@ -0,0 +1,201 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.test; + +import java.util.HashMap; + +import org.springframework.binding.collection.SharedMapDecorator; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.LocalSharedAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.core.collection.ParameterMap; +import org.springframework.webflow.core.collection.SharedAttributeMap; + +/** + * Mock implementation of the {@link ExternalContext} interface. + * + * @see ExternalContext + * + * @author Keith Donald + */ +public class MockExternalContext implements ExternalContext { + + private String contextPath; + + private String dispatcherPath; + + private String requestPathInfo; + + private ParameterMap requestParameterMap = new MockParameterMap(); + + private MutableAttributeMap requestMap = new LocalAttributeMap(); + + private SharedAttributeMap sessionMap = new LocalSharedAttributeMap(new SharedMapDecorator(new HashMap())); + + private SharedAttributeMap globalSessionMap = sessionMap; + + private SharedAttributeMap applicationMap = new LocalSharedAttributeMap(new SharedMapDecorator(new HashMap())); + + /** + * Creates a mock external context with an empty request parameter map. + * Allows for bean style usage. + */ + public MockExternalContext() { + } + + /** + * Creates a mock external context with the specified parameters in the + * request parameter map. All other properties of the external context + * can be set using the appropriate setter. + * @param requestParameterMap the request parameters + */ + public MockExternalContext(ParameterMap requestParameterMap) { + if (requestParameterMap != null) { + this.requestParameterMap = requestParameterMap; + } + } + + // implementing external context + + public String getContextPath() { + return contextPath; + } + + public String getDispatcherPath() { + return dispatcherPath; + } + + public String getRequestPathInfo() { + return requestPathInfo; + } + + public ParameterMap getRequestParameterMap() { + return requestParameterMap; + } + + public MutableAttributeMap getRequestMap() { + return requestMap; + } + + public SharedAttributeMap getSessionMap() { + return sessionMap; + } + + public SharedAttributeMap getGlobalSessionMap() { + return globalSessionMap; + } + + public SharedAttributeMap getApplicationMap() { + return applicationMap; + } + + // helper setters + + /** + * Set the context path. + * @see ExternalContext#getContextPath() + */ + public void setContextPath(String contextPath) { + this.contextPath = contextPath; + } + + /** + * Set the dispatcher path. + * @see ExternalContext#getDispatcherPath() + */ + public void setDispatcherPath(String dispatcherPath) { + this.dispatcherPath = dispatcherPath; + } + + /** + * Set the request path info. + * @see ExternalContext#getRequestPathInfo() + */ + public void setRequestPathInfo(String requestPathInfo) { + this.requestPathInfo = requestPathInfo; + } + + /** + * Set the request parameter map. + * @see ExternalContext#getRequestParameterMap() + */ + public void setRequestParameterMap(ParameterMap requestParameterMap) { + this.requestParameterMap = requestParameterMap; + } + + /** + * Set the request attribute map. + * @see ExternalContext#getRequestMap() + */ + public void setRequestMap(MutableAttributeMap requestMap) { + this.requestMap = requestMap; + } + + /** + * Set the session attribute map. + * @see ExternalContext#getSessionMap() + */ + public void setSessionMap(SharedAttributeMap sessionMap) { + this.sessionMap = sessionMap; + } + + /** + * Set the global session attribute map. By default the session attribute + * map and the global session attribute map are one and the same. + * @see ExternalContext#getGlobalSessionMap() + */ + public void setGlobalSessionMap(SharedAttributeMap globalSessionMap) { + this.globalSessionMap = globalSessionMap; + } + + /** + * Set the application attribute map. + * @see ExternalContext#getApplicationMap() + */ + public void setApplicationMap(SharedAttributeMap applicationMap) { + this.applicationMap = applicationMap; + } + + // convenience helpers + + /** + * Returns the request parameter map as a {@link MockParameterMap} + * for convenient access in a unit test. + * @see #getRequestParameterMap() + */ + public MockParameterMap getMockRequestParameterMap() { + return (MockParameterMap)requestParameterMap; + } + + /** + * Puts a request parameter into the mock parameter map. + * @param parameterName the parameter name + * @param parameterValue the parameter value + */ + public void putRequestParameter(String parameterName, String parameterValue) { + getMockRequestParameterMap().put(parameterName, parameterValue); + } + + /** + * Puts a multi-valued request parameter into the mock parameter map. + * @param parameterName the parameter name + * @param parameterValues the parameter values + */ + public void putRequestParameter(String parameterName, String[] parameterValues) { + getMockRequestParameterMap().put(parameterName, parameterValues); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/MockFlowExecutionContext.java b/spring-webflow/src/main/java/org/springframework/webflow/test/MockFlowExecutionContext.java new file mode 100644 index 00000000..49332d0c --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/MockFlowExecutionContext.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.test; + +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.execution.FlowExecutionContext; +import org.springframework.webflow.execution.FlowSession; + +/** + * A stub implementation of the flow execution context interface. + * + * @see FlowExecutionContext + * + * @author Keith Donald + */ +public class MockFlowExecutionContext implements FlowExecutionContext { + + private FlowDefinition flow; + + private FlowSession activeSession; + + private MutableAttributeMap conversationScope = new LocalAttributeMap(); + + private MutableAttributeMap attributes = new LocalAttributeMap(); + + /** + * Creates a new mock flow execution context -- automatically installs a root + * flow definition and active flow session. + */ + public MockFlowExecutionContext() { + activeSession = new MockFlowSession(); + this.flow = activeSession.getDefinition(); + } + + /** + * Creates a new mock flow execution context for the specified root flow + * definition. + */ + public MockFlowExecutionContext(Flow rootFlow) { + this.flow = rootFlow; + activeSession = new MockFlowSession(rootFlow); + } + + public String getCaption() { + return "Mock flow execution context"; + } + + // implementing flow execution context + + public FlowDefinition getDefinition() { + return flow; + } + + public boolean isActive() { + return activeSession != null; + } + + public FlowSession getActiveSession() throws IllegalStateException { + if (activeSession == null) { + throw new IllegalStateException("No flow session is active"); + } + return activeSession; + } + + public MutableAttributeMap getConversationScope() { + return conversationScope; + } + + public AttributeMap getAttributes() { + return attributes; + } + + // mutators + + /** + * Sets the top-level flow definition. + */ + public void setFlow(Flow rootFlow) { + this.flow = rootFlow; + } + + /** + * Sets the mock session to be the active session. + */ + public void setActiveSession(FlowSession activeSession) { + this.activeSession = activeSession; + } + + /** + * Sets flow execution (conversational) scope. + */ + public void setConversationScope(MutableAttributeMap scope) { + this.conversationScope = scope; + } + + // convenience accessors + + /** + * Returns the mock active flow session. + */ + public MockFlowSession getMockActiveSession() { + return (MockFlowSession)activeSession; + } + + /** + * Returns the mutable execution attribute map. + * @return the execution attribute map + */ + public MutableAttributeMap getAttributeMap() { + return attributes; + } + + /** + * Puts a execution attribute into the context. + * @param attributeName the attribute name + * @param attributeValue the attribute value + */ + public void putAttribute(String attributeName, Object attributeValue) { + attributes.put(attributeName, attributeValue); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/MockFlowServiceLocator.java b/spring-webflow/src/main/java/org/springframework/webflow/test/MockFlowServiceLocator.java new file mode 100644 index 00000000..46300528 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/MockFlowServiceLocator.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.test; + +import org.springframework.beans.factory.config.ConfigurableBeanFactory; +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistryImpl; +import org.springframework.webflow.definition.registry.StaticFlowDefinitionHolder; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.builder.DefaultFlowServiceLocator; + +/** + * A stub flow service locator implementation suitable for a test environment. + *

    + * Allows programmatic registration of subflows needed by a flow execution being + * tested, see {@link #registerSubflow(Flow)}. Subflows registered are + * typically stubs that verify parent flow input and output scenarios. + *

    + * Also supports programmatic registration of additional custom services needed + * by a flow (such as Actions) managed in a backing Spring + * {@link ConfigurableBeanFactory}. See the + * {@link #registerBean(String, Object)} method. Beans registered are typically + * mocks or stubs of business services invoked by the flow. + * + * @author Keith Donald + */ +public class MockFlowServiceLocator extends DefaultFlowServiceLocator { + + /** + * Creates a new mock flow service locator. + */ + public MockFlowServiceLocator() { + super(new FlowDefinitionRegistryImpl(), new StaticListableBeanFactory()); + } + + /** + * Register a subflow definition in the backing flow registry, typically to + * support a flow execution test. For test scenarios, the subflow is often a + * stub used to verify parent flow input and output mapping behavior. + * @param subflow the subflow + */ + public void registerSubflow(Flow subflow) { + getSubflowRegistry().registerFlowDefinition(new StaticFlowDefinitionHolder(subflow)); + } + + /** + * Register a bean in the backing bean factory, typically to support a flow + * execution test. For test scenarios, if the bean is a service invoked by a + * bean invoking action it is often a stub or dynamic mock implementation of + * the service's business interface. + * @param beanName the bean name + * @param bean the singleton instance + */ + public void registerBean(String beanName, Object bean) { + ((StaticListableBeanFactory)getBeanFactory()).addBean(beanName, bean); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/MockFlowSession.java b/spring-webflow/src/main/java/org/springframework/webflow/test/MockFlowSession.java new file mode 100644 index 00000000..babfdeb3 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/MockFlowSession.java @@ -0,0 +1,164 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.test; + +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.State; +import org.springframework.webflow.engine.ViewState; +import org.springframework.webflow.execution.FlowSession; +import org.springframework.webflow.execution.FlowSessionStatus; + +/** + * Mock implementation of the {@link FlowSession} interface. + * + * @see FlowSession + * + * @author Erwin Vervaet + */ +public class MockFlowSession implements FlowSession { + + private Flow definition; + + private State state; + + private FlowSessionStatus status = FlowSessionStatus.CREATED; + + private MutableAttributeMap scope = new LocalAttributeMap(); + + private MutableAttributeMap flashMap = new LocalAttributeMap(); + + private FlowSession parent; + + /** + * Creates a new mock flow session that sets a flow with id "mockFlow" as + * the 'active flow' in state "mockState". This session marks itself active. + */ + public MockFlowSession() { + setDefinition(new Flow("mockFlow")); + State state = new ViewState(definition, "mockState"); + setStatus(FlowSessionStatus.ACTIVE); + setState(state); + } + + /** + * Creates a new mock session in a created state for the specified flow + * definition. + */ + public MockFlowSession(Flow flow) { + setDefinition(flow); + } + + /** + * Creates a new mock session in {@link FlowSessionStatus#CREATED} state + * for the specified flow definition. + * @param flow the flow definition for the session + * @param input initial contents of 'flow scope' + */ + public MockFlowSession(Flow flow, AttributeMap input) { + setDefinition(flow); + scope.putAll(input); + } + + // implementing FlowSession + + public FlowDefinition getDefinition() { + return definition; + } + + public StateDefinition getState() { + return state; + } + + public FlowSessionStatus getStatus() { + return status; + } + + public MutableAttributeMap getScope() { + return scope; + } + + public MutableAttributeMap getFlashMap() { + return flashMap; + } + + public FlowSession getParent() { + return parent; + } + + public boolean isRoot() { + return parent == null; + } + + // mutators + + /** + * Set the flow associated with this flow session. + */ + public void setDefinition(Flow flow) { + this.definition = flow; + } + + /** + * Set the currently active state. + */ + public void setState(State state) { + this.state = state; + } + + /** + * Set the status of this flow session. + */ + public void setStatus(FlowSessionStatus status) { + this.status = status; + } + + /** + * Set the scope data maintained by this flow session. This will be the flow + * scope data of the ongoing flow execution. + */ + public void setScope(MutableAttributeMap scope) { + this.scope = scope; + } + + /** + * Set the parent flow session of this flow session in the ongoing flow + * execution. + */ + public void setParent(FlowSession parent) { + this.parent = parent; + } + + // conveniece accessors + + /** + * Returns the flow definition of this session. + */ + public Flow getDefinitionInternal() { + return definition; + } + + /** + * Returns the current state of this session. + */ + public State getStateInternal() { + return state; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/MockParameterMap.java b/spring-webflow/src/main/java/org/springframework/webflow/test/MockParameterMap.java new file mode 100644 index 00000000..adfe6f01 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/MockParameterMap.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.test; + +import java.util.HashMap; + +import org.springframework.webflow.core.collection.LocalParameterMap; +import org.springframework.webflow.core.collection.ParameterMap; + +/** + * A extension of parameter map that allows for mutation of parameters. Useful + * as a stub for testing. + * + * @see ParameterMap + * + * @author Keith Donald + */ +public class MockParameterMap extends LocalParameterMap { + + /** + * Creates a new parameter map, initially empty. + */ + public MockParameterMap() { + super(new HashMap()); + } + + /** + * Add a new parameter to this map. + * @param parameterName the parameter name + * @param parameterValue the parameter value + * @return this, to support call chaining + */ + public MockParameterMap put(String parameterName, String parameterValue) { + getMapInternal().put(parameterName, parameterValue); + return this; + } + + /** + * Add a new multi-valued parameter to this map. + * @param parameterName the parameter name + * @param parameterValues the parameter values + * @return this, to support call chaining + */ + public MockParameterMap put(String parameterName, String[] parameterValues) { + getMapInternal().put(parameterName, parameterValues); + return this; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/MockRequestContext.java b/spring-webflow/src/main/java/org/springframework/webflow/test/MockRequestContext.java new file mode 100644 index 00000000..f6f4a815 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/MockRequestContext.java @@ -0,0 +1,253 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.test; + +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.core.collection.ParameterMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; +import org.springframework.webflow.definition.TransitionDefinition; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecutionContext; +import org.springframework.webflow.execution.FlowSession; +import org.springframework.webflow.execution.RequestContext; + +/** + * Mock implementation of the RequestContext interface to + * facilitate standalone flow artifact (e.g. action) unit tests. + * + * @see org.springframework.webflow.execution.RequestContext + * @see org.springframework.webflow.execution.Action + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class MockRequestContext implements RequestContext { + + private FlowExecutionContext flowExecutionContext = new MockFlowExecutionContext(); + + private ExternalContext externalContext = new MockExternalContext(); + + private MutableAttributeMap requestScope = new LocalAttributeMap(); + + private Event lastEvent; + + private Transition lastTransition; + + private MutableAttributeMap attributes = new LocalAttributeMap(); + + /** + * Creates a new mock request context with the following defaults: + *

      + *
    • A flow execution context with a active session of flow "mockFlow" in + * state "mockState". + *
    • A mock external context with no request parameters set. + *
    + * To add request parameters to this request, use the + * {@link #putRequestParameter(String, String)} method. + */ + public MockRequestContext() { + } + + /** + * Creates a new mock request context with the following defaults: + *
      + *
    • A flow execution context with an active session for the specified flow. + *
    • A mock external context with no request parameters set. + *
    + * To add request parameters to this request, use the + * {@link #putRequestParameter(String, String)} method. + */ + public MockRequestContext(Flow flow) { + flowExecutionContext = new MockFlowExecutionContext(flow); + } + + /** + * Creates a new mock request context with the following defaults: + *
      + *
    • A flow execution context with a active session of flow "mockFlow" in + * state "mockState". + *
    • A mock external context with the provided parameters set. + *
    + */ + public MockRequestContext(ParameterMap requestParameterMap) { + externalContext = new MockExternalContext(requestParameterMap); + } + + // implementing RequestContext + + public FlowDefinition getActiveFlow() { + return getFlowExecutionContext().getActiveSession().getDefinition(); + } + + public StateDefinition getCurrentState() { + return getFlowExecutionContext().getActiveSession().getState(); + } + + public MutableAttributeMap getRequestScope() { + return requestScope; + } + + public MutableAttributeMap getFlashScope() { + return getMockFlowExecutionContext().getActiveSession().getFlashMap(); + } + + public MutableAttributeMap getFlowScope() { + return getFlowExecutionContext().getActiveSession().getScope(); + } + + public MutableAttributeMap getConversationScope() { + return getMockFlowExecutionContext().getConversationScope(); + } + + public ParameterMap getRequestParameters() { + return externalContext.getRequestParameterMap(); + } + + public ExternalContext getExternalContext() { + return externalContext; + } + + public FlowExecutionContext getFlowExecutionContext() { + return flowExecutionContext; + } + + public Event getLastEvent() { + return lastEvent; + } + + public TransitionDefinition getLastTransition() { + return lastTransition; + } + + public AttributeMap getAttributes() { + return attributes; + } + + public void setAttributes(AttributeMap attributes) { + this.attributes.replaceWith(attributes); + } + + public AttributeMap getModel() { + return getConversationScope().union(getFlowScope()).union(getFlashScope()).union(getRequestScope()); + } + + // mutators + + /** + * Sets the active flow session of the executing flow associated with this + * request. This will influence {@link #getActiveFlow()} and {@link #getCurrentState()}, + * as well as {@link #getFlowScope()} and {@link #getFlashScope()}. + */ + public void setActiveSession(FlowSession flowSession) { + getMockFlowExecutionContext().setActiveSession(flowSession); + } + + /** + * Sets the external context. + */ + public void setExternalContext(ExternalContext externalContext) { + this.externalContext = externalContext; + } + + /** + * Sets the flow execution context. + */ + public void setFlowExecutionContext(FlowExecutionContext flowExecutionContext) { + this.flowExecutionContext = flowExecutionContext; + } + + /** + * Set the last event that occured in this request context. + * @param lastEvent the event to set + */ + public void setLastEvent(Event lastEvent) { + this.lastEvent = lastEvent; + } + + /** + * Set the last transition that executed in this request context. + * @param lastTransition the last transition to set + */ + public void setLastTransition(Transition lastTransition) { + this.lastTransition = lastTransition; + } + + /** + * Set a request context attribute. + * @param attributeName the attribute name + * @param attributeValue the attribute value + */ + public void setAttribute(String attributeName, Object attributeValue) { + attributes.put(attributeName, attributeValue); + } + + /** + * Remove a request context attribute. + * @param attributeName the attribute name + */ + public void removeAttribute(String attributeName) { + attributes.remove(attributeName); + } + + // convenience accessors + + /** + * Returns the contained mutable context {@link AttributeMap attribute map} + * allowing setting of mock context attributes. + * @return the attribute map + */ + public MutableAttributeMap getAttributeMap() { + return attributes; + } + + /** + * Returns the flow execution context as a {@link MockFlowExecutionContext}. + */ + public MockFlowExecutionContext getMockFlowExecutionContext() { + return (MockFlowExecutionContext)flowExecutionContext; + } + + /** + * Returns the external context as a {@link MockExternalContext}. + */ + public MockExternalContext getMockExternalContext() { + return (MockExternalContext)externalContext; + } + + /** + * Adds a request parameter to the configured external context. + * @param parameterName the parameter name + * @param parameterValue the parameter value + */ + public void putRequestParameter(String parameterName, String parameterValue) { + getMockExternalContext().putRequestParameter(parameterName, parameterValue); + } + + /** + * Adds a multi-valued request parameter to the configured external context. + * @param parameterName the parameter name + * @param parameterValues the parameter values + */ + public void putRequestParameter(String parameterName, String[] parameterValues) { + getMockExternalContext().putRequestParameter(parameterName, parameterValues); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/MockRequestControlContext.java b/spring-webflow/src/main/java/org/springframework/webflow/test/MockRequestControlContext.java new file mode 100644 index 00000000..c705985d --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/MockRequestControlContext.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.test; + +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.RequestControlContext; +import org.springframework.webflow.engine.State; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.engine.TransitionableState; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowSession; +import org.springframework.webflow.execution.FlowSessionStatus; +import org.springframework.webflow.execution.ViewSelection; + +/** + * Mock implementation of the {@link RequestControlContext} interface to + * facilitate standalone Flow and State unit tests. + * + * @see org.springframework.webflow.execution.RequestContext + * @see org.springframework.webflow.execution.FlowSession + * @see org.springframework.webflow.engine.State + * + * @author Keith Donald + */ +public class MockRequestControlContext extends MockRequestContext implements RequestControlContext { + + /** + * Creates a new mock request control context for controlling a mock execution of the + * provided flow definition. + */ + public MockRequestControlContext(Flow rootFlow) { + super(rootFlow); + } + + // implementing RequestControlContext + + public void setCurrentState(State state) { + State previousState = (State)getCurrentState(); + getMockFlowExecutionContext().getMockActiveSession().setState(state); + if (previousState == null) { + getMockFlowExecutionContext().getMockActiveSession().setStatus(FlowSessionStatus.ACTIVE); + } + } + + public ViewSelection start(Flow flow, MutableAttributeMap input) throws IllegalStateException { + getMockFlowExecutionContext().setActiveSession(new MockFlowSession(flow, input)); + getMockFlowExecutionContext().getMockActiveSession().setStatus(FlowSessionStatus.STARTING); + ViewSelection selectedView = flow.start(this, input); + return selectedView; + } + + public ViewSelection signalEvent(Event event) { + setLastEvent(event); + ViewSelection selectedView = ((Flow)getActiveFlow()).onEvent(this); + return selectedView; + } + + public FlowSession endActiveFlowSession(MutableAttributeMap output) throws IllegalStateException { + MockFlowSession endingSession = getMockFlowExecutionContext().getMockActiveSession(); + endingSession.getDefinitionInternal().end(this, output); + endingSession.setStatus(FlowSessionStatus.ENDED); + getMockFlowExecutionContext().setActiveSession(null); + return endingSession; + } + + public ViewSelection execute(Transition transition) { + return transition.execute((TransitionableState)getCurrentState(), this); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractExternalizedFlowExecutionTests.java b/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractExternalizedFlowExecutionTests.java new file mode 100644 index 00000000..ed34faac --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractExternalizedFlowExecutionTests.java @@ -0,0 +1,206 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.test.execution; + +import java.io.File; + +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.registry.FlowDefinitionResource; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.builder.FlowAssembler; +import org.springframework.webflow.engine.builder.FlowBuilder; +import org.springframework.webflow.engine.builder.FlowServiceLocator; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.execution.FlowExecutionListener; +import org.springframework.webflow.execution.factory.StaticFlowExecutionListenerLoader; +import org.springframework.webflow.test.MockFlowServiceLocator; + +/** + * Base class for flow integration tests that verify an externalized flow + * definition executes as expected. Supports caching of the flow definition + * built from an externalized resource to speed up test execution. + * + * @author Keith Donald + */ +public abstract class AbstractExternalizedFlowExecutionTests extends AbstractFlowExecutionTests { + + /** + * The cached flow definition. + */ + private static FlowDefinition cachedFlowDefinition; + + /** + * The flag indicating if the flow definition built from an externalized + * resource as part of this test should be cached. + */ + private boolean cacheFlowDefinition = false; + + /** + * Internal helper that return the flow execution factory used by the + * test cast to a {@link FlowExecutionImplFactory}. + */ + private FlowExecutionImplFactory getFlowExecutionImplFactory() { + return (FlowExecutionImplFactory)getFlowExecutionFactory(); + } + + /** + * Returns if flow definition caching is turned on. + */ + protected boolean isCacheFlowDefinition() { + return cacheFlowDefinition; + } + + /** + * Sets the flag indicating if the flow definition built from an + * externalized resource as part of this test should be cached. + * Default is false. + */ + protected void setCacheFlowDefinition(boolean cacheFlowDefinition) { + this.cacheFlowDefinition = cacheFlowDefinition; + } + + /** + * Sets system attributes to be associated with the flow execution the next + * time one is {@link #startFlow() started} by this test. Useful for + * assigning attributes that influence flow execution behavior. + * @param executionAttributes the system attributes to assign + */ + protected void setFlowExecutionAttributes(AttributeMap executionAttributes) { + getFlowExecutionImplFactory().setExecutionAttributes(executionAttributes); + } + + /** + * Set the listener to be attached to the flow execution the next time one + * is {@link #startFlow() started} by this test. Useful for attaching a + * listener that does test assertions during the execution of the flow. + * @param executionListener the listener to attach + */ + protected void setFlowExecutionListener(FlowExecutionListener executionListener) { + getFlowExecutionImplFactory().setExecutionListenerLoader( + new StaticFlowExecutionListenerLoader(executionListener)); + } + + protected final FlowDefinition getFlowDefinition() { + if (isCacheFlowDefinition() && cachedFlowDefinition != null) { + return cachedFlowDefinition; + } + FlowServiceLocator flowServiceLocator = createFlowServiceLocator(); + Flow flow = createFlow(getFlowDefinitionResource(), flowServiceLocator); + if (isCacheFlowDefinition()) { + cachedFlowDefinition = flow; + } + return flow; + } + + /** + * Returns the flow artifact factory to use during flow definition + * construction time for accessing externally managed flow artifacts such as + * actions and flows to be used as subflows. + *

    + * This implementation just creates a {@link MockFlowServiceLocator} and + * populates it with services by calling {@link #registerMockServices(MockFlowServiceLocator)}. + * @return the flow artifact factory + */ + protected FlowServiceLocator createFlowServiceLocator() { + MockFlowServiceLocator serviceLocator = new MockFlowServiceLocator(); + registerMockServices(serviceLocator); + return serviceLocator; + } + + /** + * Template method called by {@link #createFlowServiceLocator()} to allow + * registration of mock implementations of services needed to test the flow + * execution. Useful when testing flow definitions in execution in isolation + * from flows and middle-tier services. Subclasses may override. + * @param serviceRegistry the mock service registry (and locator) + */ + protected void registerMockServices(MockFlowServiceLocator serviceRegistry) { + } + + /** + * Factory method to assemble another flow definition from a resource. + * Called by {@link #getFlowDefinition()} to create the "main" flow to test. + * May also be called by subclasses to create subflow definitions whose + * executions should also be exercised by this test. + * @param resource the flow definition resource + * @return the built flow definition, ready for execution + * @see #createFlowBuilder(Resource, FlowServiceLocator) + */ + protected final Flow createFlow(FlowDefinitionResource resource, FlowServiceLocator serviceLocator) { + FlowBuilder builder = createFlowBuilder(resource.getLocation(), serviceLocator); + FlowAssembler assembler = new FlowAssembler(resource.getId(), resource.getAttributes(), builder); + return assembler.assembleFlow(); + } + + /** + * Returns the pointer to the resource that houses the definition of the + * flow to be tested. Subclasses must implement. + *

    + * Example usage: + *

    +	 *     protected FlowDefinitionResource getFlowDefinitionResource() {
    +	 * 	      return createFlowDefinitionResource("/WEB-INF/flows/order-flow.xml");
    +	 *     }
    +	 * 
    + * @return the flow definition resource + */ + protected abstract FlowDefinitionResource getFlowDefinitionResource(); + + /** + * Factory method to create the builder that will build the flow whose + * execution will be tested. Subclasses must override. + * @param resource the externalized flow definition resource location + * @param serviceLocator the flow service locator + * @return the flow builder that will build the flow to be tested + */ + protected abstract FlowBuilder createFlowBuilder(Resource resource, FlowServiceLocator serviceLocator); + + /** + * Convenient factory method that creates a {@link FlowDefinitionResource} + * from a file path. Typically called by subclasses overriding + * {@link #getFlowDefinitionResource()}. + * @param filePath the full path to the externalized flow definition file + * @return the flow definition resource + */ + protected final FlowDefinitionResource createFlowDefinitionResource(String filePath) { + return createFlowDefinitionResource(new File(filePath)); + } + + /** + * Convenient factory method that creates a {@link FlowDefinitionResource} + * from a file in a directory. Typically called by subclasses overriding + * {@link #getFlowDefinitionResource()}. + * @param fileDirectory the directory containing the file + * @param fileName the short file name + * @return the flow definition resource pointing to the file + */ + protected final FlowDefinitionResource createFlowDefinitionResource(String fileDirectory, String fileName) { + return createFlowDefinitionResource(new File(fileDirectory, fileName)); + } + + /** + * Convenient factory method that creates a {@link FlowDefinitionResource} + * from a file. + * @param file the file + * @return the flow definition resource + */ + protected FlowDefinitionResource createFlowDefinitionResource(File file) { + return new FlowDefinitionResource(new FileSystemResource(file)); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractFlowExecutionTests.java b/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractFlowExecutionTests.java new file mode 100644 index 00000000..8f21fe4e --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractFlowExecutionTests.java @@ -0,0 +1,573 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.test.execution; + +import java.util.Collection; +import java.util.Map; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.core.style.StylerUtils; +import org.springframework.util.Assert; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.core.collection.ParameterMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.FlowExecutionFactory; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.execution.support.ExternalRedirect; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; +import org.springframework.webflow.execution.support.FlowExecutionRedirect; +import org.springframework.webflow.test.MockExternalContext; + +/** + * Base class for integration tests that verify a flow executes as expected. + * Flow execution tests captured by subclasses should test that a flow responds + * to all supported transition criteria correctly, transitioning to the correct + * states and producing the expected results on the occurence of possible + * external (user) events. + *

    + * More specifically, a typical flow execution test case will test: + *

      + *
    • That the flow execution starts as expected given a request from an + * external context containing potential input attributes (see the + * {@link #startFlow(MutableAttributeMap, ExternalContext)} variants). + *
    • That given the set of supported state transition criteria a state + * executes the appropriate transition when a matching event is signaled (with + * potential input request parameters, see the + * {@link #signalEvent(String, ExternalContext)} variants). A test case should + * be coded for each logical event that can occur, where an event drives a + * possible path through the flow. The goal should be to exercise all possible + * paths of the flow. Use a test coverage tool like Clover or Emma to assist + * with measuring your test's effectiveness. + *
    • That given a transition that leads to an interactive state type (a view + * state or an end state) that the view selection returned to the client matches + * what was expected and the current state of the flow matches what is expected. + *
    + *

    + * A flow execution test can effectively automate and validate the orchestration + * required to drive an end-to-end business task that spans several steps + * involving the user to complete. Such tests are a good way to test your system + * top-down starting at the web-tier and pushing through all the way to the DB + * without having to deploy to a servlet or portlet container. In addition, they + * can be used to effectively test a flow's execution (the web layer) + * standalone, typically with a mock service layer. Both styles of testing are + * valuable and supported. + * + * @author Keith Donald + */ +public abstract class AbstractFlowExecutionTests extends TestCase { + + /** + * The factory that will create the flow execution to test. + */ + private FlowExecutionFactory flowExecutionFactory; + + /** + * The expression parser for parsing evaluatable model attribute + * expressions. + */ + private ExpressionParser expressionParser = DefaultExpressionParserFactory.getExpressionParser(); + + /** + * The flow execution running the flow when the test is active (runtime + * object). + */ + private FlowExecution flowExecution; + + /** + * Set the expression parser responsible for parsing expression strings into + * evaluatable expression objects. + */ + public void setExpressionParser(ExpressionParser expressionParser) { + Assert.notNull(expressionParser, "The expression parser is required"); + this.expressionParser = expressionParser; + } + + /** + * Gets the factory that will create the flow execution to test. This method + * will create the factory if it is not already set. + * @return the flow execution factory + * @see #createFlowExecutionFactory() + */ + protected FlowExecutionFactory getFlowExecutionFactory() { + if (flowExecutionFactory == null) { + flowExecutionFactory = createFlowExecutionFactory(); + } + return flowExecutionFactory; + } + + /** + * Creates an ExternalContext instance. Defaults to using {@link MockExternalContext}. + * Subclasses can override if they which to use another external context + * implementation. + * @param requestParameters request parameters to put into the + * external context (optional) + * @return a new ExternalContext instance + */ + protected ExternalContext createExternalContext(ParameterMap requestParameters) { + return new MockExternalContext(requestParameters); + } + + /** + * Start the flow execution to be tested. + *

    + * Convenience operation that starts the execution with: + *

      + *
    • no input attributes + *
    • an empty {@link ExternalContext} with no environmental request + * parameters set + *
    + * @return the view selection made as a result of starting the flow + * (returned when the first interactive state (a view state or end state) is + * entered) + * @throws FlowExecutionException if an exception was thrown while starting + * the flow execution + */ + protected ViewSelection startFlow() throws FlowExecutionException { + return startFlow(null, createExternalContext(null)); + } + + /** + * Start the flow execution to be tested. + *

    + * Convenience operation that starts the execution with: + *

      + *
    • the specified input attributes, eligible for mapping by the root + * flow + *
    • an empty {@link ExternalContext} with no environmental request + * parameters set + *
    + * @param input the flow execution input attributes eligible for mapping by + * the root flow + * @return the view selection made as a result of starting the flow + * (returned when the first interactive state (a view state or end state) is + * entered) + * @throws FlowExecutionException if an exception was thrown while starting + * the flow execution + */ + protected ViewSelection startFlow(MutableAttributeMap input) throws FlowExecutionException { + return startFlow(input, createExternalContext(null)); + } + + /** + * Start the flow execution to be tested. + *

    + * This is the most flexible of the start methods. It allows you to specify: + *

      + *
    1. a map of input attributes to pass to the flow execution, eligible + * for mapping by the root flow definition + *
    2. an external context that provides the flow execution being tested + * access to the calling environment for this request + *
    + * @param input the flow execution input attributes eligible for mapping by + * the root flow + * @param context the external context providing information about the + * caller's environment, used by the flow execution during the start + * operation + * @return the view selection made as a result of starting the flow + * (returned when the first interactive state (a view state or end state) is + * entered) + * @throws FlowExecutionException if an exception was thrown while starting + * the flow execution + */ + protected ViewSelection startFlow(MutableAttributeMap input, ExternalContext context) throws FlowExecutionException { + flowExecution = getFlowExecutionFactory().createFlowExecution(getFlowDefinition()); + return flowExecution.start(input, context); + } + + /** + * Signal an occurence of an event in the current state of the flow + * execution being tested. + * @param eventId the event that occured + * @throws FlowExecutionException if an exception was thrown within a state + * of the resumed flow execution during event processing + */ + protected ViewSelection signalEvent(String eventId) throws FlowExecutionException { + return signalEvent(eventId, createExternalContext(null)); + } + + /** + * Signal an occurence of an event in the current state of the flow + * execution being tested. + * @param eventId the event that occured + * @param requestParameters request parameters needed by the flow execution + * to complete event processing + * @throws FlowExecutionException if an exception was thrown within a state + * of the resumed flow execution during event processing + */ + protected ViewSelection signalEvent(String eventId, ParameterMap requestParameters) throws FlowExecutionException { + return signalEvent(eventId, createExternalContext(requestParameters)); + } + + /** + * Signal an occurence of an event in the current state of the flow + * execution being tested. + *

    + * Note: signaling an event will cause state transitions to occur in a chain + * until control is returned to the caller. Control is returned once an + * "interactive" state type is entered: either a view state when the flow is + * paused or an end state when the flow terminates. Action states are + * executed without returning control, as their result always triggers + * another state transition, executed internally. Action states can also be + * executed in a chain like fashion (e.g. action state 1 (result), action + * state 2 (result), action state 3 (result), view state ). + *

    + * If you wish to verify expected behavior on each state transition (and not + * just when the view state triggers return of control back to the client), + * you have a few options: + *

    + * First, you may implement standalone unit tests for your + * {@link org.springframework.webflow.execution.Action} implementations. + * There you can verify that an Action executes its logic properly in + * isolation. When you do this, you may mock or stub out services the Action + * implementation needs that are expensive to initialize. You can also + * verify there that the action puts everything in the flow or request scope + * it was expected to (to meet its contract with the view it is prepping for + * display, for example). + *

    + * Second, you can attach one or more FlowExecutionListeners to the flow + * execution at start time within your test code, which will allow you to + * receive a callback on each state transition (among other points). It is + * recommended you extend + * {@link org.springframework.webflow.execution.FlowExecutionListenerAdapter} + * and only override the callback methods you are interested in. + * @param eventId the event that occured + * @param context the external context providing information about the + * caller's environment, used by the flow execution during the signal event + * operation + * @return the view selection that was made, returned once control is + * returned to the client (occurs when the flow enters a view state, or an + * end state) + * @throws FlowExecutionException if an exception was thrown within a state + * of the resumed flow execution during event processing + */ + protected ViewSelection signalEvent(String eventId, ExternalContext context) throws FlowExecutionException { + Assert.state(flowExecution != null, "The flow execution to test is [null]; " + + "you must start the flow execution before you can signal an event against it!"); + return flowExecution.signalEvent(eventId, context); + } + + /** + * Refresh the flow execution being tested, asking the current view state to + * make a "refresh" view selection. This is idempotent operation that may be + * safely called on an active but currently paused execution. Used to + * simulate a browser flow execution redirect. + * @return the current view selection for this flow execution + * @throws FlowExecutionException if an exception was thrown during refresh + */ + protected ViewSelection refresh() throws FlowExecutionException { + return refresh(createExternalContext(null)); + } + + /** + * Refresh the flow execution being tested, asking the current view state + * state to make a "refresh" view selection. This is idempotent operation + * that may be safely called on an active but currently paused execution. + * Used to simulate a browser flow execution redirect. + * @param context the external context providing information about the + * caller's environment, used by the flow execution during the refresh + * operation + * @return the current view selection for this flow execution + * @throws FlowExecutionException if an exception was thrown during refresh + */ + protected ViewSelection refresh(ExternalContext context) throws FlowExecutionException { + Assert.state(flowExecution != null, + "The flow execution to test is [null]; you must start the flow execution before you can refresh it!"); + return flowExecution.refresh(context); + } + + // convenience accessors + + /** + * Returns the flow execution being tested. + * @return the flow execution + * @throws IllegalStateException the execution has not been started + */ + protected FlowExecution getFlowExecution() throws IllegalStateException { + Assert.state(flowExecution != null, + "The flow execution to test is [null]; you must start the flow execution before you can query it!"); + return flowExecution; + } + + /** + * Returns the attribute in conversation scope. Conversation-scoped + * attributes are shared by all flow sessions. + * @param attributeName the name of the attribute + * @return the attribute value + */ + protected Object getConversationAttribute(String attributeName) { + return getFlowExecution().getConversationScope().get(attributeName); + } + + /** + * Returns the required attribute in conversation scope; asserts the + * attribute is present. Conversation-scoped attributes are shared by all + * flow sessions. + * @param attributeName the name of the attribute + * @return the attribute value + * @throws IllegalStateException if the attribute was not present + */ + protected Object getRequiredConversationAttribute(String attributeName) throws IllegalStateException { + return getFlowExecution().getConversationScope().getRequired(attributeName); + } + + /** + * Returns the required attribute in conversation scope; asserts the + * attribute is present and of the required type. Conversation-scoped + * attributes are shared by all flow sessions. + * @param attributeName the name of the attribute + * @return the attribute value + * @throws IllegalStateException if the attribute was not present or not of + * the required type + */ + protected Object getRequiredConversationAttribute(String attributeName, Class requiredType) + throws IllegalStateException { + return getFlowExecution().getConversationScope().getRequired(attributeName, requiredType); + } + + /** + * Returns the attribute in flow scope. Flow-scoped attributes are local to + * the active flow session. + * @param attributeName the name of the attribute + * @return the attribute value + */ + protected Object getFlowAttribute(String attributeName) { + return getFlowExecution().getActiveSession().getScope().get(attributeName); + } + + /** + * Returns the required attribute in flow scope; asserts the attribute is + * present. Flow-scoped attributes are local to the active flow session. + * @param attributeName the name of the attribute + * @return the attribute value + * @throws IllegalStateException if the attribute was not present + */ + protected Object getRequiredFlowAttribute(String attributeName) throws IllegalStateException { + return getFlowExecution().getActiveSession().getScope().getRequired(attributeName); + } + + /** + * Returns the required attribute in flow scope; asserts the attribute is + * present and of the correct type. Flow-scoped attributes are local to the + * active flow session. + * @param attributeName the name of the attribute + * @return the attribute value + * @throws IllegalStateException if the attribute was not present or was of + * the wrong type + */ + protected Object getRequiredFlowAttribute(String attributeName, Class requiredType) throws IllegalStateException { + return getFlowExecution().getActiveSession().getScope().getRequired(attributeName, requiredType); + } + + // assert helpers + + /** + * Assert that the active flow session is for the flow with the provided id. + * @param expectedActiveFlowId the flow id that should have a session active + * in the tested flow execution + */ + protected void assertActiveFlowEquals(String expectedActiveFlowId) { + assertEquals("The active flow id '" + getFlowExecution().getActiveSession().getDefinition().getId() + + "' does not equal the expected active flow id '" + expectedActiveFlowId + "'", expectedActiveFlowId, + getFlowExecution().getActiveSession().getDefinition().getId()); + } + + /** + * Assert that the entire flow execution is active; that is, it has not + * ended and has been started. + */ + protected void assertFlowExecutionActive() { + assertTrue("The flow execution is not active but it should be", getFlowExecution().isActive()); + } + + /** + * Assert that the entire flow execution has ended; that is, it is no longer + * active. + */ + protected void assertFlowExecutionEnded() { + assertTrue("The flow execution is still active but it should have ended", !getFlowExecution().isActive()); + } + + /** + * Assert that the current state of the flow execution equals the provided + * state id. + * @param expectedCurrentStateId the expected current state + */ + protected void assertCurrentStateEquals(String expectedCurrentStateId) { + assertEquals("The current state '" + getFlowExecution().getActiveSession().getState().getId() + + "' does not equal the expected state '" + expectedCurrentStateId + "'", expectedCurrentStateId, + getFlowExecution().getActiveSession().getState().getId()); + } + + /** + * Assert that the view name equals the provided value. + * @param expectedViewName the expected name + * @param viewSelection the selected view + */ + protected void assertViewNameEquals(String expectedViewName, ApplicationView viewSelection) { + assertEquals("The view name is wrong:", expectedViewName, viewSelection.getViewName()); + } + + /** + * Assert that the selected view contains the specified model attribute with + * the provided expected value. + * @param expectedValue the expected value + * @param attributeName the attribute name (can be an expression) + * @param viewSelection the selected view with a model attribute map to + * assert against + */ + protected void assertModelAttributeEquals(Object expectedValue, String attributeName, ApplicationView viewSelection) { + assertEquals("The model attribute '" + attributeName + "' value is wrong:", expectedValue, + evaluateModelAttributeExpression(attributeName, viewSelection.getModel())); + } + + /** + * Assert that the selected view contains the specified collection model + * attribute with the provided expected size. + * @param expectedSize the expected size + * @param attributeName the collection attribute name (can be an expression + * @param viewSelection the selected view with a model attribute map to + * assert against + */ + protected void assertModelAttributeCollectionSize(int expectedSize, String attributeName, + ApplicationView viewSelection) { + assertModelAttributeNotNull(attributeName, viewSelection); + Collection c = (Collection)evaluateModelAttributeExpression(attributeName, viewSelection.getModel()); + assertEquals("The model attribute '" + attributeName + "' collection size is wrong:", expectedSize, c.size()); + } + + /** + * Assert that the selected view contains the specified model attribute. + * @param attributeName the attribute name (can be an expression) + * @param viewSelection the selected view with a model attribute map to + * assert against + */ + protected void assertModelAttributeNotNull(String attributeName, ApplicationView viewSelection) { + assertNotNull("The model attribute '" + attributeName + "' is null but should not be; model contents are " + + StylerUtils.style(viewSelection.getModel()), evaluateModelAttributeExpression(attributeName, + viewSelection.getModel())); + } + + /** + * Assert that the selected view does not contain the specified model + * attribute. + * @param attributeName the attribute name (can be an expression) + * @param viewSelection the selected view with a model attribute map to + * assert against + */ + protected void assertModelAttributeNull(String attributeName, ApplicationView viewSelection) { + assertNull("The model attribute '" + attributeName + "' is not null but should be; model contents are " + + StylerUtils.style(viewSelection.getModel()), evaluateModelAttributeExpression(attributeName, + viewSelection.getModel())); + } + + // other helpers + + /** + * Assert that the returned view selection is an instance of + * {@link ApplicationView}. + * @param viewSelection the view selection + */ + protected ApplicationView applicationView(ViewSelection viewSelection) { + Assert.isInstanceOf(ApplicationView.class, viewSelection, "Unexpected class of view selection: "); + return (ApplicationView)viewSelection; + } + + /** + * Assert that the returned view selection is an instance of + * {@link FlowExecutionRedirect}. + * @param viewSelection the view selection + */ + protected FlowExecutionRedirect flowExecutionRedirect(ViewSelection viewSelection) { + Assert.isInstanceOf(FlowExecutionRedirect.class, viewSelection, "Unexpected class of view selection: "); + return (FlowExecutionRedirect)viewSelection; + } + + /** + * Assert that the returned view selection is an instance of + * {@link FlowDefinitionRedirect}. + * @param viewSelection the view selection + */ + protected FlowDefinitionRedirect flowDefinitionRedirect(ViewSelection viewSelection) { + Assert.isInstanceOf(FlowDefinitionRedirect.class, viewSelection, "Unexpected class of view selection: "); + return (FlowDefinitionRedirect)viewSelection; + } + + /** + * Assert that the returned view selection is an instance of + * {@link ExternalRedirect}. + * @param viewSelection the view selection + */ + protected ExternalRedirect externalRedirect(ViewSelection viewSelection) { + Assert.isInstanceOf(ExternalRedirect.class, viewSelection, "Unexpected class of view selection: "); + return (ExternalRedirect)viewSelection; + } + + /** + * Assert that the returned view selection is the + * {@link ViewSelection#NULL_VIEW}. + * @param viewSelection the view selection + */ + protected void nullView(ViewSelection viewSelection) { + assertEquals("Not the null view selection:", viewSelection, ViewSelection.NULL_VIEW); + } + + /** + * Evaluates a model attribute expression. + * @param attributeName the attribute expression + * @param model the model map + * @return the attribute expression value + */ + protected Object evaluateModelAttributeExpression(String attributeName, Map model) { + return expressionParser.parseExpression(attributeName).evaluate(model, null); + } + + /** + * Factory method to create the flow execution factory. Subclasses + * could override this if they want to use a custom flow execution factory. + * The default implementation just returns a {@link FlowExecutionImplFactory} + * instance. + * @return the flow execution factory + */ + protected FlowExecutionFactory createFlowExecutionFactory() { + return new FlowExecutionImplFactory(); + } + + /** + * Directly update the flow execution used by the test by setting + * it to given flow execution. Use this if you have somehow manipulated + * the flow execution being tested and want to continue the test + * with another flow execution. + * @param flowExecution the flow execution to use + */ + protected void updateFlowExecution(FlowExecution flowExecution) { + this.flowExecution = flowExecution; + } + + /** + * Returns the flow definition to be tested. Subclasses must implement. + * @return the flow definition + */ + protected abstract FlowDefinition getFlowDefinition(); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractXmlFlowExecutionTests.java b/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractXmlFlowExecutionTests.java new file mode 100644 index 00000000..a1c55d69 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/execution/AbstractXmlFlowExecutionTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.test.execution; + +import org.springframework.core.io.Resource; +import org.springframework.webflow.engine.builder.FlowBuilder; +import org.springframework.webflow.engine.builder.FlowServiceLocator; +import org.springframework.webflow.engine.builder.xml.XmlFlowBuilder; + +/** + * Base class for flow integration tests that verify an XML flow definition + * executes as expected. + *

    + * Example usage: + * + *

    + * public class SearchFlowExecutionTests extends AbstractXmlFlowExecutionTests {
    + * 
    + *     protected FlowDefinitionResource getFlowDefinitionResource() {
    + *         return createFlowDefinitionResource("src/main/webapp/WEB-INF/flows/search-flow.xml");
    + *     }
    + * 
    + *     public void testStartFlow() {
    + *         startFlow();
    + *         assertCurrentStateEquals("displaySearchCriteria");
    + *     }
    + * 
    + *     public void testDisplayCriteriaSubmitSuccess() {
    + *         startFlow();
    + *         MockParameterMap parameters = new MockParameterMap();
    + *         parameters.put("firstName", "Keith");
    + *         parameters.put("lastName", "Donald");
    + *         ViewSelection view = signalEvent("search", parameters);
    + *         assertCurrentStateEquals("displaySearchResults");
    + *         assertModelAttributeCollectionSize(1, "results", view);
    + *     } 
    + * }
    + * 
    + * + * @author Keith Donald + */ +public abstract class AbstractXmlFlowExecutionTests extends AbstractExternalizedFlowExecutionTests { + + protected final FlowBuilder createFlowBuilder(Resource resource, FlowServiceLocator flowServiceLocator) { + return new XmlFlowBuilder(resource, flowServiceLocator); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/execution/package.html b/spring-webflow/src/main/java/org/springframework/webflow/test/execution/package.html new file mode 100644 index 00000000..c28ca718 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/execution/package.html @@ -0,0 +1,7 @@ + + +

    +Support for testing the execution of a flow definition. +

    + + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/package.html b/spring-webflow/src/main/java/org/springframework/webflow/test/package.html new file mode 100644 index 00000000..5c502e0a --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/package.html @@ -0,0 +1,24 @@ + + +

    +Support for testing flows and their associated artifacts. +

    +

    +When you want to unit test one of your flows the +{@link org.springframework.webflow.test.execution.AbstractFlowExecutionTests} +and associated subclasses provide a base you can extend. +

    +

    +When unit testing flow artifacts such as actions in isolation, the +{@link org.springframework.webflow.test.MockRequestContext} +is of particular interest. +

    +

    +All mock implementations provided by this package are NOT intended to be used for +anything but standalone unit tests. They are simple state holders, stub implementations, +at least if you follow Martin +Fowler's reasoning. These classes are called Mocks to be consistent with the naming +convention in the rest of the Spring framework (e.g. MockHttpServletRequest, ...). +

    + + diff --git a/spring-webflow/src/main/java/org/springframework/webflow/util/DispatchMethodInvoker.java b/spring-webflow/src/main/java/org/springframework/webflow/util/DispatchMethodInvoker.java new file mode 100644 index 00000000..36156b84 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/util/DispatchMethodInvoker.java @@ -0,0 +1,142 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Map; + +import org.springframework.binding.method.InvalidMethodKeyException; +import org.springframework.binding.method.MethodKey; +import org.springframework.core.NestedRuntimeException; +import org.springframework.util.Assert; +import org.springframework.util.CachingMapDecorator; + +/** + * Invoker and cache for dispatch methods that all share the same target object. + * The dispatch methods typically share the same form, but multiple exist per + * target object, and they only differ in name. + * + * @author Keith Donald + * @author Ben Hale + */ +public class DispatchMethodInvoker { + + /** + * The target object to dispatch to. + */ + private Object target; + + /** + * The parameter types describing the dispatch method signature. + */ + private Class[] parameterTypes; + + /** + * The resolved method cache. + */ + private Map methodCache = new CachingMapDecorator() { + public Object create(Object key) { + String methodName = (String)key; + try { + return new MethodKey(target.getClass(), methodName, parameterTypes).getMethod(); + } + catch (InvalidMethodKeyException e) { + throw new MethodLookupException("Unable to resolve dispatch method " + e.getMethodKey() + + "'; make sure the method name is correct and such a method is defined on targetClass " + + target.getClass().getName(), e); + } + } + }; + + /** + * Creates a dispatch method invoker. + * @param target the target to dispatch to + * @param parameterTypes the parameter types defining the argument signature + * of the dispatch methods + */ + public DispatchMethodInvoker(Object target, Class[] parameterTypes) { + Assert.notNull(target, "The target of a dispatch method invocation is required"); + this.target = target; + this.parameterTypes = parameterTypes; + } + + /** + * Returns the target object method calls are dispatched to. + */ + public Object getTarget() { + return target; + } + + /** + * Returns the parameter types defining the argument signature of the + * dispatch methods. + */ + public Class[] getParameterTypes() { + return parameterTypes; + } + + /** + * Dispatch a call with given arguments to named dispatcher method. + * @param methodName the name of the method to invoke + * @param arguments the arguments to pass to the method + * @return the result of the method invokation + * @throws MethodLookupException when the method cannot be resolved + * @throws Exception when the invoked method throws an exception + */ + public Object invoke(String methodName, Object[] arguments) throws MethodLookupException, Exception { + try { + Method dispatchMethod = getDispatchMethod(methodName); + return dispatchMethod.invoke(target, arguments); + } + catch (InvocationTargetException e) { + // the invoked method threw an exception; have it propagate to the caller + Throwable t = e.getTargetException(); + if (t instanceof Exception) { + throw (Exception)e.getTargetException(); + } + else { + throw (Error)e.getTargetException(); + } + } + } + + /** + * Get a handle to the method of the specified name, with the signature + * defined by the configured parameter types and return type. + * @param methodName the method name + * @return the method + * @throws MethodLookupException when the method cannot be resolved + */ + private Method getDispatchMethod(String methodName) throws MethodLookupException { + return (Method)methodCache.get(methodName); + } + + /** + * Thrown when a dispatch method could not be resolved. + */ + public static class MethodLookupException extends NestedRuntimeException { + + /** + * Create a new method lookup exception. + * @param msg a descriptive message + * @param ex the underlying cause of this exception + */ + public MethodLookupException(String msg, Throwable ex) { + super(msg, ex); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/util/RandomGuid.java b/spring-webflow/src/main/java/org/springframework/webflow/util/RandomGuid.java new file mode 100644 index 00000000..73e53945 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/util/RandomGuid.java @@ -0,0 +1,223 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.util; + +/* + * RandomGUID from http://www.javaexchange.com/aboutRandomGUID.html + * @version 1.2.1 11/05/02 + * @author Marc A. Mnich + * + * From www.JavaExchange.com, Open Software licensing + * + * 11/05/02 -- Performance enhancement from Mike Dubman. + * Moved InetAddr.getLocal to static block. Mike has measured + * a 10 fold improvement in run time. + * 01/29/02 -- Bug fix: Improper seeding of nonsecure Random object + * caused duplicate GUIDs to be produced. Random object + * is now only created once per JVM. + * 01/19/02 -- Modified random seeding and added new constructor + * to allow secure random feature. + * 01/14/02 -- Added random function seeding with JVM run time + */ + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Random; + +/** + * Globally unique identifier generator. + *

    + * In the multitude of java GUID generators, I found none that guaranteed + * randomness. GUIDs are guaranteed to be globally unique by using ethernet + * MACs, IP addresses, time elements, and sequential numbers. GUIDs are not + * expected to be random and most often are easy/possible to guess given a + * sample from a given generator. SQL Server, for example generates GUID that + * are unique but sequencial within a given instance. + *

    + * GUIDs can be used as security devices to hide things such as files within a + * filesystem where listings are unavailable (e.g. files that are served up from + * a Web server with indexing turned off). This may be desireable in cases where + * standard authentication is not appropriate. In this scenario, the RandomGuids + * are used as directories. Another example is the use of GUIDs for primary keys + * in a database where you want to ensure that the keys are secret. Random GUIDs + * can then be used in a URL to prevent hackers (or users) from accessing + * records by guessing or simply by incrementing sequential numbers. + *

    + * There are many other possiblities of using GUIDs in the realm of security and + * encryption where the element of randomness is important. This class was + * written for these purposes but can also be used as a general purpose GUID + * generator as well. + *

    + * RandomGuid generates truly random GUIDs by using the system's IP address + * (name/IP), system time in milliseconds (as an integer), and a very large + * random number joined together in a single String that is passed through an + * MD5 hash. The IP address and system time make the MD5 seed globally unique + * and the random number guarantees that the generated GUIDs will have no + * discernable pattern and cannot be guessed given any number of previously + * generated GUIDs. It is generally not possible to access the seed information + * (IP, time, random number) from the resulting GUIDs as the MD5 hash algorithm + * provides one way encryption. + *

    + * Security of RandomGuid: RandomGuid can be called one of two ways -- + * with the basic java Random number generator or a cryptographically strong + * random generator (SecureRandom). The choice is offered because the secure + * random generator takes about 3.5 times longer to generate its random numbers + * and this performance hit may not be worth the added security especially + * considering the basic generator is seeded with a cryptographically strong + * random seed. + *

    + * Seeding the basic generator in this way effectively decouples the random + * numbers from the time component making it virtually impossible to predict the + * random number component even if one had absolute knowledge of the System + * time. Thanks to Ashutosh Narhari for the suggestion of using the static + * method to prime the basic random generator. + *

    + * Using the secure random option, this class complies with the statistical + * random number generator tests specified in FIPS 140-2, Security Requirements + * for Cryptographic Modules, secition 4.9.1. + *

    + * I converted all the pieces of the seed to a String before handing it over to + * the MD5 hash so that you could print it out to make sure it contains the data + * you expect to see and to give a nice warm fuzzy. If you need better + * performance, you may want to stick to byte[] arrays. + *

    + * I believe that it is important that the algorithm for generating random GUIDs + * be open for inspection and modification. This class is free for all uses. + * + * @version 1.2.1 11/05/02 + * @author Marc A. Mnich + */ +public class RandomGuid extends Object { + + private static Random random; + + private static SecureRandom secureRandom; + + private static String id; + + private String valueBeforeMD5 = ""; + + private String valueAfterMD5 = ""; + + /* + * Static block to take care of one time secureRandom seed. It takes a few + * seconds to initialize SecureRandom. You might want to consider removing + * this static block or replacing it with a "time since first loaded" seed + * to reduce this time. This block will run only once per JVM instance. + */ + static { + secureRandom = new SecureRandom(); + long secureInitializer = secureRandom.nextLong(); + random = new Random(secureInitializer); + try { + id = InetAddress.getLocalHost().toString(); + } + catch (UnknownHostException e) { + throw new RuntimeException(e); + } + } + + /** + * Default constructor. With no specification of security option, this + * constructor defaults to lower security, high performance. + */ + public RandomGuid() { + getRandomGuid(false); + } + + /** + * Constructor with security option. Setting secure true enables each random + * number generated to be cryptographically strong. Secure false defaults to + * the standard Random function seeded with a single cryptographically + * strong random number. + */ + public RandomGuid(boolean secure) { + getRandomGuid(secure); + } + + /** + * Method to generate the random GUID. + */ + private void getRandomGuid(boolean secure) { + MessageDigest md5 = null; + StringBuffer sbValueBeforeMD5 = new StringBuffer(); + + try { + md5 = MessageDigest.getInstance("MD5"); + } + catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + + long time = System.currentTimeMillis(); + long rand = 0; + + if (secure) { + rand = secureRandom.nextLong(); + } + else { + rand = random.nextLong(); + } + + // This StringBuffer can be a long as you need; the MD5 + // hash will always return 128 bits. You can change + // the seed to include anything you want here. + // You could even stream a file through the MD5 making + // the odds of guessing it at least as great as that + // of guessing the contents of the file! + sbValueBeforeMD5.append(id); + sbValueBeforeMD5.append(":"); + sbValueBeforeMD5.append(Long.toString(time)); + sbValueBeforeMD5.append(":"); + sbValueBeforeMD5.append(Long.toString(rand)); + + valueBeforeMD5 = sbValueBeforeMD5.toString(); + md5.update(valueBeforeMD5.getBytes()); + + byte[] array = md5.digest(); + StringBuffer sb = new StringBuffer(); + for (int j = 0; j < array.length; ++j) { + int b = array[j] & 0xFF; + if (b < 0x10) + sb.append('0'); + sb.append(Integer.toHexString(b)); + } + valueAfterMD5 = sb.toString(); + } + + /** + * Convert to the standard format for GUID (Useful for SQL Server + * UniqueIdentifiers, etc). + * Example: "C2FEEEAC-CFCD-11D1-8B05-00600806D9B6". + */ + public String toString() { + String raw = valueAfterMD5.toUpperCase(); + StringBuffer sb = new StringBuffer(); + sb.append(raw.substring(0, 8)); + sb.append("-"); + sb.append(raw.substring(8, 12)); + sb.append("-"); + sb.append(raw.substring(12, 16)); + sb.append("-"); + sb.append(raw.substring(16, 20)); + sb.append("-"); + sb.append(raw.substring(20)); + return sb.toString(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/util/RandomGuidUidGenerator.java b/spring-webflow/src/main/java/org/springframework/webflow/util/RandomGuidUidGenerator.java new file mode 100644 index 00000000..2533800a --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/util/RandomGuidUidGenerator.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.util; + +import java.io.Serializable; + +/** + * A key generator that uses the RandomGuid support class. The default + * implementation used by the webflow system. + * + * @author Keith Donald + */ +public class RandomGuidUidGenerator implements UidGenerator, Serializable { + + /** + * Should the random GUID generated be secure? + */ + private boolean secure; + + /** + * Returns whether or not the generated random numbers are + * secure, meaning cryptographically strong. + */ + public boolean isSecure() { + return secure; + } + + /** + * Sets whether or not the generated random numbers should be + * secure. If set to true, generated GUIDs are cryptographically + * strong. + */ + public void setSecure(boolean secure) { + this.secure = secure; + } + + public Serializable generateUid() { + return new RandomGuid(secure).toString(); + } + + public Serializable parseUid(String encodedUid) { + return encodedUid; + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/util/ReflectionUtils.java b/spring-webflow/src/main/java/org/springframework/webflow/util/ReflectionUtils.java new file mode 100644 index 00000000..428122d8 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/util/ReflectionUtils.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.util; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Simple utility class for working with the java reflection API. Only intended for + * internal use. Will likely disappear in a future release of Spring Web Flow and + * simply rely on {@link org.springframework.util.ReflectionUtils} if necessary. + * + * @author Keith Donald + */ +public class ReflectionUtils { + + /** + * Invoke the specified {@link Method} against the supplied target object + * with no arguments. The target object can be null when + * invoking a static {@link Method}. All exceptions are treated as fatal and will be + * converted to unchecked exceptions. + * @see #invokeMethod(java.lang.reflect.Method, Object, Object[]) + * @throws RuntimeException when something goes wrong invoking the method or + * when the method itself throws an exception + */ + public static Object invokeMethod(Method method, Object target) throws RuntimeException { + return invokeMethod(method, target, null); + } + + /** + * Invoke the specified {@link Method} against the supplied target object + * with the supplied arguments. The target object can be null when invoking a + * static {@link Method}. All exceptions are treated as fatal and will be + * converted to unchecked exceptions. + * @see #invokeMethod(java.lang.reflect.Method, Object, Object[]) + * @throws RuntimeException when something goes wrong invoking the method or + * when the method itself throws an exception + */ + public static Object invokeMethod(Method method, Object target, Object[] args) throws RuntimeException { + try { + return method.invoke(target, args); + } + catch (IllegalAccessException ex) { + handleReflectionException(ex); + throw new IllegalStateException("Unexpected reflection exception - " + ex.getClass().getName() + ": " + + ex.getMessage()); + } + catch (InvocationTargetException ex) { + handleReflectionException(ex); + throw new IllegalStateException("Unexpected reflection exception - " + ex.getClass().getName() + ": " + + ex.getMessage()); + } + } + + /** + * Handle the given reflection exception. + * Should only be called if no checked exception is expected to + * be thrown by the target method. + *

    + * Throws the underlying RuntimeException or Error in case + * of an InvocationTargetException with such a root cause. Throws + * an IllegalStateException with an appropriate message else. + * @param ex the reflection exception to handle + */ + private static void handleReflectionException(Exception ex) { + if (ex instanceof NoSuchMethodException) { + throw new IllegalStateException("Method not found: " + ex.getMessage()); + } + if (ex instanceof IllegalAccessException) { + throw new IllegalStateException("Could not access method: " + ex.getMessage()); + } + if (ex instanceof InvocationTargetException) { + handleInvocationTargetException((InvocationTargetException) ex); + } + throw new IllegalStateException( + "Unexpected reflection exception - " + ex.getClass().getName() + ": " + ex.getMessage()); + } + + /** + * Handle the given invocation target exception. + * Should only be called if no checked exception is expected to + * be thrown by the target method. + *

    + * Throws the underlying RuntimeException or Error in case + * of such a root cause. Throws an IllegalStateException else. + * @param ex the invocation target exception to handle + */ + private static void handleInvocationTargetException(InvocationTargetException ex) { + if (ex.getTargetException() instanceof RuntimeException) { + throw (RuntimeException) ex.getTargetException(); + } + if (ex.getTargetException() instanceof Error) { + throw (Error) ex.getTargetException(); + } + throw new IllegalStateException( + "Unexpected exception thrown by method - " + ex.getTargetException().getClass().getName() + + ": " + ex.getTargetException().getMessage()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/util/ResourceHolder.java b/spring-webflow/src/main/java/org/springframework/webflow/util/ResourceHolder.java new file mode 100644 index 00000000..254461e5 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/util/ResourceHolder.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.util; + +import org.springframework.core.io.Resource; + +/** + * Simple interface for all objects (typically flow builders) that hold on to a + * resource defining a flow (e.g. an XML file). Provides a way to access + * information about the underlying resource like the last modified date. + * + * @see org.springframework.webflow.engine.builder.FlowBuilder + * + * @author Erwin Vervaet + * @author Keith Donald + */ +public interface ResourceHolder { + + /** + * Returns the flow definition resource held by this holder. + */ + public Resource getResource(); +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/util/UidGenerator.java b/spring-webflow/src/main/java/org/springframework/webflow/util/UidGenerator.java new file mode 100644 index 00000000..c57b9271 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/util/UidGenerator.java @@ -0,0 +1,40 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.util; + +import java.io.Serializable; + +/** + * A strategy for generating ids for uniquely identifying execution artifacts + * such as FlowExecutions and any other uniquely identified flow artifact. + * + * @author Keith Donald + */ +public interface UidGenerator { + + /** + * Generate a new unique id. + * @return a serializable id, guaranteed to be unique in some context + */ + public Serializable generateUid(); + + /** + * Convert the string-encoded uid into its original object form. + * @param encodedUid the string encoded uid + * @return the converted uid + */ + public Serializable parseUid(String encodedUid); +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/util/package.html b/spring-webflow/src/main/java/org/springframework/webflow/util/package.html new file mode 100644 index 00000000..e281a702 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/util/package.html @@ -0,0 +1,5 @@ + + +General purpose utility classes used internally by the Spring Web Flow system. + + diff --git a/spring-webflow/src/main/java/overview.html b/spring-webflow/src/main/java/overview.html new file mode 100644 index 00000000..a6b3e21a --- /dev/null +++ b/spring-webflow/src/main/java/overview.html @@ -0,0 +1,89 @@ + + +

    +
    +The public Java Documentation for Spring Web Flow, a framework for modeling and executing user interface flow. +

    +

    +Spring Web Flow's packages are partitioned across a set of logical layers. Higher layers depend on the layers directly beneath. Lower layers never depend on higher layers. +

    +

    +The layers of Spring Web Flow, from lowest to highest, are shown below: +

    +

    + +
    +Layer architecture diagram +

    +

    +The description, subsystems, and source packages of each layer are summarized below: +

    +

    Execution Core Layer

    +

    +Contains the central public Spring Web Flow API elements. This includes elements to model flow definitions +as well as execute those flow definitions. As the "bottom layer", this layer defines key domain interfaces and is highly stable. +

    +

    Subsystems

    +
      +
    1. {@link org.springframework.webflow.core Core} +
    2. {@link org.springframework.webflow.definition Flow Definition} +
    3. {@link org.springframework.webflow.definition.registry Flow Definition Registry} +
    4. {@link org.springframework.webflow.context External Context} +
    5. {@link org.springframework.webflow.conversation Conversation} +
    6. {@link org.springframework.webflow.execution Flow Execution} +
    7. {@link org.springframework.webflow.execution.repository Flow Execution Repository} +
    8. {@link org.springframework.webflow.action Action} +
    9. {@link org.springframework.webflow.util Util} +
    +

    +

    Executor Layer

    +

    +Contains services called "flow executors" that drive the execution of flow definitions. This layer defines the +core FlowExecutor service interface and implementation, as well as adaption code for executing flows in several +specific environments. Support for Spring MVC, Struts, and Java Server Faces (JSF) environments is housed here. +This layer depends on the stable Execution Core, but is not coupled to the more volatile Execution Engine +implementation. +

    +

    Subsystems

    +
      +
    1. {@link org.springframework.webflow.executor Core} +
    2. {@link org.springframework.webflow.executor.mvc Spring MVC} +
    3. {@link org.springframework.webflow.executor.struts Struts} +
    4. {@link org.springframework.webflow.executor.jsf Java Server Faces (JSF)} +
    +

    Execution Engine Layer

    +

    +

    +Contains concrete implementations of the stable Execution Engine abstractions. This layer defines the +finite-state machine that carries out runtime flow execution. It also contains a builder subsystem for +assembling flows from externalized resources such as XML files. +

    +

    Subsystems

    +
      +
    1. {@link org.springframework.webflow.engine Engine Implementation} +
    2. {@link org.springframework.webflow.engine.builder Flow Builder} +
    +

    Test Layer

    +

    +

    +Contains support code for testing flow executions. Two types of support are provided: stubs for unit +testing engine artifacts, and base classes for writing flow execution integration tests. This layer +depends on the Execution Core and Execution Engine layers. +

    +

    Subsystems

    +
      +
    1. {@link org.springframework.webflow.test Unit Test} +
    2. {@link org.springframework.webflow.test.execution Execution Test} +
    +

    System Configuration Layer

    +

    +

    Subsystems

    +

    +Contains support for configuring the flow executor engine using Spring. A Spring 2.0 config schema is provided. +This is the top layer and depends on the Execution Core, Executor, and Execution Engine layers. +

    +
      +
    1. {@link org.springframework.webflow.config Spring Configuration Support} +
    + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/TestBean.java b/spring-webflow/src/test/java/org/springframework/webflow/TestBean.java new file mode 100644 index 00000000..0a4509a0 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/TestBean.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow; + +import java.io.Serializable; + +/** + * Simple test bean used by some test cases. + * Note that this bean has value semantics. + */ +public class TestBean implements Serializable { + + public String datum1 = ""; + + public int datum2; + + public boolean executed; + + public void execute() { + this.executed = true; + } + + public String getDatum1() { + return datum1; + } + + public int getDatum2() { + return datum2; + } + + public boolean isExecuted() { + return executed; + } + + public void execute(String parameter) { + this.executed = true; + this.datum1 = parameter; + } + + public int execute(String parameter, int parameter2) { + this.executed = true; + this.datum1 = parameter; + this.datum2 = parameter2; + return datum2; + } + + public boolean equals(Object obj) { + if (!(obj instanceof TestBean)) { + return false; + } + TestBean other = (TestBean)obj; + return datum1.equals(other.datum1) && datum2 == other.datum2 && executed == other.executed; + } + + public int hashCode() { + return (datum1.hashCode() + datum2 + (executed ? 1:0)) * 29; + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/TestBeanWithMap.java b/spring-webflow/src/test/java/org/springframework/webflow/TestBeanWithMap.java new file mode 100644 index 00000000..73231e34 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/TestBeanWithMap.java @@ -0,0 +1,36 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; + +/** + * Simple test bean with a Map property. + */ +public class TestBeanWithMap implements Serializable { + + private Map map = new HashMap(); + + public Map getMap() { + return map; + } + + public void setMap(Map map) { + this.map = map; + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/TestException.java b/spring-webflow/src/test/java/org/springframework/webflow/TestException.java new file mode 100644 index 00000000..caae774a --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/TestException.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow; + +/** + * Dummy exception used by several test cases. + */ +public class TestException extends Exception { + + public TestException() { + super(); + } + + public TestException(String message, Throwable cause) { + super(message, cause); + } + + public TestException(String message) { + super(message); + } + + public TestException(Throwable cause) { + super(cause); + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/UnitTestTemplate.java b/spring-webflow/src/test/java/org/springframework/webflow/UnitTestTemplate.java new file mode 100644 index 00000000..d4d36f8d --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/UnitTestTemplate.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow; + +import junit.framework.TestCase; + +/** + * Keith likes to have these little cut & paste examples in the source + * repositories. If only he knew the power of code templates in Eclipse... + */ +public class UnitTestTemplate extends TestCase { + + protected void setUp() throws Exception { + } + + public void testScenario1() { + } + + public void testScenario2() { + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/action/AbstractActionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/action/AbstractActionTests.java new file mode 100644 index 00000000..0ba38b78 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/action/AbstractActionTests.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import junit.framework.TestCase; + +import org.springframework.beans.factory.BeanInitializationException; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Unit tests for {@link AbstractAction}. + */ +public class AbstractActionTests extends TestCase { + + private TestAbstractAction action = new TestAbstractAction(); + + public void testInitCallback() throws Exception { + action.afterPropertiesSet(); + assertTrue(action.initialized); + } + + public void testInitCallbackWithException() throws Exception { + action = new TestAbstractAction() { + protected void initAction() { + throw new IllegalStateException("Cannot initialize"); + } + }; + try { + action.afterPropertiesSet(); + fail("Should've failed initialization"); + } + catch (BeanInitializationException e) { + assertFalse(action.initialized); + } + } + + public void testNormalExecute() throws Exception { + action = new TestAbstractAction() { + protected Event doExecute(RequestContext context) throws Exception { + return success(); + } + }; + Event result = action.execute(new MockRequestContext()); + assertEquals("success", result.getId()); + assertTrue(result.getAttributes().size() == 0); + } + + public void testExceptionalExecute() throws Exception { + try { + action.execute(new MockRequestContext()); + fail("Should've failed execute"); + } + catch (IllegalStateException e) { + + } + } + + public void testPreExecuteShortCircuit() throws Exception { + action = new TestAbstractAction() { + protected Event doPreExecute(RequestContext context) throws Exception { + return success(); + } + }; + Event result = action.execute(new MockRequestContext()); + assertEquals("success", result.getId()); + } + + public void testPostExecuteCalled() throws Exception { + testNormalExecute(); + assertTrue(action.postExecuteCalled); + } + + public class TestAbstractAction extends AbstractAction { + private boolean initialized; + + private boolean postExecuteCalled; + + protected void initAction() { + initialized = true; + } + + protected Event doExecute(RequestContext context) throws Exception { + throw new IllegalStateException("Should not be called"); + } + + protected void doPostExecute(RequestContext context) { + postExecuteCalled = true; + } + } + + public void testSuccess() { + Event event = action.success(); + assertEquals(action.getEventFactorySupport().getSuccessEventId(), event.getId()); + } + + public void testSuccessResult() { + Object o = new Object(); + Event event = action.success(o); + assertEquals(action.getEventFactorySupport().getSuccessEventId(), event.getId()); + assertSame(o, event.getAttributes().get(action.getEventFactorySupport().getResultAttributeName())); + } + + public void testError() { + Event event = action.error(); + assertEquals(action.getEventFactorySupport().getErrorEventId(), event.getId()); + } + + public void testErrorException() { + IllegalArgumentException e = new IllegalArgumentException("woops"); + Event event = action.error(e); + assertEquals(action.getEventFactorySupport().getErrorEventId(), event.getId()); + assertSame(e, event.getAttributes().get(action.getEventFactorySupport().getExceptionAttributeName())); + } + + public void testYes() { + Event event = action.yes(); + assertEquals(action.getEventFactorySupport().getYesEventId(), event.getId()); + } + + public void testNo() { + Event event = action.no(); + assertEquals(action.getEventFactorySupport().getNoEventId(), event.getId()); + } + + public void testTrueResult() { + Event event = action.result(true); + assertEquals(action.getEventFactorySupport().getYesEventId(), event.getId()); + } + + public void testFalseResult() { + Event event = action.result(false); + assertEquals(action.getEventFactorySupport().getNoEventId(), event.getId()); + } + + public void testCustomResult() { + Event event = action.result("custom"); + assertEquals("custom", event.getId()); + } + + public void testCustomResultObject() { + Event event = action.result("custom", "result", "value"); + assertEquals("custom", event.getId()); + assertEquals("value", event.getAttributes().getString("result")); + } + + public void testCustomResultCollection() { + LocalAttributeMap collection = new LocalAttributeMap(); + collection.put("result", "value"); + Event event = action.result("custom", collection); + assertEquals("custom", event.getId()); + assertEquals("value", event.getAttributes().getString("result")); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/action/AttributeMapperActionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/action/AttributeMapperActionTests.java new file mode 100644 index 00000000..bf60e6df --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/action/AttributeMapperActionTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import junit.framework.TestCase; + +import org.springframework.binding.mapping.DefaultAttributeMapper; +import org.springframework.binding.mapping.MappingBuilder; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Unit test for the {@link AttributeMapperAction}. + * + * @author Erwin Vervaet + */ +public class AttributeMapperActionTests extends TestCase { + + private MappingBuilder mappingBuilder = new MappingBuilder(DefaultExpressionParserFactory + .getExpressionParser()); + + public void testMapping() throws Exception { + DefaultAttributeMapper mapper = new DefaultAttributeMapper(); + mapper.addMapping(mappingBuilder.source("${externalContext.requestParameterMap.foo}") + .target("${flowScope.bar}").value()); + AttributeMapperAction action = new AttributeMapperAction(mapper); + + MockRequestContext context = new MockRequestContext(); + context.putRequestParameter("foo", "value"); + + assertTrue(context.getFlowScope().size() == 0); + + action.execute(context); + + assertEquals(1, context.getFlowScope().size()); + assertEquals("value", context.getFlowScope().get("bar")); + } + + public void testNullIllegalArgument() { + try { + new AttributeMapperAction(null); + fail("Should've thrown illegal argument"); + } + catch (IllegalArgumentException e) { + + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/action/CompositeActionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/action/CompositeActionTests.java new file mode 100644 index 00000000..424f131f --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/action/CompositeActionTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import junit.framework.TestCase; + +import org.easymock.EasyMock; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Unit tests for the {@link CompositeAction} class. + * + * @author Ulrik Sandberg + */ +public class CompositeActionTests extends TestCase { + + private CompositeAction tested; + + private Action actionMock; + + protected void setUp() throws Exception { + super.setUp(); + actionMock = (Action)EasyMock.createMock(Action.class); + Action[] actions = new Action[] { actionMock }; + tested = new CompositeAction(actions); + } + + public void testDoExecute() throws Exception { + MockRequestContext mockRequestContext = new MockRequestContext(); + LocalAttributeMap attributes = new LocalAttributeMap(); + attributes.put("some key", "some value"); + EasyMock.expect(actionMock.execute(mockRequestContext)).andReturn(new Event(this, "some event", attributes)); + EasyMock.replay(new Object[] { actionMock }); + Event result = tested.doExecute(mockRequestContext); + EasyMock.verify(new Object[] { actionMock }); + assertEquals("some event", result.getId()); + assertEquals(1, result.getAttributes().size()); + } + + public void testDoExecuteWithError() throws Exception { + tested.setStopOnError(true); + MockRequestContext mockRequestContext = new MockRequestContext(); + EasyMock.expect(actionMock.execute(mockRequestContext)).andReturn(new Event(this, "error")); + EasyMock.replay(new Object[] { actionMock }); + Event result = tested.doExecute(mockRequestContext); + EasyMock.verify(new Object[] { actionMock }); + assertEquals("error", result.getId()); + } + + public void testDoExecuteWithNullResult() throws Exception { + tested.setStopOnError(true); + MockRequestContext mockRequestContext = new MockRequestContext(); + EasyMock.expect(actionMock.execute(mockRequestContext)).andReturn(null); + EasyMock.replay(new Object[] { actionMock }); + Event result = tested.doExecute(mockRequestContext); + EasyMock.verify(new Object[] { actionMock }); + assertEquals("Expecting success since no check is performed if null result,", "success", result.getId()); + } + + public void testMultipleActions() throws Exception { + CompositeAction ca = new CompositeAction(new Action[] { new Action() { + public Event execute(RequestContext context) throws Exception { + return new Event(this, "foo"); + } + }, new Action() { + public Event execute(RequestContext context) throws Exception { + return new Event(this, "bar"); + } + } }); + assertEquals("Result of last executed action should be returned", "bar", ca.execute(new MockRequestContext()) + .getId()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/action/EvaluateActionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/action/EvaluateActionTests.java new file mode 100644 index 00000000..5f65ceed --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/action/EvaluateActionTests.java @@ -0,0 +1,63 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.webflow.TestBean; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Unit tests for {@link EvaluateAction}. + */ +public class EvaluateActionTests extends TestCase { + + private ExpressionParser parser = DefaultExpressionParserFactory.getExpressionParser(); + + private MockRequestContext context = new MockRequestContext(); + + protected void setUp() throws Exception { + context.getFlowScope().put("foo", "bar"); + context.getFlowScope().put("bean", new TestBean()); + } + + public void testEvaluateExpressionNoResult() throws Exception { + EvaluateAction action = new EvaluateAction(parser.parseExpression("flowScope.foo")); + Event result = action.execute(context); + assertEquals("bar", result.getId()); + assertNull(context.getFlowScope().get("baz")); + } + + public void testEvaluateExpressionResult() throws Exception { + EvaluateAction action = new EvaluateAction(parser.parseExpression("flowScope.foo"), new ActionResultExposer( + "baz", ScopeType.FLOW)); + Event result = action.execute(context); + assertEquals("bar", result.getId()); + assertEquals("bar", context.getFlowScope().get("baz")); + } + + public void testBeanResult() throws Exception { + EvaluateAction action = new EvaluateAction(parser.parseExpression("flowScope.bean"), new ActionResultExposer( + "baz", ScopeType.FLOW)); + Event result = action.execute(context); + assertEquals("success", result.getId()); + assertEquals(new TestBean(), context.getFlowScope().get("baz")); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/action/FormActionBindingTests.java b/spring-webflow/src/test/java/org/springframework/webflow/action/FormActionBindingTests.java new file mode 100644 index 00000000..d9bba8d9 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/action/FormActionBindingTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import junit.framework.TestCase; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.validation.DataBinder; +import org.springframework.validation.Errors; +import org.springframework.webflow.context.servlet.ServletExternalContext; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Unit test for the {@link FormAction} class, dealing with binding related issues. + * + * @author Erwin Vervaet + */ +public class FormActionBindingTests extends TestCase { + + public static class TestBean { + + private Long prop; + public String otherProp; + + public Long getProp() { + return prop; + } + + public void setProp(Long prop) { + this.prop = prop; + } + } + + public void testMessageCodesOnBindFailure() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.addParameter("prop", "A"); + MockHttpServletResponse response = new MockHttpServletResponse(); + MockRequestContext context = new MockRequestContext(); + context.setExternalContext(new ServletExternalContext(null, request, response)); + context.setAttribute("method", "bindAndValidate"); + + // use a FormAction to do the binding + FormAction formAction = new FormAction(); + formAction.setFormObjectClass(TestBean.class); + formAction.setFormObjectName("formObject"); + formAction.execute(context); + Errors formActionErrors = new FormObjectAccessor(context).getCurrentFormErrors(formAction.getFormErrorsScope()); + assertNotNull(formActionErrors); + assertTrue(formActionErrors.hasErrors()); + + assertEquals(1, formActionErrors.getErrorCount()); + assertEquals(0, formActionErrors.getGlobalErrorCount()); + assertEquals(1, formActionErrors.getFieldErrorCount("prop")); + } + + public void testFieldBinding() throws Exception { + FormAction formAction = new FormAction() { + protected Object createFormObject(RequestContext context) throws Exception { + TestBean res = new TestBean(); + res.setProp(new Long(-1)); + res.otherProp = "initialValue"; + return res; + } + + protected void initBinder(RequestContext context, DataBinder binder) { + binder.initDirectFieldAccess(); + } + }; + formAction.setFormObjectName("formObject"); + + MockRequestContext context = new MockRequestContext(); + + context.setAttribute("method", "setupForm"); + formAction.execute(context); + Errors errors = new FormObjectAccessor(context).getFormErrors("formObject", ScopeType.FLASH); + assertNotNull(errors); + assertEquals(new Long(-1), errors.getFieldValue("prop")); + + //this fails because of SWF-193 + assertEquals("initialValue", errors.getFieldValue("otherProp")); + + context.putRequestParameter("prop", "1"); + context.putRequestParameter("otherProp", "value"); + context.setAttribute("method", "bind"); + formAction.execute(context); + + TestBean formObject = (TestBean) + new FormObjectAccessor(context).getFormObject("formObject", ScopeType.FLOW); + errors = new FormObjectAccessor(context).getFormErrors("formObject", ScopeType.FLASH); + assertNotNull(formObject); + assertEquals(new Long(1), formObject.getProp()); + assertEquals(new Long(1), errors.getFieldValue("prop")); + assertEquals("value", formObject.otherProp); + assertEquals("value", errors.getFieldValue("otherProp")); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/action/FormActionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/action/FormActionTests.java new file mode 100644 index 00000000..14577cee --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/action/FormActionTests.java @@ -0,0 +1,464 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import junit.framework.TestCase; + +import org.springframework.validation.BindException; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; +import org.springframework.webflow.core.collection.LocalParameterMap; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.test.MockExternalContext; +import org.springframework.webflow.test.MockParameterMap; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Unit test for the {@link FormAction} class. + * + * @author Erwin Vervaet + */ +public class FormActionTests extends TestCase { + + private static class TestBean { + + private String prop; + + public TestBean() { + } + + public TestBean(String prop) { + this.prop = prop; + } + + public String getProp() { + return prop; + } + + public void setProp(String prop) { + this.prop = prop; + } + } + + private static class OtherTestBean { + + private String otherProp; + + public String getOtherProp() { + return otherProp; + } + + public void setOtherProp(String otherProp) { + this.otherProp = otherProp; + } + } + + public static class TestBeanValidator implements Validator { + private boolean invoked; + + public boolean supports(Class clazz) { + return TestBean.class.equals(clazz); + } + + public void validate(Object formObject, Errors errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "prop", "Prop cannot be empty"); + invoked = true; + } + + public void validateTestBean(TestBean formObject, Errors errors) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "prop", "Prop cannot be empty"); + invoked = true; + } + } + + private FormAction action; + + protected void setUp() throws Exception { + action = createFormAction("test"); + } + + public void testSetupForm() throws Exception { + MockRequestContext context = new MockRequestContext(); + + // setupForm() should initialize the form object and the Errors + // instance, but no bind & validate should happen since bindOnSetupForm + // is not set + + assertEquals(action.getEventFactorySupport().getSuccessEventId(), action.setupForm(context).getId()); + + assertEquals(2, context.getRequestScope().size()); + assertEquals(2, context.getFlowScope().size()); + assertFalse(getErrors(context).hasErrors()); + assertNull(getFormObject(context).getProp()); + } + + protected LocalParameterMap parameters() { + MockParameterMap map = new MockParameterMap(); + map.put("prop", "value"); + return map; + } + + protected LocalParameterMap blankParameters() { + MockParameterMap map = new MockParameterMap(); + map.put("prop", ""); + return map; + } + + + public void testSetupFormWithExistingFormObject() throws Exception { + MockRequestContext context = new MockRequestContext(parameters()); + + assertEquals(action.getEventFactorySupport().getSuccessEventId(), action.setupForm(context).getId()); + + Errors errors = getErrors(context); + errors.reject("dummy"); + TestBean formObject = getFormObject(context); + formObject.setProp("bla"); + + // setupForm() should leave the existing form object and Errors instance + // untouched, at least when no bind & validate is done (bindOnSetupForm + // == false) + + assertEquals(action.getEventFactorySupport().getSuccessEventId(), action.setupForm(context).getId()); + + assertEquals(2, context.getRequestScope().size()); + assertEquals(2, context.getFlowScope().size()); + assertSame(errors, getErrors(context)); + assertSame(formObject, getFormObject(context)); + assertTrue(getErrors(context).hasErrors()); + assertEquals("bla", getFormObject(context).getProp()); + } + + public void testBindAndValidate() throws Exception { + MockRequestContext context = new MockRequestContext(parameters()); + + // bindAndValidate() should setup a new form object and errors instance + // and do a bind & validate + + context.setAttribute("validatorMethod", "validateTestBean"); + assertEquals(action.getEventFactorySupport().getSuccessEventId(), action.bindAndValidate(context).getId()); + + assertEquals(2, context.getRequestScope().size()); + assertEquals(2, context.getFlowScope().size()); + assertFalse(getErrors(context).hasErrors()); + assertEquals("value", getFormObject(context).getProp()); + } + + public void testBindAndValidateFailure() throws Exception { + MockRequestContext context = new MockRequestContext(); + + // bindAndValidate() should setup a new form object and errors instance + // and do a bind & validate, which fails because the provided value is + // empty + + assertEquals(action.getEventFactorySupport().getErrorEventId(), action.bindAndValidate(context).getId()); + + assertEquals(2, context.getRequestScope().size()); + assertEquals(2, context.getFlowScope().size()); + assertTrue(getErrors(context).hasErrors()); + assertNull(getFormObject(context).getProp()); + } + + public void testBindAndValidateWithExistingFormObject() throws Exception { + MockRequestContext context = new MockRequestContext(parameters()); + + assertEquals(action.getEventFactorySupport().getSuccessEventId(), action.setupForm(context).getId()); + + Errors errors = getErrors(context); + errors.reject("dummy"); + TestBean formObject = getFormObject(context); + formObject.setProp("bla"); + + // bindAndValidate() should leave the existing form object untouched + // but should setup a new Errors instance during bind & validate + + assertEquals(action.getEventFactorySupport().getSuccessEventId(), action.bindAndValidate(context).getId()); + + assertEquals(2, context.getRequestScope().size()); + assertEquals(2, context.getFlowScope().size()); + assertNotSame(errors, getErrors(context)); + assertSame(formObject, getFormObject(context)); + assertFalse(getErrors(context).hasErrors()); + assertEquals("value", getFormObject(context).getProp()); + } + + // this is what happens in a 'form state' + public void testBindAndValidateFailureThenSetupForm() throws Exception { + MockRequestContext context = new MockRequestContext(blankParameters()); + + // setup existing form object & errors + assertEquals(action.getEventFactorySupport().getSuccessEventId(), action.setupForm(context).getId()); + TestBean formObject = getFormObject(context); + formObject.setProp("bla"); + + assertEquals(action.getEventFactorySupport().getErrorEventId(), action.bindAndValidate(context).getId()); + + assertEquals(2, context.getRequestScope().size()); + assertEquals(2, context.getFlowScope().size()); + assertSame(formObject, getFormObject(context)); + assertTrue(getErrors(context).hasErrors()); + assertEquals("", getFormObject(context).getProp()); + + Errors errors = getErrors(context); + + // the setupForm() should leave the form object and error info setup by + // the + // bind & validate untouched + + assertEquals(action.getEventFactorySupport().getSuccessEventId(), action.setupForm(context).getId()); + + assertEquals(2, context.getRequestScope().size()); + assertEquals(2, context.getFlowScope().size()); + assertSame(errors, getErrors(context)); + assertSame(formObject, getFormObject(context)); + assertTrue(getErrors(context).hasErrors()); + assertEquals("", getFormObject(context).getProp()); + } + + public void testMultipleFormObjectsInOneFlow() throws Exception { + MockRequestContext context = new MockRequestContext(parameters()); + + FormAction otherAction = createFormAction("otherTest"); + + assertEquals(action.getEventFactorySupport().getSuccessEventId(), action.setupForm(context).getId()); + assertEquals(action.getEventFactorySupport().getSuccessEventId(), otherAction.setupForm(context).getId()); + + assertEquals(3, context.getRequestScope().size()); + assertEquals(3, context.getFlowScope().size()); + assertNotSame(getErrors(context), getErrors(context, "otherTest")); + assertNotSame(getFormObject(context), getFormObject(context, "otherTest")); + assertFalse(getErrors(context).hasErrors()); + assertFalse(getErrors(context, "otherTest").hasErrors()); + assertNull(getFormObject(context).getProp()); + assertNull(getFormObject(context, "otherTest").getProp()); + + assertEquals(action.getEventFactorySupport().getSuccessEventId(), action.bindAndValidate(context).getId()); + + assertEquals(3, context.getRequestScope().size()); + assertEquals(3, context.getFlowScope().size()); + assertNotSame(getErrors(context), getErrors(context, "otherTest")); + assertNotSame(getFormObject(context), getFormObject(context, "otherTest")); + assertFalse(getErrors(context).hasErrors()); + assertFalse(getErrors(context, "otherTest").hasErrors()); + assertEquals("value", getFormObject(context).getProp()); + assertNull(getFormObject(context, "otherTest").getProp()); + + context.setExternalContext(new MockExternalContext(blankParameters())); + + assertEquals(action.getEventFactorySupport().getErrorEventId(), otherAction.bindAndValidate(context).getId()); + + assertEquals(3, context.getRequestScope().size()); + assertEquals(3, context.getFlowScope().size()); + assertNotSame(getErrors(context), getErrors(context, "otherTest")); + assertNotSame(getFormObject(context), getFormObject(context, "otherTest")); + assertFalse(getErrors(context).hasErrors()); + assertTrue(getErrors(context, "otherTest").hasErrors()); + assertEquals("value", getFormObject(context).getProp()); + assertEquals("", getFormObject(context, "otherTest").getProp()); + } + + public void testGetFormObject() throws Exception { + MockRequestContext context = new MockRequestContext(parameters()); + FormAction action = createFormAction("test"); + TestBean formObject = (TestBean)action.getFormObject(context); + assertNotNull(formObject); + formObject = new TestBean(); + TestBean testBean = formObject; + new FormObjectAccessor(context).putFormObject(formObject, action.getFormObjectName(), action + .getFormObjectScope()); + formObject = (TestBean)action.getFormObject(context); + assertSame(formObject, testBean); + } + + public void testGetFormErrors() throws Exception { + MockRequestContext context = new MockRequestContext(parameters()); + FormAction action = createFormAction("test"); + action.setupForm(context); + Errors errors = action.getFormErrors(context); + assertNotNull(errors); + assertTrue(!errors.hasErrors()); + errors = new BindException(getFormObject(context), "test"); + Errors testErrors = errors; + new FormObjectAccessor(context).putFormErrors(errors, action.getFormErrorsScope()); + errors = action.getFormErrors(context); + assertSame(errors, testErrors); + } + + public void testFormObjectAccessUsingAlias() throws Exception { + MockRequestContext context = new MockRequestContext(blankParameters()); + + FormAction otherAction = createFormAction("otherTest"); + + assertEquals(action.getEventFactorySupport().getSuccessEventId(), action.setupForm(context).getId()); + + assertSame(getFormObject(context), new FormObjectAccessor(context).getCurrentFormObject()); + assertSame(getErrors(context), new FormObjectAccessor(context).getCurrentFormErrors()); + + assertEquals(action.getEventFactorySupport().getSuccessEventId(), otherAction.setupForm(context).getId()); + + assertSame(getFormObject(context, "otherTest"), new FormObjectAccessor(context).getCurrentFormObject()); + assertSame(getErrors(context, "otherTest"), new FormObjectAccessor(context).getCurrentFormErrors()); + + assertEquals(action.getEventFactorySupport().getErrorEventId(), action.bindAndValidate(context).getId()); + + assertSame(getFormObject(context), new FormObjectAccessor(context).getCurrentFormObject()); + assertSame(getErrors(context), new FormObjectAccessor(context).getCurrentFormErrors()); + + context.setExternalContext(new MockExternalContext(parameters())); + + assertEquals(action.getEventFactorySupport().getSuccessEventId(), otherAction.bindAndValidate(context).getId()); + + assertSame(getFormObject(context, "otherTest"), new FormObjectAccessor(context).getCurrentFormObject()); + assertSame(getErrors(context, "otherTest"), new FormObjectAccessor(context).getCurrentFormErrors()); + } + + // as reported in SWF-4 + public void testInconsistentFormObjectAndErrors() throws Exception { + MockRequestContext context = new MockRequestContext(parameters()); + + assertEquals(action.getEventFactorySupport().getSuccessEventId(), action.setupForm(context).getId()); + + Object formObject = getFormObject(context); + BindException errors = (BindException)getErrors(context); + + assertTrue(formObject instanceof TestBean); + assertTrue(errors.getTarget() instanceof TestBean); + assertSame(formObject, errors.getTarget()); + + context = new MockRequestContext(); + context.setLastEvent(new Event(this, "start")); + + OtherTestBean freshBean = new OtherTestBean(); + context.getFlowScope().put("test", freshBean); + context.getRequestScope().put(BindException.ERROR_KEY_PREFIX + "test", errors); + + FormAction otherAction = createFormAction("test"); + otherAction.setFormObjectClass(OtherTestBean.class); + + assertEquals(action.getEventFactorySupport().getSuccessEventId(), otherAction.setupForm(context).getId()); + + formObject = context.getFlowScope().get("test"); + errors = (BindException)getErrors(context); + + assertTrue(formObject instanceof OtherTestBean); + assertSame(freshBean, formObject); + assertTrue("Expected OtherTestBean, but was " + errors.getTarget().getClass(), + errors.getTarget() instanceof OtherTestBean); + assertSame(formObject, errors.getTarget()); + } + + public void testMultipleFormObjects() throws Exception { + MockRequestContext context = new MockRequestContext(parameters()); + + FormAction action1 = createFormAction("test1"); + action1.setupForm(context); + TestBean test1 = (TestBean)context.getFlowScope().get("test1"); + assertNotNull(test1); + assertSame(test1, new FormObjectAccessor(context).getCurrentFormObject()); + + FormAction action2 = createFormAction("test2"); + action2.setupForm(context); + TestBean test2 = (TestBean)context.getFlowScope().get("test2"); + assertNotNull(test2); + assertSame(test2, new FormObjectAccessor(context).getCurrentFormObject()); + + MockParameterMap parameters = new MockParameterMap(); + parameters.put("prop", "12345"); + context.setExternalContext(new MockExternalContext(parameters)); + action1.bindAndValidate(context); + TestBean test11 = (TestBean)context.getFlowScope().get("test1"); + assertSame(test1, test11); + assertEquals("12345", test1.getProp()); + assertSame(test1, new FormObjectAccessor(context).getCurrentFormObject()); + + parameters = new MockParameterMap(); + parameters.put("prop", "123456"); + context.setExternalContext(new MockExternalContext(parameters)); + action2.bindAndValidate(context); + TestBean test22 = (TestBean)context.getFlowScope().get("test2"); + assertSame(test22, test2); + assertEquals("123456", test2.getProp()); + assertSame(test2, new FormObjectAccessor(context).getCurrentFormObject()); + } + + public void testFormObjectAndNoErrors() throws Exception { + // this typically happens with mapping from parent flow to subflow + MockRequestContext context = new MockRequestContext(parameters()); + + TestBean testBean = new TestBean(); + testBean.setProp("bla"); + context.getFlowScope().put("test", testBean); + + action.setupForm(context); + + // should have created a new empty errors instance, but left the form + // object alone + // since we didn't to bindOnSetupForm + + assertSame(testBean, getFormObject(context)); + assertEquals("bla", getFormObject(context).getProp()); + assertNotNull(getErrors(context)); + assertSame(testBean, ((BindException)getErrors(context)).getTarget()); + assertFalse(getErrors(context).hasErrors()); + } + + public void testSetupFormThenBindAndValidate() throws Exception { + FormAction action = createFormAction("testBean"); + MockRequestContext context = new MockRequestContext(); + Event result = action.setupForm(context); + assertEquals("success", result.getId()); + Object formObject = action.getFormObject(context); + assertSame(formObject, action.getFormObject(context)); + assertTrue(formObject instanceof TestBean); + context.putRequestParameter("prop", "foo"); + context.getAttributeMap().put("validatorMethod", "validateTestBean"); + result = action.bindAndValidate(context); + assertEquals("success", result.getId()); + assertSame(formObject, action.getFormObject(context)); + assertEquals(true, ((TestBeanValidator)action.getValidator()).invoked); + } + // helpers + + private FormAction createFormAction(String formObjectName) { + FormAction res = new FormAction(); + res.setFormObjectName(formObjectName); + res.setFormObjectClass(TestBean.class); + res.setValidator(new TestBeanValidator()); + res.setFormObjectScope(ScopeType.FLOW); + res.setFormErrorsScope(ScopeType.REQUEST); + res.initAction(); + return res; + } + + private Errors getErrors(RequestContext context) { + return getErrors(context, "test"); + } + + private Errors getErrors(RequestContext context, String formObjectName) { + return new FormObjectAccessor(context).getFormErrors(formObjectName, ScopeType.REQUEST); + } + + private TestBean getFormObject(RequestContext context) { + return getFormObject(context, "test"); + } + + private TestBean getFormObject(RequestContext context, String formObjectName) { + return (TestBean)context.getFlowScope().get(formObjectName); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/action/LocalBeanInvokingActionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/action/LocalBeanInvokingActionTests.java new file mode 100644 index 00000000..929201f7 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/action/LocalBeanInvokingActionTests.java @@ -0,0 +1,87 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.method.MethodSignature; +import org.springframework.binding.method.Parameter; +import org.springframework.binding.method.Parameters; +import org.springframework.webflow.TestBean; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Unit test for the {@link LocalBeanInvokingAction}. + * + * @author Keith Donald + */ +public class LocalBeanInvokingActionTests extends TestCase { + + private TestBean bean = new TestBean(); + + private LocalBeanInvokingAction action; + + private MockRequestContext context = new MockRequestContext(); + + public void setUp() { + action = new LocalBeanInvokingAction(new MethodSignature("execute"), bean); + } + + public void testInvokeBean() throws Exception { + action.execute(context); + assertTrue(bean.executed); + } + + public void testNullTargetBean() throws Exception { + try { + action = new LocalBeanInvokingAction(new MethodSignature("execute"), null); + fail("Should've failed with iae"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testExposeResultInScopes() throws Exception { + LocalAttributeMap attributes = new LocalAttributeMap(); + attributes.put("foo", "a string value"); + attributes.put("bar", "12345"); + context.setLastEvent(new Event(this, "submit", attributes)); + MethodSignature method = new MethodSignature("execute", new Parameters(new Parameter[] { + new Parameter(String.class, expression("lastEvent.attributes.foo")), + new Parameter(Integer.class, expression("lastEvent.attributes.bar")) })); + action = new LocalBeanInvokingAction(method, bean); + action.setMethodResultExposer(new ActionResultExposer("foo", ScopeType.REQUEST)); + testInvokeBean(); + assertEquals(new Integer(12345), context.getRequestScope().get("foo")); + + context.getRequestScope().clear(); + + action.setMethodResultExposer(new ActionResultExposer("foo", ScopeType.FLOW)); + testInvokeBean(); + assertEquals(new Integer(12345), context.getFlowScope().get("foo")); + assertNull(context.getRequestScope().get("foo")); + } + + private Expression expression(String string) { + return DefaultExpressionParserFactory.getExpressionParser().parseExpression(string); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/action/MultiActionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/action/MultiActionTests.java new file mode 100644 index 00000000..5842fda0 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/action/MultiActionTests.java @@ -0,0 +1,92 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import junit.framework.TestCase; + +import org.springframework.webflow.action.MultiAction.MethodResolver; +import org.springframework.webflow.engine.AnnotatedAction; +import org.springframework.webflow.engine.ViewState; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.test.MockFlowSession; +import org.springframework.webflow.test.MockRequestContext; +import org.springframework.webflow.util.DispatchMethodInvoker.MethodLookupException; + +/** + * Unit tests for {@link MultiAction}. + */ +public class MultiActionTests extends TestCase { + + private TestMultiAction action = new TestMultiAction(); + + private MockRequestContext context = new MockRequestContext(); + + public void testDispatchWithMethodSignature() throws Exception { + context.getAttributeMap().put(AnnotatedAction.METHOD_ATTRIBUTE, "increment"); + action.execute(context); + assertEquals(1, action.counter); + } + + public void testDispatchWithBogusMethodSignature() throws Exception { + context.getAttributeMap().put(AnnotatedAction.METHOD_ATTRIBUTE, "bogus"); + try { + action.execute(context); + fail("Should've failed with no such method"); + } + catch (MethodLookupException e) { + + } + } + + public void testDispatchWithCurrentStateId() throws Exception { + MockFlowSession session = context.getMockFlowExecutionContext().getMockActiveSession(); + session.setState(new ViewState(session.getDefinitionInternal(), "increment")); + action.execute(context); + assertEquals(1, action.counter); + } + + public void testNoSuchMethodWithCurrentStateId() throws Exception { + try { + action.execute(context); + fail("Should've failed with no such method"); + } + catch (MethodLookupException e) { + + } + } + + public void testCannotResolveMethod() throws Exception { + try { + context.getMockFlowExecutionContext().getMockActiveSession().setState(null); + action.execute(context); + fail("Should've failed with illegal state"); + } + catch (IllegalStateException e) { + + } + } + + public void testCustomMethodResolver() throws Exception { + MethodResolver methodResolver = new MethodResolver() { + public String resolveMethod(RequestContext context) { + return "increment"; + } + }; + action.setMethodResolver(methodResolver); + action.execute(context); + assertEquals(1, action.counter); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/action/ResultObjectEventFactoryTests.java b/spring-webflow/src/test/java/org/springframework/webflow/action/ResultObjectEventFactoryTests.java new file mode 100644 index 00000000..b0257777 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/action/ResultObjectEventFactoryTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import junit.framework.TestCase; + +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowSessionStatus; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Unit tests for {@link ResultObjectBasedEventFactory}. + */ +public class ResultObjectEventFactoryTests extends TestCase { + + private MockRequestContext context = new MockRequestContext(); + + private ResultObjectBasedEventFactory factory = new ResultObjectBasedEventFactory(); + + public void testAlreadyAnEvent() { + Event event = new Event(this, "event"); + Event result = factory.createResultEvent(this, event, context); + assertSame(event, result); + } + + public void testMappedTypes() { + assertTrue(factory.isMappedValueType(FlowSessionStatus.class)); + assertTrue(factory.isMappedValueType(boolean.class)); + assertTrue(factory.isMappedValueType(Boolean.class)); + assertTrue(factory.isMappedValueType(String.class)); + assertFalse(factory.isMappedValueType(Integer.class)); + } + + public void testNullResult() { + Event result = factory.createResultEvent(this, null, context); + assertEquals("null", result.getId()); + } + + public void testBooleanResult() { + Event result = factory.createResultEvent(this, Boolean.TRUE, context); + assertEquals("yes", result.getId()); + result = factory.createResultEvent(this, Boolean.FALSE, context); + assertEquals("no", result.getId()); + } + + public void testLabeledEnumResult() { + Event result = factory.createResultEvent(this, FlowSessionStatus.ACTIVE, context); + assertEquals("Active", result.getId()); + } + + public void testOtherResult() { + Event result = factory.createResultEvent(this, "hello", context); + assertEquals("hello", result.getId()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/action/SetActionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/action/SetActionTests.java new file mode 100644 index 00000000..507c8c5b --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/action/SetActionTests.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.binding.expression.SettableExpression; +import org.springframework.webflow.TestBean; +import org.springframework.webflow.TestBeanWithMap; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Unit tests for {@link SetAction}. + */ +public class SetActionTests extends TestCase { + + private ExpressionParser parser = DefaultExpressionParserFactory.getExpressionParser(); + + private MockRequestContext context = new MockRequestContext(); + + public void testSetActionWithBooleanValue() throws Exception { + context.getFlowScope().put("bean", new TestBean()); + + SettableExpression attr = parser.parseSettableExpression("bean.executed"); + Expression value = parser.parseExpression("true"); + SetAction action = new SetAction(attr, ScopeType.FLOW, value); + Event outcome = action.execute(context); + assertEquals("success", outcome.getId()); + assertEquals(true, ((TestBean)context.getFlowScope().get("bean")).executed); + } + + public void testSetActionWithStringValue() throws Exception { + SettableExpression attr = parser.parseSettableExpression("backState"); + Expression value = parser.parseExpression("'otherState'"); // ${'otherState'} also works + SetAction action = new SetAction(attr, ScopeType.FLOW, value); + assertEquals("success", action.execute(context).getId()); + assertEquals("otherState", context.getFlowScope().get("backState")); + } + + public void testSetActionWithValueFromMap() throws Exception { + TestBeanWithMap beanWithMap = new TestBeanWithMap(); + beanWithMap.getMap().put("key1", "value1"); + beanWithMap.getMap().put("key2", "value2"); + context.getFlowScope().put("beanWithMap", beanWithMap); + + SettableExpression attr = parser.parseSettableExpression("key"); + Expression value = parser.parseExpression("${flowScope.beanWithMap.map['key1']}"); + SetAction action = new SetAction(attr, ScopeType.FLASH, value); + assertEquals("success", action.execute(context).getId()); + assertEquals("value1", context.getFlashScope().get("key")); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/action/SuccessEventFactoryTests.java b/spring-webflow/src/test/java/org/springframework/webflow/action/SuccessEventFactoryTests.java new file mode 100644 index 00000000..656bff7c --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/action/SuccessEventFactoryTests.java @@ -0,0 +1,37 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import junit.framework.TestCase; + +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Unit tests for {@link SuccessEventFactory}. + */ +public class SuccessEventFactoryTests extends TestCase { + + private MockRequestContext context = new MockRequestContext(); + + private SuccessEventFactory factory = new SuccessEventFactory(); + + public void testDefaultAdaptionRules() { + Event result = factory.createResultEvent(this, "result", context); + assertEquals("success", result.getId()); + assertEquals("result", result.getAttributes().getString("result")); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/action/TestMultiAction.java b/spring-webflow/src/test/java/org/springframework/webflow/action/TestMultiAction.java new file mode 100644 index 00000000..bc52e1ef --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/action/TestMultiAction.java @@ -0,0 +1,47 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action; + +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +/** + * Simple multi-action implementation used by the unit tests. + * + * @author Erwin Vervaet + */ +public class TestMultiAction extends MultiAction { + + int counter = 0; + + public Event increment(RequestContext context) throws Exception { + counter++; + return success(); + } + + public Event decrement(RequestContext context) throws Exception { + counter--; + return success(); + } + + public int getCounter() { + return counter; + } + + public void setCounter(int counter) { + this.counter = counter; + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/action/portlet/SetPortletModeActionTest.java b/spring-webflow/src/test/java/org/springframework/webflow/action/portlet/SetPortletModeActionTest.java new file mode 100644 index 00000000..3db39e4f --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/action/portlet/SetPortletModeActionTest.java @@ -0,0 +1,90 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.action.portlet; + +import javax.portlet.PortletMode; + +import junit.framework.TestCase; + +import org.springframework.mock.web.portlet.MockActionResponse; +import org.springframework.mock.web.portlet.MockRenderResponse; +import org.springframework.webflow.context.portlet.PortletExternalContext; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Unit test for the {@link SetPortletModeAction} class. + * + * @author Ulrik Sandberg + */ +public class SetPortletModeActionTest extends TestCase { + + private SetPortletModeAction tested; + + protected void setUp() throws Exception { + super.setUp(); + tested = new SetPortletModeAction(); + } + + protected void tearDown() throws Exception { + super.tearDown(); + tested = null; + } + + public void testDoExecute() throws Exception { + MockActionResponse mockActionResponse = new MockActionResponse(); + PortletExternalContext externalContext = new PortletExternalContext(null, null, mockActionResponse); + MockRequestContext mockRequestContext = new MockRequestContext(); + mockRequestContext.setExternalContext(externalContext); + + // perform test + Event result = tested.doExecute(mockRequestContext); + + assertEquals(tested.getEventFactorySupport().getSuccessEventId(), result.getId()); + assertEquals(tested.getPortletMode(), mockActionResponse.getPortletMode()); + } + + public void testDoExecuteWithPortletModeAsAttribute() throws Exception { + MockActionResponse mockActionResponse = new MockActionResponse(); + PortletExternalContext externalContext = new PortletExternalContext(null, null, mockActionResponse); + MockRequestContext mockRequestContext = new MockRequestContext(); + mockRequestContext.setExternalContext(externalContext); + mockRequestContext.setAttribute(SetPortletModeAction.PORTLET_MODE_ATTRIBUTE, PortletMode.HELP); + + // perform test + Event result = tested.doExecute(mockRequestContext); + + assertEquals(tested.getEventFactorySupport().getSuccessEventId(), result.getId()); + assertEquals(PortletMode.HELP, mockActionResponse.getPortletMode()); + } + + public void testDoExecuteWithWrongResponseClass() throws Exception { + MockRenderResponse mockRenderResponse = new MockRenderResponse(); + PortletExternalContext externalContext = new PortletExternalContext(null, null, mockRenderResponse); + MockRequestContext mockRequestContext = new MockRequestContext(); + mockRequestContext.setExternalContext(externalContext); + mockRequestContext.setAttribute(SetPortletModeAction.PORTLET_MODE_ATTRIBUTE, PortletMode.HELP); + + // perform test + try { + tested.doExecute(mockRequestContext); + fail("ActionExecutionException expected"); + } + catch (IllegalStateException e) { + // expected + } + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/config/BeanConfigTests.java b/spring-webflow/src/test/java/org/springframework/webflow/config/BeanConfigTests.java new file mode 100644 index 00000000..5b590eee --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/config/BeanConfigTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.config; + +import junit.framework.TestCase; + +import org.springframework.beans.factory.BeanFactory; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.webflow.executor.mvc.FlowController; + +/** + * Test case that illustrates configuration of a FlowController and its + * associated artefacts using classic spring bean configuration information. + * This test case does not really test much but serves more as an example. + * + * @author Erwin Vervaet + */ +public class BeanConfigTests extends TestCase { + + private BeanFactory beanFactory; + + protected void setUp() throws Exception { + beanFactory = new ClassPathXmlApplicationContext("webflow-config-classic.xml", BeanConfigTests.class); + } + + public void testFlowControllerConfig() { + FlowController flowController = (FlowController)beanFactory.getBean("flowController"); + assertEquals("test-flow", flowController.getArgumentHandler().getDefaultFlowId()); + } + + public void testFlowControllerBeanConfig() { + FlowController flowController = (FlowController)beanFactory.getBean("flowController-bean"); + assertEquals("test-flow", flowController.getArgumentHandler().getDefaultFlowId()); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/config/FlowExecutorFactoryBeanTests.java b/spring-webflow/src/test/java/org/springframework/webflow/config/FlowExecutorFactoryBeanTests.java new file mode 100644 index 00000000..6da86115 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/config/FlowExecutorFactoryBeanTests.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.config; + +import junit.framework.TestCase; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; +import org.springframework.webflow.conversation.ConversationManager; +import org.springframework.webflow.conversation.impl.SessionBindingConversationManager; +import org.springframework.webflow.definition.registry.FlowDefinitionLocator; +import org.springframework.webflow.execution.repository.FlowExecutionRepository; +import org.springframework.webflow.execution.repository.continuation.ClientContinuationFlowExecutionRepository; +import org.springframework.webflow.execution.repository.continuation.ContinuationFlowExecutionRepository; +import org.springframework.webflow.execution.repository.support.SimpleFlowExecutionRepository; +import org.springframework.webflow.executor.FlowExecutorImpl; + +/** + * Test case for {@link FlowExecutorFactoryBean}. + * + * @author Erwin Vervaet + */ +public class FlowExecutorFactoryBeanTests extends TestCase { + + private ApplicationContext applicationContext; + + protected void setUp() throws Exception { + this.applicationContext = + new ClassPathXmlApplicationContext("flow-executor-factory-bean.xml", BeanConfigTests.class); + } + + public void testSetMaxConversationsAndMaxContinuations() { + FlowExecutorImpl flowExecutor = (FlowExecutorImpl)applicationContext.getBean("flowExecutor0"); + assertTrue(flowExecutor.getExecutionRepository() instanceof ContinuationFlowExecutionRepository); + ContinuationFlowExecutionRepository repo = + (ContinuationFlowExecutionRepository)flowExecutor.getExecutionRepository(); + SessionBindingConversationManager conversationManager = + (SessionBindingConversationManager)repo.getConversationManager(); + assertEquals(1, conversationManager.getMaxConversations()); + assertEquals(10, repo.getMaxContinuations()); + } + + public void testRepoConfigurationWithDefaultConversationManager() throws Exception { + SimpleFlowExecutionRepository simple = + (SimpleFlowExecutionRepository)setupRepo(RepositoryType.SIMPLE, null); + assertTrue(simple.getConversationManager() instanceof SessionBindingConversationManager); + assertTrue(simple.isAlwaysGenerateNewNextKey()); + + SimpleFlowExecutionRepository singleKey = + (SimpleFlowExecutionRepository)setupRepo(RepositoryType.SINGLEKEY, null); + assertTrue(singleKey.getConversationManager() instanceof SessionBindingConversationManager); + assertFalse(singleKey.isAlwaysGenerateNewNextKey()); + + ContinuationFlowExecutionRepository continuation = + (ContinuationFlowExecutionRepository)setupRepo(RepositoryType.CONTINUATION, null); + assertTrue(continuation.getConversationManager() instanceof SessionBindingConversationManager); + + ClientContinuationFlowExecutionRepository client = + (ClientContinuationFlowExecutionRepository)setupRepo(RepositoryType.CLIENT, null); + assertFalse( + "Client repo does not use SessionBindingConversationManager by default", + client.getConversationManager() instanceof SessionBindingConversationManager); + } + + public void testRepoConfiguration() throws Exception { + ConversationManager cm = new CustomConversationManager(); + + SimpleFlowExecutionRepository simple = + (SimpleFlowExecutionRepository)setupRepo(RepositoryType.SIMPLE, cm); + assertTrue(simple.getConversationManager() instanceof CustomConversationManager); + assertTrue(simple.isAlwaysGenerateNewNextKey()); + + SimpleFlowExecutionRepository singleKey = + (SimpleFlowExecutionRepository)setupRepo(RepositoryType.SINGLEKEY, cm); + assertTrue(singleKey.getConversationManager() instanceof CustomConversationManager); + assertFalse(singleKey.isAlwaysGenerateNewNextKey()); + + ContinuationFlowExecutionRepository continuation = + (ContinuationFlowExecutionRepository)setupRepo(RepositoryType.CONTINUATION, cm); + assertTrue(continuation.getConversationManager() instanceof CustomConversationManager); + + ClientContinuationFlowExecutionRepository client = + (ClientContinuationFlowExecutionRepository)setupRepo(RepositoryType.CLIENT, cm); + assertTrue(client.getConversationManager() instanceof CustomConversationManager); + } + + private FlowExecutionRepository setupRepo( + RepositoryType repoType, ConversationManager conversationManager) throws Exception { + FlowExecutorFactoryBean flowExecutorFactoryBean = new FlowExecutorFactoryBean(); + flowExecutorFactoryBean.setDefinitionLocator( + (FlowDefinitionLocator)applicationContext.getBean("flowRegistry")); + flowExecutorFactoryBean.setRepositoryType(repoType); + if (conversationManager != null) { + flowExecutorFactoryBean.setConversationManager(conversationManager); + } + flowExecutorFactoryBean.afterPropertiesSet(); + FlowExecutorImpl flowExecutor = (FlowExecutorImpl)flowExecutorFactoryBean.getObject(); + return flowExecutor.getExecutionRepository(); + } + + private static class CustomConversationManager extends SessionBindingConversationManager { + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/config/TestFlowExecutionListenerCriteria.java b/spring-webflow/src/test/java/org/springframework/webflow/config/TestFlowExecutionListenerCriteria.java new file mode 100644 index 00000000..706165a1 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/config/TestFlowExecutionListenerCriteria.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.config; + +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.execution.factory.FlowExecutionListenerCriteria; + +/** + * Dummy test implementation of a FlowExecutionListenerCriteria. + * Not intended for actual use. + * + * @author Erwin Vervaet + */ +public class TestFlowExecutionListenerCriteria implements FlowExecutionListenerCriteria { + + public boolean appliesTo(FlowDefinition definition) { + return definition.getAttributes().contains("dummy"); + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/config/WebFlowConfigNamespaceHandlerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/config/WebFlowConfigNamespaceHandlerTests.java new file mode 100644 index 00000000..c1a103e7 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/config/WebFlowConfigNamespaceHandlerTests.java @@ -0,0 +1,132 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.config; + +import junit.framework.TestCase; + +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; +import org.springframework.core.io.ClassPathResource; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.engine.support.ApplicationViewSelector; +import org.springframework.webflow.execution.factory.ConditionalFlowExecutionListenerLoader; +import org.springframework.webflow.execution.factory.StaticFlowExecutionListenerLoader; +import org.springframework.webflow.execution.repository.continuation.ClientContinuationFlowExecutionRepository; +import org.springframework.webflow.execution.repository.continuation.ContinuationFlowExecutionRepository; +import org.springframework.webflow.execution.repository.support.SimpleFlowExecutionRepository; +import org.springframework.webflow.executor.FlowExecutorImpl; + +/** + * Unit tests for the WebFlowConfigNamespaceHandler and its BeanDefinitionParsers. + * + * @author Ben Hale + * @author Erwin Vervaet + */ +public class WebFlowConfigNamespaceHandlerTests extends TestCase { + + private DefaultListableBeanFactory beanFactory; + + protected void setUp() throws Exception { + super.setUp(); + this.beanFactory = new DefaultListableBeanFactory(); + XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(this.beanFactory); + reader.loadBeanDefinitions(new ClassPathResource("org/springframework/webflow/config/webflow-config-namespace.xml")); + } + + public void testRegistryWithPath() { + FlowDefinitionRegistry registry = (FlowDefinitionRegistry)this.beanFactory.getBean("withPath"); + assertEquals("Incorrect number of flows loaded", 1, registry.getFlowDefinitionCount()); + } + + public void testRegistryWithoutPath() { + FlowDefinitionRegistry registry = (FlowDefinitionRegistry)this.beanFactory.getBean("withoutPath"); + assertEquals("Incorrect number of flows loaded", 0, registry.getFlowDefinitionCount()); + } + + public void testRegistryWithPathWithWildcards() { + FlowDefinitionRegistry registry = (FlowDefinitionRegistry)this.beanFactory.getBean("withPathWithWildcards"); + assertEquals("Incorrect number of flows loaded", 0, registry.getFlowDefinitionCount()); + } + + public void testDefaultExecutor() { + FlowExecutorImpl flowExecutor = (FlowExecutorImpl)this.beanFactory.getBean("defaultExecutor"); + assertSame(this.beanFactory.getBean("withPathWithWildcards"), flowExecutor.getDefinitionLocator()); + AttributeMap attribs = ((FlowExecutionImplFactory)flowExecutor.getExecutionFactory()).getExecutionAttributes(); + assertEquals(1, attribs.size()); // defaults have been applied + assertEquals(new Boolean(true), attribs.get(ApplicationViewSelector.ALWAYS_REDIRECT_ON_PAUSE_ATTRIBUTE)); + } + + public void testSimpleExecutor() { + FlowExecutorImpl flowExecutor = (FlowExecutorImpl)this.beanFactory.getBean("simpleExecutor"); + assertSame(this.beanFactory.getBean("withPathWithWildcards"), flowExecutor.getDefinitionLocator()); + assertTrue(flowExecutor.getExecutionRepository() instanceof SimpleFlowExecutionRepository); + assertTrue(((SimpleFlowExecutionRepository)flowExecutor.getExecutionRepository()).isAlwaysGenerateNewNextKey()); + AttributeMap attribs = ((FlowExecutionImplFactory)flowExecutor.getExecutionFactory()).getExecutionAttributes(); + assertEquals(3, attribs.size()); + assertEquals(new Boolean(true), attribs.get(ApplicationViewSelector.ALWAYS_REDIRECT_ON_PAUSE_ATTRIBUTE)); + assertEquals("test", attribs.get("test")); + assertEquals(new Integer(1), attribs.get("test1")); + assertSame(StaticFlowExecutionListenerLoader.EMPTY_INSTANCE, + ((FlowExecutionImplFactory)flowExecutor.getExecutionFactory()).getExecutionListenerLoader()); + } + + public void testContinuationExecutor() { + FlowExecutorImpl flowExecutor = (FlowExecutorImpl)this.beanFactory.getBean("continuationExecutor"); + assertSame(this.beanFactory.getBean("withPathWithWildcards"), flowExecutor.getDefinitionLocator()); + assertTrue(flowExecutor.getExecutionRepository() instanceof ContinuationFlowExecutionRepository); + AttributeMap attribs = ((FlowExecutionImplFactory)flowExecutor.getExecutionFactory()).getExecutionAttributes(); + assertEquals(1, attribs.size()); + assertEquals(new Boolean(false), attribs.get(ApplicationViewSelector.ALWAYS_REDIRECT_ON_PAUSE_ATTRIBUTE)); + ConditionalFlowExecutionListenerLoader ll = (ConditionalFlowExecutionListenerLoader) + ((FlowExecutionImplFactory)flowExecutor.getExecutionFactory()).getExecutionListenerLoader(); + assertEquals(1, ll.getListeners(new Flow("test")).length); + assertSame(this.beanFactory.getBean("listener1"), ll.getListeners(new Flow("test"))[0]); + } + + public void testClientExecutor() { + FlowExecutorImpl flowExecutor = (FlowExecutorImpl)this.beanFactory.getBean("clientExecutor"); + assertSame(this.beanFactory.getBean("withPathWithWildcards"), flowExecutor.getDefinitionLocator()); + assertTrue(flowExecutor.getExecutionRepository() instanceof ClientContinuationFlowExecutionRepository); + AttributeMap attribs = ((FlowExecutionImplFactory)flowExecutor.getExecutionFactory()).getExecutionAttributes(); + assertEquals(1, attribs.size()); + assertEquals(new Boolean(true), attribs.get(ApplicationViewSelector.ALWAYS_REDIRECT_ON_PAUSE_ATTRIBUTE)); + ConditionalFlowExecutionListenerLoader ll = (ConditionalFlowExecutionListenerLoader) + ((FlowExecutionImplFactory)flowExecutor.getExecutionFactory()).getExecutionListenerLoader(); + assertEquals(2, ll.getListeners(new Flow("flow1")).length); + assertSame(this.beanFactory.getBean("listener1"), ll.getListeners(new Flow("flow1"))[0]); + assertSame(this.beanFactory.getBean("listener2"), ll.getListeners(new Flow("flow1"))[1]); + assertEquals(1, ll.getListeners(new Flow("flow2")).length); + assertSame(this.beanFactory.getBean("listener2"), ll.getListeners(new Flow("flow2"))[0]); + assertEquals(1, ll.getListeners(new Flow("flow3")).length); + assertSame(this.beanFactory.getBean("listener2"), ll.getListeners(new Flow("flow3"))[0]); + } + + public void testSingleKeyExecutor() { + FlowExecutorImpl flowExecutor = (FlowExecutorImpl)this.beanFactory.getBean("singleKeyExecutor"); + assertSame(this.beanFactory.getBean("withPathWithWildcards"), flowExecutor.getDefinitionLocator()); + assertTrue(flowExecutor.getExecutionRepository() instanceof SimpleFlowExecutionRepository); + assertFalse(((SimpleFlowExecutionRepository)flowExecutor.getExecutionRepository()).isAlwaysGenerateNewNextKey()); + AttributeMap attribs = ((FlowExecutionImplFactory)flowExecutor.getExecutionFactory()).getExecutionAttributes(); + assertEquals(1, attribs.size()); + assertEquals(new Boolean(true), attribs.get(ApplicationViewSelector.ALWAYS_REDIRECT_ON_PAUSE_ATTRIBUTE)); + assertSame(StaticFlowExecutionListenerLoader.EMPTY_INSTANCE, + ((FlowExecutionImplFactory)flowExecutor.getExecutionFactory()).getExecutionListenerLoader()); + } + +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/config/flow-executor-factory-bean.xml b/spring-webflow/src/test/java/org/springframework/webflow/config/flow-executor-factory-bean.xml new file mode 100644 index 00000000..2d6cca7c --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/config/flow-executor-factory-bean.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/config/webflow-config-classic.xml b/spring-webflow/src/test/java/org/springframework/webflow/config/webflow-config-classic.xml new file mode 100644 index 00000000..cc86bd04 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/config/webflow-config-classic.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + + + + + + + + true + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/config/webflow-config-namespace.xml b/spring-webflow/src/test/java/org/springframework/webflow/config/webflow-config-namespace.xml new file mode 100644 index 00000000..05b8024d --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/config/webflow-config-namespace.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletContextMapTests.java b/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletContextMapTests.java new file mode 100644 index 00000000..9e2b41b9 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletContextMapTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.context.portlet; + +import java.util.Iterator; + +import junit.framework.TestCase; + +import org.springframework.mock.web.portlet.MockPortletContext; + +/** + * Unit test for the {@link PortletContextMap} class. + * + * @author Ulrik Sandberg + */ +public class PortletContextMapTests extends TestCase { + + private PortletContextMap tested; + + private MockPortletContext context; + + protected void setUp() throws Exception { + super.setUp(); + context = new MockPortletContext(); + tested = new PortletContextMap(context); + } + + protected void tearDown() throws Exception { + super.tearDown(); + context = null; + tested = null; + } + + public void testGetAttribute() { + context.setAttribute("Some key", "Some value"); + // perform test + Object result = tested.getAttribute("Some key"); + assertEquals("Some value", result); + } + + public void testSetAttribute() { + // perform test + tested.setAttribute("Some key", "Some value"); + assertEquals("Some value", context.getAttribute("Some key")); + } + + public void testRemoveAttribute() { + context.setAttribute("Some key", "Some value"); + // perform test + tested.removeAttribute("Some key"); + assertNull(context.getAttribute("Some key")); + } + + public void testGetAttributeNames() { + context.setAttribute("Some key", "Some value"); + context.removeAttribute("javax.servlet.context.tempdir"); + // perform test + Iterator names = tested.getAttributeNames(); + assertNotNull("Null result unexpected", names); + assertTrue("More elements", names.hasNext()); + String name = (String)names.next(); + assertEquals("Some key", name); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletExternalContextTests.java b/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletExternalContextTests.java new file mode 100644 index 00000000..bc48d1f3 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletExternalContextTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.context.portlet; + +import junit.framework.TestCase; + +import org.springframework.mock.web.portlet.MockPortletContext; +import org.springframework.mock.web.portlet.MockPortletRequest; +import org.springframework.mock.web.portlet.MockPortletResponse; + +/** + * Unit tests for {@link PortletExternalContext}. + */ +public class PortletExternalContextTests extends TestCase { + + private PortletExternalContext context = new PortletExternalContext(new MockPortletContext(), + new MockPortletRequest(), new MockPortletResponse()); + + public void testApplicationMap() { + assertEquals(1, context.getApplicationMap().size()); + context.getApplicationMap().put("foo", "bar"); + assertEquals("bar", context.getApplicationMap().get("foo")); + assertEquals("bar", context.getContext().getAttribute("foo")); + } + + public void testSessionMap() { + assertEquals(0, context.getSessionMap().size()); + context.getSessionMap().put("foo", "bar"); + assertEquals("bar", context.getSessionMap().get("foo")); + assertEquals("bar", context.getRequest().getPortletSession().getAttribute("foo")); + } + + public void testRequestMap() { + assertEquals(0, context.getRequestMap().size()); + context.getRequestMap().put("foo", "bar"); + assertEquals("bar", context.getRequestMap().get("foo")); + assertEquals("bar", context.getRequest().getAttribute("foo")); + } + + public void testOther() { + assertNull(context.getRequestPathInfo()); + assertNull(context.getDispatcherPath()); + assertNotNull(context.getResponse()); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletRequestMapTests.java b/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletRequestMapTests.java new file mode 100644 index 00000000..095394d2 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletRequestMapTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.context.portlet; + +import java.util.Iterator; + +import junit.framework.TestCase; + +import org.springframework.mock.web.portlet.MockPortletRequest; + +/** + * Unit test for the {@link PortletRequestMap} class. + * + * @author Ulrik Sandberg + */ +public class PortletRequestMapTests extends TestCase { + + private PortletRequestMap tested; + + private MockPortletRequest request; + + protected void setUp() throws Exception { + super.setUp(); + request = new MockPortletRequest(); + tested = new PortletRequestMap(request); + } + + protected void tearDown() throws Exception { + super.tearDown(); + request = null; + tested = null; + } + + public void testGetAttribute() { + request.setAttribute("Some key", "Some value"); + // perform test + Object result = tested.getAttribute("Some key"); + assertEquals("Some value", result); + } + + public void testSetAttribute() { + // perform test + tested.setAttribute("Some key", "Some value"); + assertEquals("Some value", request.getAttribute("Some key")); + } + + public void testRemoveAttribute() { + request.setAttribute("Some key", "Some value"); + // perform test + tested.removeAttribute("Some key"); + assertNull(request.getAttribute("Some key")); + } + + public void testGetAttributeNames() { + request.setAttribute("Some key", "Some value"); + request.removeAttribute("javax.servlet.context.tempdir"); + // perform test + Iterator names = tested.getAttributeNames(); + assertNotNull("Null result unexpected", names); + assertTrue("More elements", names.hasNext()); + String name = (String)names.next(); + assertEquals("Some key", name); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletRequestParameterMapTests.java b/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletRequestParameterMapTests.java new file mode 100644 index 00000000..5c08476b --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletRequestParameterMapTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.context.portlet; + +import java.util.Iterator; + +import junit.framework.TestCase; + +import org.springframework.mock.web.portlet.MockPortletRequest; + +/** + * Unit test for the {@link PortletRequestParameterMap} class. + * + * @author Ulrik Sandberg + */ +public class PortletRequestParameterMapTests extends TestCase { + + private PortletRequestParameterMap tested; + + private MockPortletRequest request; + + protected void setUp() throws Exception { + super.setUp(); + request = new MockPortletRequest(); + tested = new PortletRequestParameterMap(request); + } + + protected void tearDown() throws Exception { + super.tearDown(); + request = null; + tested = null; + } + + public void testGetAttribute() { + request.setParameter("Some param", "Some value"); + // perform test + Object result = tested.getAttribute("Some param"); + assertEquals("Some value", result); + } + + public void testSetAttribute() { + // perform test + try { + tested.setAttribute("Some key", "Some value"); + fail("UnsupportedOperationException expected"); + } + catch (UnsupportedOperationException expected) { + // expected + } + } + + public void testRemoveAttribute() { + request.setParameter("Some param", "Some value"); + // perform test + try { + tested.removeAttribute("Some param"); + fail("UnsupportedOperationException expected"); + } + catch (UnsupportedOperationException expected) { + // expected + } + } + + public void testGetAttributeNames() { + request.setParameter("Some param", "Some value"); + // perform test + Iterator names = tested.getAttributeNames(); + assertNotNull("Null result unexpected", names); + assertTrue("More elements", names.hasNext()); + String name = (String)names.next(); + assertEquals("Some param", name); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletSessionMapTests.java b/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletSessionMapTests.java new file mode 100644 index 00000000..ff8e12de --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/portlet/PortletSessionMapTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.context.portlet; + +import java.util.Iterator; + +import javax.portlet.PortletSession; + +import junit.framework.TestCase; + +import org.springframework.mock.web.portlet.MockPortletRequest; +import org.springframework.web.util.WebUtils; + +/** + * Unit test for the {@link PortletSessionMap} class. + * + * @author Ulrik Sandberg + */ +public class PortletSessionMapTests extends TestCase { + + private PortletSessionMap tested; + + private MockPortletRequest request; + + protected void setUp() throws Exception { + super.setUp(); + request = new MockPortletRequest(); + tested = new PortletSessionMap(request, PortletSession.PORTLET_SCOPE); + } + + protected void tearDown() throws Exception { + super.tearDown(); + request = null; + tested = null; + } + + public void testGetAttribute() { + request.getPortletSession().setAttribute("Some key", "Some value"); + // perform test + Object result = tested.getAttribute("Some key"); + assertEquals("Some value", result); + } + + public void testGetAttributeNullSession() { + request.setSession(null); + // perform test + Object result = tested.getAttribute("Some key"); + assertNull("No value expected", result); + } + + public void testSetAttribute() { + // perform test + tested.setAttribute("Some key", "Some value"); + assertEquals("Some value", request.getPortletSession().getAttribute("Some key")); + } + + public void testRemoveAttribute() { + request.getPortletSession().setAttribute("Some key", "Some value"); + // perform test + tested.removeAttribute("Some key"); + assertNull(request.getPortletSession().getAttribute("Some key")); + } + + public void testRemoveAttributeNullSession() { + request.setSession(null); + // perform test + tested.removeAttribute("Some key"); + assertNull(request.getPortletSession().getAttribute("Some key")); + } + + public void testGetAttributeNames() { + request.getPortletSession().setAttribute("Some key", "Some value"); + // perform test + Iterator names = tested.getAttributeNames(); + assertNotNull("Null result unexpected", names); + assertTrue("More elements", names.hasNext()); + String name = (String)names.next(); + assertEquals("Some key", name); + } + + public void testGetAttributeNamesNullSession() { + request.setSession(null); + // perform test + Iterator names = tested.getAttributeNames(); + assertNotNull("Null result unexpected", names); + assertFalse("No elements expected", names.hasNext()); + } + + public void testGetSessionAsMutex() { + Object mutex = tested.getMutex(); + assertSame(mutex, request.getPortletSession()); + } + + public void testGetSessionMutex() { + Object object = new Object(); + request.getPortletSession().setAttribute(WebUtils.SESSION_MUTEX_ATTRIBUTE, object); + Object mutex = tested.getMutex(); + assertSame(mutex, object); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpServletContextMapTests.java b/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpServletContextMapTests.java new file mode 100644 index 00000000..003e31b9 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpServletContextMapTests.java @@ -0,0 +1,124 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.context.servlet; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import junit.framework.TestCase; + +import org.springframework.mock.web.MockServletContext; + +/** + * Test case for the {@link HttpServletContextMap} class. + * + * @author Ulrik Sandberg + * @author Erwin Vervaet + */ +public class HttpServletContextMapTests extends TestCase { + + private HttpServletContextMap tested; + + private MockServletContext context; + + protected void setUp() throws Exception { + super.setUp(); + context = new MockServletContext(); + // a fresh MockServletContext seems to already contain an element; + // that's confusing, so we remove it + context.removeAttribute("javax.servlet.context.tempdir"); + tested = new HttpServletContextMap(context); + tested.put("SomeKey", "SomeValue"); + } + + protected void tearDown() throws Exception { + super.tearDown(); + context = null; + tested = null; + } + + public void testIsEmpty() { + tested.remove("SomeKey"); + assertEquals("size,", 0, tested.size()); + assertEquals("isEmpty,", true, tested.isEmpty()); + } + + public void testSizeAddOne() { + assertEquals("size,", 1, tested.size()); + } + + public void testSizeAddTwo() { + tested.put("SomeOtherKey", "SomeOtherValue"); + assertEquals("size,", 2, tested.size()); + } + + public void testContainsKey() { + assertEquals("containsKey,", true, tested.containsKey("SomeKey")); + } + + public void testContainsValue() { + assertTrue(tested.containsValue("SomeValue")); + } + + public void testGet() { + assertEquals("get,", "SomeValue", tested.get("SomeKey")); + } + + public void testPut() { + Object old = tested.put("SomeKey", "SomeNewValue"); + + assertEquals("old value,", "SomeValue", old); + assertEquals("new value,", "SomeNewValue", tested.get("SomeKey")); + } + + public void testRemove() { + Object old = tested.remove("SomeKey"); + + assertEquals("old value,", "SomeValue", old); + assertNull("should be gone", tested.get("SomeKey")); + } + + public void testPutAll() { + Map otherMap = new HashMap(); + otherMap.put("SomeOtherKey", "SomeOtherValue"); + otherMap.put("SomeKey", "SomeUpdatedValue"); + tested.putAll(otherMap); + assertEquals("SomeOtherValue", tested.get("SomeOtherKey")); + assertEquals("SomeUpdatedValue", tested.get("SomeKey")); + } + + public void testClear() { + tested.clear(); + assertTrue(tested.isEmpty()); + } + + public void testKeySet() { + assertEquals(1, tested.keySet().size()); + assertTrue(tested.keySet().contains("SomeKey")); + } + + public void testValues() { + assertEquals(1, tested.values().size()); + assertTrue(tested.values().contains("SomeValue")); + } + + public void testEntrySet() { + assertEquals(1, tested.entrySet().size()); + assertEquals("SomeKey", ((Entry)tested.entrySet().iterator().next()).getKey()); + assertEquals("SomeValue", ((Entry)tested.entrySet().iterator().next()).getValue()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpServletRequestMapTests.java b/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpServletRequestMapTests.java new file mode 100644 index 00000000..a48fd53f --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpServletRequestMapTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.context.servlet; + +import java.util.Iterator; + +import junit.framework.TestCase; + +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * Unit test for the {@link HttpServletRequestMap} class. + * + * @author Ulrik Sandberg + */ +public class HttpServletRequestMapTests extends TestCase { + + private HttpServletRequestMap tested; + + private MockHttpServletRequest request; + + protected void setUp() throws Exception { + super.setUp(); + request = new MockHttpServletRequest(); + tested = new HttpServletRequestMap(request); + } + + protected void tearDown() throws Exception { + super.tearDown(); + request = null; + tested = null; + } + + public void testGetAttribute() { + request.setAttribute("Some key", "Some value"); + // perform test + Object result = tested.getAttribute("Some key"); + assertEquals("Some value", result); + } + + public void testSetAttribute() { + // perform test + tested.setAttribute("Some key", "Some value"); + assertEquals("Some value", request.getAttribute("Some key")); + } + + public void testRemoveAttribute() { + request.setAttribute("Some key", "Some value"); + // perform test + tested.removeAttribute("Some key"); + assertNull(request.getAttribute("Some key")); + } + + public void testGetAttributeNames() { + request.setAttribute("Some key", "Some value"); + request.removeAttribute("javax.servlet.context.tempdir"); + // perform test + Iterator names = tested.getAttributeNames(); + assertNotNull("Null result unexpected", names); + assertTrue("More elements", names.hasNext()); + String name = (String)names.next(); + assertEquals("Some key", name); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpServletRequestParameterMapTests.java b/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpServletRequestParameterMapTests.java new file mode 100644 index 00000000..8f075fa4 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpServletRequestParameterMapTests.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.context.servlet; + +import java.util.Iterator; + +import junit.framework.TestCase; + +import org.springframework.mock.web.MockHttpServletRequest; + +/** + * Unit test for the {@link HttpServletRequestParameterMap} class. + * + * @author Ulrik Sandberg + */ +public class HttpServletRequestParameterMapTests extends TestCase { + + private HttpServletRequestParameterMap tested; + + private MockHttpServletRequest request; + + protected void setUp() throws Exception { + super.setUp(); + request = new MockHttpServletRequest(); + tested = new HttpServletRequestParameterMap(request); + } + + protected void tearDown() throws Exception { + super.tearDown(); + request = null; + tested = null; + } + + public void testGetAttribute() { + request.setParameter("Some param", "Some value"); + // perform test + Object result = tested.getAttribute("Some param"); + assertEquals("Some value", result); + } + + public void testSetAttribute() { + // perform test + try { + tested.setAttribute("Some key", "Some value"); + fail("UnsupportedOperationException expected"); + } + catch (UnsupportedOperationException expected) { + // expected + } + } + + public void testRemoveAttribute() { + request.setParameter("Some param", "Some value"); + // perform test + try { + tested.removeAttribute("Some param"); + fail("UnsupportedOperationException expected"); + } + catch (UnsupportedOperationException expected) { + // expected + } + } + + public void testGetAttributeNames() { + request.setParameter("Some param", "Some value"); + // perform test + Iterator names = tested.getAttributeNames(); + assertNotNull("Null result unexpected", names); + assertTrue("More elements", names.hasNext()); + String name = (String)names.next(); + assertEquals("Some param", name); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpSessionMapBindingListenerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpSessionMapBindingListenerTests.java new file mode 100644 index 00000000..edaea63a --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpSessionMapBindingListenerTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.context.servlet; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; + +import junit.framework.TestCase; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.webflow.core.collection.AttributeMapBindingEvent; +import org.springframework.webflow.core.collection.AttributeMapBindingListener; + +/** + * Unit tests for {@link HttpSessionMapBindingListener}. + * + * @author Erwin Vervaet + */ +public class HttpSessionMapBindingListenerTests extends TestCase { + + private HttpServletRequest request; + private HttpSession session; + private TestAttributeMapBindingListener value; + + protected void setUp() throws Exception { + request = new MockHttpServletRequest(); + session = request.getSession(true); + value = new TestAttributeMapBindingListener(); + } + + public void testValueBoundUnBound() { + value.valueBoundEvent = null; + value.valueUnboundEvent = null; + session.setAttribute("key", new HttpSessionMapBindingListener(value, new HttpSessionMap(request))); + assertNotNull(value.valueBoundEvent); + assertNull(value.valueUnboundEvent); + value.valueBoundEvent = null; + value.valueUnboundEvent = null; + session.removeAttribute("key"); + assertNull(value.valueBoundEvent); + assertNotNull(value.valueUnboundEvent); + } + + private static class TestAttributeMapBindingListener implements AttributeMapBindingListener { + + public AttributeMapBindingEvent valueBoundEvent; + public AttributeMapBindingEvent valueUnboundEvent; + + public void valueBound(AttributeMapBindingEvent event) { + this.valueBoundEvent = event; + assertEquals("key", event.getAttributeName()); + assertSame(event.getAttributeValue(), this); + } + + public void valueUnbound(AttributeMapBindingEvent event) { + this.valueUnboundEvent = event; + assertEquals("key", event.getAttributeName()); + assertSame(event.getAttributeValue(), this); + } + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpSessionMapTests.java b/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpSessionMapTests.java new file mode 100644 index 00000000..d26e44ea --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/HttpSessionMapTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.context.servlet; + +import java.util.Iterator; + +import junit.framework.TestCase; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.web.util.WebUtils; + +/** + * Unit test for the {@link HttpSessionMap} class. + * + * @author Ulrik Sandberg + */ +public class HttpSessionMapTests extends TestCase { + + private HttpSessionMap tested; + + private MockHttpServletRequest request; + + protected void setUp() throws Exception { + super.setUp(); + request = new MockHttpServletRequest(); + tested = new HttpSessionMap(request); + } + + protected void tearDown() throws Exception { + super.tearDown(); + request = null; + tested = null; + } + + public void testGetAttribute() { + request.getSession().setAttribute("Some key", "Some value"); + // perform test + Object result = tested.getAttribute("Some key"); + assertEquals("Some value", result); + } + + public void testGetAttributeNullSession() { + request.setSession(null); + // perform test + Object result = tested.getAttribute("Some key"); + assertNull("No value expected", result); + } + + public void testSetAttribute() { + // perform test + tested.setAttribute("Some key", "Some value"); + assertEquals("Some value", request.getSession().getAttribute("Some key")); + } + + public void testRemoveAttribute() { + request.getSession().setAttribute("Some key", "Some value"); + // perform test + tested.removeAttribute("Some key"); + assertNull(request.getSession().getAttribute("Some key")); + } + + public void testRemoveAttributeNullSession() { + request.setSession(null); + // perform test + tested.removeAttribute("Some key"); + assertNull(request.getSession().getAttribute("Some key")); + } + + public void testGetAttributeNames() { + request.getSession().setAttribute("Some key", "Some value"); + // perform test + Iterator names = tested.getAttributeNames(); + assertNotNull("Null result unexpected", names); + assertTrue("More elements", names.hasNext()); + String name = (String)names.next(); + assertEquals("Some key", name); + } + + public void testGetAttributeNamesNullSession() { + request.setSession(null); + // perform test + Iterator names = tested.getAttributeNames(); + assertNotNull("Null result unexpected", names); + assertFalse("No elements expected", names.hasNext()); + } + + public void testGetSessionAsMutex() { + Object mutex = tested.getMutex(); + assertSame(mutex, request.getSession()); + } + + public void testGetSessionMutex() { + Object object = new Object(); + request.getSession().setAttribute(WebUtils.SESSION_MUTEX_ATTRIBUTE, object); + Object mutex = tested.getMutex(); + assertSame(mutex, object); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/ServletExternalContextTests.java b/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/ServletExternalContextTests.java new file mode 100644 index 00000000..bfbf61dd --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/context/servlet/ServletExternalContextTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.context.servlet; + +import junit.framework.TestCase; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; + +/** + * Unit tests for {@link ServletExternalContext}. + */ +public class ServletExternalContextTests extends TestCase { + + private ServletExternalContext context = new ServletExternalContext(new MockServletContext(), + new MockHttpServletRequest(), new MockHttpServletResponse()); + + public void testApplicationMap() { + assertEquals(1, context.getApplicationMap().size()); + context.getApplicationMap().put("foo", "bar"); + assertEquals("bar", context.getApplicationMap().get("foo")); + assertEquals("bar", context.getContext().getAttribute("foo")); + } + + public void testSessionMap() { + assertEquals(0, context.getSessionMap().size()); + context.getSessionMap().put("foo", "bar"); + assertEquals("bar", context.getSessionMap().get("foo")); + assertEquals("bar", context.getRequest().getSession().getAttribute("foo")); + } + + public void testRequestMap() { + assertEquals(0, context.getRequestMap().size()); + context.getRequestMap().put("foo", "bar"); + assertEquals("bar", context.getRequestMap().get("foo")); + assertEquals("bar", context.getRequest().getAttribute("foo")); + } + + public void testOther() { + assertEquals(null, context.getRequestPathInfo()); + assertEquals("", context.getDispatcherPath()); + assertNotNull(context.getResponse()); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/conversation/impl/SessionBindingConversationManagerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/conversation/impl/SessionBindingConversationManagerTests.java new file mode 100644 index 00000000..b3744ddc --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/conversation/impl/SessionBindingConversationManagerTests.java @@ -0,0 +1,135 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.conversation.impl; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import junit.framework.TestCase; + +import org.springframework.webflow.context.ExternalContextHolder; +import org.springframework.webflow.conversation.Conversation; +import org.springframework.webflow.conversation.ConversationException; +import org.springframework.webflow.conversation.ConversationId; +import org.springframework.webflow.conversation.ConversationParameters; +import org.springframework.webflow.core.collection.SharedAttributeMap; +import org.springframework.webflow.test.MockExternalContext; + +/** + * Unit tests for {@link SessionBindingConversationManager}. + */ +public class SessionBindingConversationManagerTests extends TestCase { + + private SessionBindingConversationManager conversationManager; + + protected void setUp() throws Exception { + conversationManager = new SessionBindingConversationManager(); + } + + protected void tearDown() throws Exception { + ExternalContextHolder.setExternalContext(null); + } + + public void testConversationLifeCycle() { + ExternalContextHolder.setExternalContext(new MockExternalContext()); + Conversation conversation = conversationManager.beginConversation( + new ConversationParameters("test", "test", "test")); + ConversationId conversationId = conversation.getId(); + assertNotNull(conversationManager.getConversation(conversationId)); + conversation.end(); + try { + conversationManager.getConversation(conversationId); + fail("Conversation should have ben removed"); + } + catch (ConversationException e) { + } + } + + public void testNoPassivation() { + ExternalContextHolder.setExternalContext(new MockExternalContext()); + Conversation conversation = conversationManager.beginConversation( + new ConversationParameters("test", "test", "test")); + conversation.putAttribute("testAttribute", "testValue"); + ConversationId conversationId = conversation.getId(); + + Conversation conversation2 = conversationManager.getConversation(conversationId); + assertSame(conversation, conversation2); + assertEquals("testValue", conversation2.getAttribute("testAttribute")); + conversation.end(); + } + + public void testPassivation() throws Exception { + MockExternalContext externalContext = new MockExternalContext(); + ExternalContextHolder.setExternalContext(externalContext); + Conversation conversation = conversationManager.beginConversation( + new ConversationParameters("test", "test", "test")); + conversation.putAttribute("testAttribute", "testValue"); + ConversationId conversationId = conversation.getId(); + ExternalContextHolder.setExternalContext(null); + byte[] passiveSession = passivate(externalContext.getSessionMap()); + + String id = conversationId.toString(); + conversationId = conversationManager.parseConversationId(id); + + externalContext.setSessionMap(activate(passiveSession)); + ExternalContextHolder.setExternalContext(externalContext); + Conversation conversation2 = conversationManager.getConversation(conversationId); + assertNotSame(conversation, conversation2); + assertEquals("testValue", conversation2.getAttribute("testAttribute")); + conversation.end(); + } + + public void testMaxConversations() { + conversationManager.setMaxConversations(2); + ExternalContextHolder.setExternalContext(new MockExternalContext()); + Conversation conversation1 = conversationManager.beginConversation( + new ConversationParameters("test", "test", "test")); + conversation1.lock(); + assertNotNull(conversationManager.getConversation(conversation1.getId())); + Conversation conversation2 = conversationManager.beginConversation( + new ConversationParameters("test", "test", "test")); + assertNotNull(conversationManager.getConversation(conversation1.getId())); + assertNotNull(conversationManager.getConversation(conversation2.getId())); + Conversation conversation3 = conversationManager.beginConversation( + new ConversationParameters("test", "test", "test")); + try { + conversation1.end(); + conversation1.unlock(); + conversationManager.getConversation(conversation1.getId()); + fail(); + } + catch (ConversationException e) { + } + assertNotNull(conversationManager.getConversation(conversation2.getId())); + assertNotNull(conversationManager.getConversation(conversation3.getId())); + } + + private byte[] passivate(SharedAttributeMap session) throws Exception { + // session is serialized out + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + ObjectOutputStream oout = new ObjectOutputStream(bout); + oout.writeObject(session); + return bout.toByteArray(); + } + + private SharedAttributeMap activate(byte[] sessionData) throws Exception { + // session is serialized back in + return (SharedAttributeMap)new ObjectInputStream(new ByteArrayInputStream(sessionData)).readObject(); + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/core/DefaultExpressionParserFactoryTests.java b/spring-webflow/src/test/java/org/springframework/webflow/core/DefaultExpressionParserFactoryTests.java new file mode 100644 index 00000000..7850bdba --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/core/DefaultExpressionParserFactoryTests.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.ExpressionParser; + +/** + * Unit tests for {@link DefaultExpressionParserFactory}. + */ +public class DefaultExpressionParserFactoryTests extends TestCase { + + public void testGetDefaultExpressionParser() { + ExpressionParser parser = DefaultExpressionParserFactory.getExpressionParser(); + assertNotNull(parser); + assertTrue(parser instanceof WebFlowOgnlExpressionParser); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/core/WebFlowOgnlExpressionParserTests.java b/spring-webflow/src/test/java/org/springframework/webflow/core/WebFlowOgnlExpressionParserTests.java new file mode 100644 index 00000000..e0bc5caa --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/core/WebFlowOgnlExpressionParserTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.springframework.binding.collection.MapAdaptable; +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.SettableExpression; +import org.springframework.webflow.core.collection.LocalAttributeMap; + +/** + * Unit tests for {@link WebFlowOgnlExpressionParser}. + */ +public class WebFlowOgnlExpressionParserTests extends TestCase { + + WebFlowOgnlExpressionParser parser = new WebFlowOgnlExpressionParser(); + + public void testEvalSimpleExpression() { + ArrayList list = new ArrayList(); + Expression exp = parser.parseExpression("size()"); + Integer size = (Integer)exp.evaluate(list, null); + assertEquals(0, size.intValue()); + } + + public void testEvalMapAdaptable() { + MapAdaptable adaptable = new MapAdaptable() { + public Map asMap() { + HashMap map = new HashMap(); + map.put("size", new Integer(0)); + return map; + } + }; + Expression exp = parser.parseExpression("size"); + Integer size = (Integer)exp.evaluate(adaptable, null); + assertEquals(0, size.intValue()); + } + + public void testEvalAndSetMutableMap() { + LocalAttributeMap map = new LocalAttributeMap(); + map.put("size", new Integer(0)); + Expression exp = parser.parseExpression("size"); + Integer size = (Integer)exp.evaluate(map, null); + assertEquals(0, size.intValue()); + assertTrue(exp instanceof SettableExpression); + SettableExpression sexp = (SettableExpression)exp; + sexp.evaluateToSet(map, new Integer(1), null); + size = (Integer)exp.evaluate(map, null); + assertEquals(1, size.intValue()); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/core/collection/CollectionUtilsTests.java b/spring-webflow/src/test/java/org/springframework/webflow/core/collection/CollectionUtilsTests.java new file mode 100644 index 00000000..2cef1744 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/core/collection/CollectionUtilsTests.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core.collection; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link CollectionUtils}. + */ +public class CollectionUtilsTests extends TestCase { + + public void testSingleEntryMap() { + AttributeMap map1 = CollectionUtils.singleEntryMap("foo", "bar"); + AttributeMap map2 = CollectionUtils.singleEntryMap("foo", "bar"); + assertEquals(map1, map2); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/core/collection/LocalAttributeMapTests.java b/spring-webflow/src/test/java/org/springframework/webflow/core/collection/LocalAttributeMapTests.java new file mode 100644 index 00000000..145cc67e --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/core/collection/LocalAttributeMapTests.java @@ -0,0 +1,338 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core.collection; + +import java.math.BigDecimal; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link LocalAttributeMap}. + */ +public class LocalAttributeMapTests extends TestCase { + + private LocalAttributeMap attributeMap = new LocalAttributeMap(); + + public void setUp() { + attributeMap.put("string", "A string"); + attributeMap.put("integer", new Integer(12345)); + attributeMap.put("boolean", Boolean.TRUE); + attributeMap.put("long", new Long(12345)); + attributeMap.put("double", new Double(12345)); + attributeMap.put("float", new Float(12345)); + attributeMap.put("bigDecimal", new BigDecimal("12345.67")); + attributeMap.put("bean", new TestBean()); + attributeMap.put("stringArray", new String[] { "1", "2", "3" }); + attributeMap.put("collection", new LinkedList()); + } + + public void testGet() { + TestBean bean = (TestBean)attributeMap.get("bean"); + assertNotNull(bean); + } + + public void testGetNull() { + TestBean bean = (TestBean)attributeMap.get("bogus"); + assertNull(bean); + } + + public void testGetRequiredType() { + TestBean bean = (TestBean)attributeMap.get("bean", TestBean.class); + assertNotNull(bean); + } + + public void testGetWrongType() { + try { + attributeMap.get("bean", String.class); + fail("Should've failed iae"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testGetWithDefaultOption() { + TestBean d = new TestBean(); + TestBean bean = (TestBean)attributeMap.get("bean", d); + assertNotNull(bean); + assertNotSame(bean, d); + } + + public void testGetWithDefault() { + TestBean d = new TestBean(); + TestBean bean = (TestBean)attributeMap.get("bogus", d); + assertSame(bean, d); + } + + public void testGetRequired() { + TestBean bean = (TestBean)attributeMap.getRequired("bean"); + assertNotNull(bean); + } + + public void testGetRequiredNotPresent() { + try { + attributeMap.getRequired("bogus"); + fail("Should've failed iae"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testGetRequiredOfType() { + TestBean bean = (TestBean)attributeMap.getRequired("bean", TestBean.class); + assertNotNull(bean); + } + + public void testGetRequiredWrongType() { + try { + attributeMap.getRequired("bean", String.class); + fail("Should've failed iae"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testGetNumber() { + BigDecimal bd = (BigDecimal)attributeMap.getNumber("bigDecimal", BigDecimal.class); + assertEquals(new BigDecimal("12345.67"), bd); + } + + public void testGetNumberWrongType() { + try { + attributeMap.getNumber("bigDecimal", Integer.class); + fail("Should've failed iae"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testGetNumberWithDefaultOption() { + BigDecimal d = new BigDecimal("1"); + BigDecimal bd = (BigDecimal)attributeMap.getNumber("bigDecimal", BigDecimal.class, d); + assertEquals(new BigDecimal("12345.67"), bd); + assertNotSame(d, bd); + } + + public void testGetNumberWithDefault() { + BigDecimal d = new BigDecimal("1"); + BigDecimal bd = (BigDecimal)attributeMap.getNumber("bogus", BigDecimal.class, d); + assertEquals(d, bd); + assertSame(d, bd); + } + + public void testGetNumberRequired() { + BigDecimal bd = (BigDecimal)attributeMap.getRequiredNumber("bigDecimal", BigDecimal.class); + assertEquals(new BigDecimal("12345.67"), bd); + } + + public void testGetNumberRequiredNotPresent() { + try { + attributeMap.getRequiredNumber("bogus", BigDecimal.class); + fail("Should've failed iae"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testGetInteger() { + Integer i = attributeMap.getInteger("integer"); + assertEquals(new Integer(12345), i); + } + + public void testGetIntegerNull() { + Integer i = attributeMap.getInteger("bogus"); + assertNull(i); + } + + public void testGetIntegerRequired() { + Integer i = attributeMap.getRequiredInteger("integer"); + assertEquals(new Integer(12345), i); + } + + public void testGetIntegerRequiredNotPresent() { + try { + attributeMap.getRequiredInteger("bogus"); + fail("Should've failed iae"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testGetLong() { + Long i = attributeMap.getLong("long"); + assertEquals(new Long(12345), i); + } + + public void testGetLongNull() { + Long i = attributeMap.getLong("bogus"); + assertNull(i); + } + + public void testGetLongRequired() { + Long i = attributeMap.getRequiredLong("long"); + assertEquals(new Long(12345), i); + } + + public void testGetLongRequiredNotPresent() { + try { + attributeMap.getRequiredLong("bogus"); + fail("Should've failed iae"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testGetString() { + String i = attributeMap.getString("string"); + assertEquals("A string", i); + } + + public void testGetStringNull() { + String i = attributeMap.getString("bogus"); + assertNull(i); + } + + public void testGetStringRequired() { + String i = attributeMap.getRequiredString("string"); + assertEquals("A string", i); + } + + public void testGetStringRequiredNotPresent() { + try { + attributeMap.getRequiredString("bogus"); + fail("Should've failed iae"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testGetBoolean() { + Boolean i = attributeMap.getBoolean("boolean"); + assertEquals(Boolean.TRUE, i); + } + + public void testGetBooleanNull() { + Boolean i = attributeMap.getBoolean("bogus"); + assertNull(i); + } + + public void testGetBooleanRequired() { + Boolean i = attributeMap.getRequiredBoolean("boolean"); + assertEquals(Boolean.TRUE, i); + } + + public void testGetBooleanRequiredNotPresent() { + try { + attributeMap.getRequiredBoolean("bogus"); + fail("Should've failed iae"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testGetArray() { + String[] i = (String[])attributeMap.getArray("stringArray", String[].class); + assertEquals(3, i.length); + } + + public void testGetArrayNull() { + String[] i = (String[])attributeMap.getArray("A bogus array", String[].class); + assertNull(i); + } + + public void testGetArrayRequired() { + String[] i = (String[])attributeMap.getRequiredArray("stringArray", String[].class); + assertEquals(3, i.length); + } + + public void testGetArrayRequiredNotPresent() { + try { + attributeMap.getRequiredArray("A bogus array", String[].class); + fail("Should've failed iae"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testGetCollection() { + LinkedList i = (LinkedList)attributeMap.getCollection("collection", List.class); + assertEquals(0, i.size()); + } + + public void testGetCollectionNull() { + LinkedList i = (LinkedList)attributeMap.getCollection("bogus", List.class); + assertNull(i); + } + + public void testGetCollectionRequired() { + LinkedList i = (LinkedList)attributeMap.getRequiredCollection("collection", List.class); + assertEquals(0, i.size()); + } + + public void testGetCollectionRequiredNotPresent() { + try { + attributeMap.getRequiredCollection("A bogus collection"); + fail("Should've failed iae"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testGetMap() { + Map map = attributeMap.asMap(); + assertEquals(10, map.size()); + } + + public void testUnion() { + LocalAttributeMap one = new LocalAttributeMap(); + one.put("foo", "bar"); + one.put("bar", "baz"); + + LocalAttributeMap two = new LocalAttributeMap(); + two.put("cat", "coz"); + two.put("bar", "boo"); + + AttributeMap three = one.union(two); + assertEquals(3, three.size()); + assertEquals("bar", three.get("foo")); + assertEquals("coz", three.get("cat")); + assertEquals("boo", three.get("bar")); + } + + public void testEquality() { + LocalAttributeMap map = new LocalAttributeMap(); + map.put("foo", "bar"); + + LocalAttributeMap map2 = new LocalAttributeMap(); + map2.put("foo", "bar"); + + assertEquals(map, map2); + } + +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/core/collection/LocalParameterMapTests.java b/spring-webflow/src/test/java/org/springframework/webflow/core/collection/LocalParameterMapTests.java new file mode 100644 index 00000000..3e369c7a --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/core/collection/LocalParameterMapTests.java @@ -0,0 +1,254 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core.collection; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.easymock.EasyMock; +import org.springframework.web.multipart.MultipartFile; + +/** + * Unit tests for {@link LocalParameterMap}. + */ +public class LocalParameterMapTests extends TestCase { + + private LocalParameterMap parameterMap; + + public void setUp() { + Map map = new HashMap(); + map.put("string", "A string"); + map.put("integer", "12345"); + map.put("boolean", "true"); + map.put("stringArray", new String[] { "1", "2", "3" }); + map.put("emptyArray", new String[0]); + map.put("multipartFile", EasyMock.createMock(MultipartFile.class)); + parameterMap = new LocalParameterMap(map); + } + + public void testSize() { + assertTrue(!parameterMap.isEmpty()); + assertEquals(6, parameterMap.size()); + } + + public void testGet() { + String value = parameterMap.get("string"); + assertEquals("A string", value); + } + + public void testGetNull() { + String value = parameterMap.get("bogus"); + assertNull(value); + } + + public void testGetRequired() { + String value = parameterMap.getRequired("string"); + assertEquals("A string", value); + } + + public void testGetRequiredWithConversion() { + Integer value = (Integer)parameterMap.getRequired("integer", Integer.class); + assertEquals(new Integer(12345), value); + } + + public void testGetRequiredNotPresent() { + try { + parameterMap.getRequired("bogus"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testGetWithDefaultOption() { + String value = parameterMap.get("string", "default"); + assertEquals("A string", value); + } + + public void testGetWithDefault() { + String value = parameterMap.get("bogus", "default"); + assertEquals("default", value); + } + + public void testGetWithDefaultAndConversion() { + Object value = parameterMap.get("bogus", Integer.class, new Integer(1)); + assertEquals(new Integer(1), value); + } + + public void testGetWithDefaultAndConversionNotAssignable() { + try { + parameterMap.get("bogus", Integer.class, "1"); + fail("'1' isn't a integer"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testGetArray() { + String[] value = parameterMap.getArray("stringArray"); + assertEquals(3, value.length); + } + + public void testGetEmptyArray() { + String[] array = parameterMap.getArray("emptyArray"); + assertEquals(0, array.length); + } + + public void testGetArrayNull() { + String[] value = parameterMap.getArray("bogus"); + assertNull(value); + } + + public void testGetArrayRequired() { + String[] value = parameterMap.getRequiredArray("stringArray"); + assertEquals(3, value.length); + } + + public void getArrayWithConversion() { + Integer[] values = (Integer[])parameterMap.getArray("stringArray", Integer.class); + assertEquals(new Integer(1), values[0]); + assertEquals(new Integer(2), values[1]); + assertEquals(new Integer(3), values[2]); + } + + public void testGetRequiredArrayNotPresent() { + try { + parameterMap.getRequiredArray("bogus"); + } + catch (IllegalArgumentException e) { + + } + } + + public void testGetSingleValueAsArray() { + String[] value = parameterMap.getArray("string"); + assertEquals(1, value.length); + assertEquals("A string", value[0]); + } + + public void testGetArrayAsSingleVaue() { + String value = parameterMap.get("stringArray"); + assertEquals("1", value); + } + + public void testGetEmptyArrayAsSingleVaue() { + String value = parameterMap.get("emptyArray"); + assertEquals(null, value); + } + + public void testGetConversion() { + Integer i = parameterMap.getInteger("integer"); + assertEquals(new Integer(12345), i); + } + + public void testGetArrayConversion() { + Integer[] i = (Integer[])parameterMap.getArray("stringArray", Integer.class); + assertEquals(i.length, 3); + assertEquals(new Integer(1), i[0]); + assertEquals(new Integer(2), i[1]); + assertEquals(new Integer(3), i[2]); + } + + public void getRequiredArrayWithConversion() { + Integer[] values = (Integer[])parameterMap.getRequiredArray("stringArray", Integer.class); + assertEquals(new Integer(1), values[0]); + assertEquals(new Integer(2), values[1]); + assertEquals(new Integer(3), values[2]); + } + + public void testGetNumber() { + Integer value = (Integer)parameterMap.getNumber("integer", Integer.class); + assertEquals(new Integer(12345), value); + } + + public void testGetRequiredNumber() { + Integer value = (Integer)parameterMap.getRequiredNumber("integer", Integer.class); + assertEquals(new Integer(12345), value); + } + + public void testGetNumberWithDefault() { + Integer value = (Integer)parameterMap.getNumber("bogus", Integer.class, new Integer(12345)); + assertEquals(new Integer(12345), value); + } + + public void testGetInteger() { + Integer value = parameterMap.getInteger("integer"); + assertEquals(new Integer(12345), value); + } + + public void testGetRequiredInteger() { + Integer value = parameterMap.getRequiredInteger("integer"); + assertEquals(new Integer(12345), value); + } + + public void testGetIntegerWithDefault() { + Integer value = parameterMap.getInteger("bogus", new Integer(12345)); + assertEquals(new Integer(12345), value); + } + + public void testGetLong() { + Long value = parameterMap.getLong("integer"); + assertEquals(new Long(12345), value); + } + + public void testGetRequiredLong() { + Long value = parameterMap.getRequiredLong("integer"); + assertEquals(new Long(12345), value); + } + + public void testGetLongWithDefault() { + Long value = parameterMap.getLong("bogus", new Long(12345)); + assertEquals(new Long(12345), value); + } + + public void testGetBoolean() { + Boolean value = parameterMap.getBoolean("boolean"); + assertEquals(Boolean.TRUE, value); + } + + public void testGetRequiredBoolean() { + Boolean value = parameterMap.getRequiredBoolean("boolean"); + assertEquals(Boolean.TRUE, value); + } + + public void testGetBooleanWithDefault() { + Boolean value = parameterMap.getBoolean("bogus", Boolean.TRUE); + assertEquals(Boolean.TRUE, value); + } + + public void testGetMultipart() { + MultipartFile file = parameterMap.getMultipartFile("multipartFile"); + assertNotNull(file); + } + + public void testGetRequiredMultipart() { + MultipartFile file = parameterMap.getRequiredMultipartFile("multipartFile"); + assertNotNull(file); + } + + public void testEquality() { + LocalParameterMap map1 = new LocalParameterMap(new HashMap(parameterMap.asMap())); + assertEquals(parameterMap, map1); + } + + public void testAsAttributeMap() { + AttributeMap map = parameterMap.asAttributeMap(); + assertEquals(map.asMap(), parameterMap.asMap()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/core/collection/TestBean.java b/spring-webflow/src/test/java/org/springframework/webflow/core/collection/TestBean.java new file mode 100644 index 00000000..ff068e28 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/core/collection/TestBean.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.core.collection; + +import java.io.Serializable; + +/** + * Test bean used in unit tests. + */ +public class TestBean implements Serializable { + + private int amount = 0; + + public int getAmount() { + return amount; + } + + public void setAmount(int amount) { + this.amount = amount; + } + + public boolean equals(Object o) { + if (!(o instanceof TestBean)) { + return false; + } + return amount == ((TestBean)o).amount; + } + + public int hashCode() { + return amount * 29; + } + + public String toString() { + return "[TestBean amount = " + amount + "]"; + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistryImplTests.java b/spring-webflow/src/test/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistryImplTests.java new file mode 100644 index 00000000..8469c50c --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/definition/registry/FlowDefinitionRegistryImplTests.java @@ -0,0 +1,141 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.definition.registry; + +import junit.framework.TestCase; + +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; + +/** + * Unit tests for {@link FlowDefinitionRegistryImpl}. + */ +public class FlowDefinitionRegistryImplTests extends TestCase { + + private FlowDefinitionRegistryImpl registry = new FlowDefinitionRegistryImpl(); + + private FlowDefinition fooFlow; + + protected void setUp() { + fooFlow = new FooFlow(); + } + + public void testEmptyRegistryAsserts() { + assertEquals(0, registry.getFlowDefinitionCount()); + assertEquals(0, registry.getFlowDefinitionIds().length); + assertEquals(0, registry.getFlowDefinitions().length); + } + + public void testNoSuchFlowDefinition() { + try { + registry.getFlowDefinition("bogus"); + fail("Should've bombed with NoSuchFlow"); + } + catch (NoSuchFlowDefinitionException e) { + + } + } + + 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.getFlowDefinition("foo").getId()); + } + + public void testRegisterFlowSameIds() { + registry.registerFlowDefinition(new StaticFlowDefinitionHolder(fooFlow)); + FooFlow newFlow = new FooFlow(); + registry.registerFlowDefinition(new StaticFlowDefinitionHolder(newFlow)); + assertEquals(1, registry.getFlowDefinitionCount()); + assertSame(newFlow, registry.getFlowDefinition("foo")); + } + + public void testRegisterMultipleFlows() { + registry.registerFlowDefinition(new StaticFlowDefinitionHolder(fooFlow)); + FooFlow newFlow = new FooFlow(); + newFlow.id = "bar"; + registry.registerFlowDefinition(new StaticFlowDefinitionHolder(newFlow)); + assertEquals(2, registry.getFlowDefinitionCount()); + assertSame(fooFlow, registry.getFlowDefinition("foo")); + assertSame(newFlow, registry.getFlowDefinition("bar")); + } + + public void testRefresh() { + testRegisterMultipleFlows(); + registry.refresh(); + assertEquals(2, registry.getFlowDefinitionCount()); + assertSame(fooFlow, registry.getFlowDefinition("foo")); + } + + public void testRefreshValidFlow() { + testRegisterMultipleFlows(); + registry.refresh("foo"); + assertEquals(2, registry.getFlowDefinitionCount()); + assertSame(fooFlow, registry.getFlowDefinition("foo")); + } + + public void testRefreshNoSuchFlow() { + testRegisterMultipleFlows(); + try { + registry.refresh("bogus"); + fail("Should've bombed with NoSuchFlow"); + } + catch (NoSuchFlowDefinitionException e) { + + } + } + + public void testParentHierarchy() { + testRegisterMultipleFlows(); + FlowDefinitionRegistryImpl child = new FlowDefinitionRegistryImpl(); + child.setParent(registry); + FooFlow fooFlow = new FooFlow(); + child.registerFlowDefinition(new StaticFlowDefinitionHolder(fooFlow)); + assertSame(fooFlow, child.getFlowDefinition("foo")); + assertEquals("bar", child.getFlowDefinition("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; + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/ActionExecutorTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/ActionExecutorTests.java new file mode 100644 index 00000000..ad0f63d4 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/ActionExecutorTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import junit.framework.TestCase; + +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowSessionStatus; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.TestAction; +import org.springframework.webflow.test.MockFlowSession; +import org.springframework.webflow.test.MockRequestContext; + +public class ActionExecutorTests extends TestCase { + + public void testBasicExecute() { + TestAction action = new TestAction(); + Event result = ActionExecutor.execute(action, new MockRequestContext()); + assertEquals("success", result.getId()); + } + + public void testExceptionWhileStarted() { + TestAction action = new TestAction() { + protected Event doExecute(RequestContext context) throws Exception { + throw new IllegalStateException("Oops"); + } + }; + try { + ActionExecutor.execute(action, new MockRequestContext()); + fail("Should've failed"); + } + catch (ActionExecutionException e) { + assertTrue(e.getCause() instanceof IllegalStateException); + } + } + + public void testExceptionWhileStarting() { + TestAction action = new TestAction() { + protected Event doExecute(RequestContext context) throws Exception { + throw new IllegalStateException("Oops"); + } + }; + MockRequestContext context = new MockRequestContext(); + MockFlowSession starting = new MockFlowSession(new Flow("flow")); + starting.setStatus(FlowSessionStatus.STARTING); + context.getMockFlowExecutionContext().setActiveSession(starting); + try { + ActionExecutor.execute(action, context); + fail("Should've failed"); + } + catch (ActionExecutionException e) { + assertTrue(e.getCause() instanceof IllegalStateException); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/ActionStateTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/ActionStateTests.java new file mode 100644 index 00000000..41a77161 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/ActionStateTests.java @@ -0,0 +1,113 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import junit.framework.TestCase; + +import org.springframework.webflow.engine.impl.FlowExecutionImpl; +import org.springframework.webflow.engine.support.DefaultTargetStateResolver; +import org.springframework.webflow.engine.support.EventIdTransitionCriteria; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.TestAction; +import org.springframework.webflow.test.MockExternalContext; + +/** + * Tests that each of the Flow state types execute as expected when entered. + * + * @author Keith Donald + */ +public class ActionStateTests extends TestCase { + + public void testActionStateSingleAction() { + Flow flow = new Flow("myFlow"); + ActionState state = new ActionState(flow, "actionState"); + state.getActionList().add(new TestAction()); + state.getTransitionSet().add(new Transition(on("success"), to("finish"))); + new EndState(flow, "finish"); + FlowExecution flowExecution = new FlowExecutionImpl(flow); + flowExecution.start(null, new MockExternalContext()); + assertEquals(1, ((TestAction)state.getActionList().get(0)).getExecutionCount()); + } + + public void testActionAttributesChain() { + Flow flow = new Flow("myFlow"); + ActionState state = new ActionState(flow, "actionState"); + state.getActionList().add(new TestAction("not mapped result")); + state.getActionList().add(new TestAction(null)); + state.getActionList().add(new TestAction("")); + state.getActionList().add(new TestAction("success")); + state.getTransitionSet().add(new Transition(on("success"), to("finish"))); + new EndState(flow, "finish"); + FlowExecution flowExecution = new FlowExecutionImpl(flow); + flowExecution.start(null, new MockExternalContext()); + Action[] actions = state.getActionList().toArray(); + for (int i = 0; i < actions.length; i++) { + TestAction action = (TestAction)actions[i]; + assertEquals(1, action.getExecutionCount()); + } + } + + public void testActionAttributesChainNoMatchingTransition() { + Flow flow = new Flow("myFlow"); + ActionState state = new ActionState(flow, "actionState"); + state.getActionList().add(new TestAction("not mapped result")); + state.getActionList().add(new TestAction(null)); + state.getActionList().add(new TestAction("")); + state.getActionList().add(new TestAction("yet another not mapped result")); + state.getTransitionSet().add(new Transition(on("success"), to("finish"))); + new EndState(flow, "finish"); + FlowExecution flowExecution = new FlowExecutionImpl(flow); + try { + flowExecution.start(null, new MockExternalContext()); + fail("Should not have matched to another state transition"); + } + catch (NoMatchingTransitionException e) { + // expected + } + } + + public void testActionAttributesChainNamedActions() { + Flow flow = new Flow("myFlow"); + ActionState state = new ActionState(flow, "actionState"); + state.getActionList().add(new AnnotatedAction(new TestAction("not mapped result"))); + state.getActionList().add(new AnnotatedAction(new TestAction(null))); + AnnotatedAction action3 = new AnnotatedAction(new TestAction("")); + action3.setName("action3"); + state.getActionList().add(action3); + AnnotatedAction action4 = new AnnotatedAction(new TestAction("success")); + action4.setName("action4"); + state.getActionList().add(action4); + state.getTransitionSet().add(new Transition(on("action4.success"), to("finish"))); + new EndState(flow, "finish"); + FlowExecution flowExecution = new FlowExecutionImpl(flow); + flowExecution.start(null, new MockExternalContext()); + assertTrue(!flowExecution.isActive()); + Action[] actions = state.getActionList().toArray(); + for (int i = 0; i < actions.length; i++) { + AnnotatedAction action = (AnnotatedAction)actions[i]; + assertEquals(1, ((TestAction)(action.getTargetAction())).getExecutionCount()); + } + } + + protected TransitionCriteria on(String event) { + return new EventIdTransitionCriteria(event); + } + + protected TargetStateResolver to(String stateId) { + return new DefaultTargetStateResolver(stateId); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/AnnotatedObjectTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/AnnotatedObjectTests.java new file mode 100644 index 00000000..0cf1b369 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/AnnotatedObjectTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import junit.framework.TestCase; + +public class AnnotatedObjectTests extends TestCase { + + private AnnotatedObject object = new Flow("foo"); + + public void testSetCaption() { + object.setCaption("caption"); + assertEquals("caption", object.getCaption()); + } + + public void testSetDescription() { + object.setDescription("description"); + assertEquals("description", object.getDescription()); + } + + public void testPutCustomAttributes() { + object.getAttributeMap().put("foo", "bar"); + assertEquals("bar", object.getAttributeMap().get("foo")); + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/AnnotedActionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/AnnotedActionTests.java new file mode 100644 index 00000000..24a29431 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/AnnotedActionTests.java @@ -0,0 +1,60 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import junit.framework.TestCase; + +import org.springframework.webflow.action.AbstractAction; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.TestAction; +import org.springframework.webflow.test.MockRequestContext; + +public class AnnotedActionTests extends TestCase { + + private AnnotatedAction action = new AnnotatedAction(new TestAction()); + + private MockRequestContext context = new MockRequestContext(); + + protected void setUp() throws Exception { + } + + public void testBasicExecute() throws Exception { + assertEquals("success", action.execute(context).getId()); + } + + public void testExecuteWithCustomAttribute() throws Exception { + action.getAttributeMap().put("attr", "value"); + action.setTargetAction(new AbstractAction() { + protected Event doExecute(RequestContext context) throws Exception { + assertEquals("value", context.getAttributes().getString("attr")); + return success(); + } + }); + assertEquals("success", action.execute(context).getId()); + } + + public void testExecuteWithName() throws Exception { + action.getAttributeMap().put("name", "foo"); + action.setTargetAction(new AbstractAction() { + protected Event doExecute(RequestContext context) throws Exception { + assertEquals("foo", context.getAttributes().getString("name")); + return success(); + } + }); + assertEquals("foo.success", action.execute(context).getId()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/DecisionStateTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/DecisionStateTests.java new file mode 100644 index 00000000..0bedee65 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/DecisionStateTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import junit.framework.TestCase; + +import org.springframework.webflow.engine.support.DefaultTargetStateResolver; +import org.springframework.webflow.engine.support.EventIdTransitionCriteria; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.test.MockRequestControlContext; + +/** + * Tests that each of the Flow state types execute as expected when entered. + * + * @author Keith Donald + */ +public class DecisionStateTests extends TestCase { + + public void testIfDecision() { + Flow flow = new Flow("flow"); + DecisionState state = new DecisionState(flow, "decisionState"); + state.getTransitionSet().add(new Transition(new EventIdTransitionCriteria("foo"), to("target"))); + new EndState(flow, "target"); + MockRequestControlContext context = new MockRequestControlContext(flow); + context.setLastEvent(new Event(this, "foo")); + state.enter(context); + assertFalse(context.getFlowExecutionContext().isActive()); + } + + public void testElseDecision() { + Flow flow = new Flow("flow"); + DecisionState state = new DecisionState(flow, "decisionState"); + state.getTransitionSet().add(new Transition(new EventIdTransitionCriteria("foo"), to("invalid"))); + state.getTransitionSet().add(new Transition(to("target"))); + new EndState(flow, "target"); + MockRequestControlContext context = new MockRequestControlContext(flow); + context.setLastEvent(new Event(this, "bogus")); + state.enter(context); + assertFalse(context.getFlowExecutionContext().isActive()); + } + + public void testNoMatching() { + Flow flow = new Flow("flow"); + DecisionState state = new DecisionState(flow, "decisionState"); + state.getTransitionSet().add(new Transition(new EventIdTransitionCriteria("foo"), to("invalid"))); + state.getTransitionSet().add(new Transition(new EventIdTransitionCriteria("bar"), to("invalid"))); + MockRequestControlContext context = new MockRequestControlContext(flow); + context.setLastEvent(new Event(this, "bogus")); + try { + state.enter(context); + fail("Expected no matching"); + } + catch (NoMatchingTransitionException e) { + + } + } + + protected TargetStateResolver to(String stateId) { + return new DefaultTargetStateResolver(stateId); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/EndStateTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/EndStateTests.java new file mode 100644 index 00000000..912c19b6 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/EndStateTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.support.StaticExpression; +import org.springframework.binding.mapping.DefaultAttributeMapper; +import org.springframework.binding.mapping.MappingBuilder; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.engine.impl.FlowExecutionImpl; +import org.springframework.webflow.engine.support.ApplicationViewSelector; +import org.springframework.webflow.engine.support.EventIdTransitionCriteria; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.FlowExecutionListener; +import org.springframework.webflow.execution.FlowExecutionListenerAdapter; +import org.springframework.webflow.execution.FlowSession; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.test.MockExternalContext; + +/** + * Tests that each of the Flow state types execute as expected when entered. + * + * @author Keith Donald + */ +public class EndStateTests extends TestCase { + + public void testEndStateTerminateFlow() { + Flow flow = new Flow("myFlow"); + EndState state = new EndState(flow, "finish"); + state.setViewSelector(view("myViewName")); + FlowExecution flowExecution = new FlowExecutionImpl(flow); + ApplicationView view = (ApplicationView)flowExecution.start(null, new MockExternalContext()); + assertFalse(flowExecution.isActive()); + assertEquals("myViewName", view.getViewName()); + } + + public void testEndStateTerminateFlowWithOutput() { + Flow flow = new Flow("myFlow"); + DefaultAttributeMapper inputMapper = new DefaultAttributeMapper(); + MappingBuilder mapping = new MappingBuilder(DefaultExpressionParserFactory.getExpressionParser()); + inputMapper.addMapping(mapping.source("attr1").target("flowScope.attr1").value()); + flow.setInputMapper(inputMapper); + + EndState state = new EndState(flow, "finish"); + DefaultAttributeMapper outputMapper = new DefaultAttributeMapper(); + outputMapper.addMapping(mapping.source("flowScope.attr1").target("attr1").value()); + outputMapper.addMapping(mapping.source("flowScope.attr2").target("attr2").value()); + state.setOutputMapper(outputMapper); + + FlowExecutionListener outputVerifier = new FlowExecutionListenerAdapter() { + public void sessionEnded(RequestContext context, FlowSession session, AttributeMap output) { + assertEquals("value1", output.get("attr1")); + assertNull(output.get("attr2")); + } + }; + FlowExecution flowExecution = new FlowExecutionImpl(flow, new FlowExecutionListener[] { outputVerifier }, null); + LocalAttributeMap input = new LocalAttributeMap(); + input.put("attr1", "value1"); + ViewSelection view = flowExecution.start(input, new MockExternalContext()); + assertFalse(flowExecution.isActive()); + assertEquals(ViewSelection.NULL_VIEW, view); + } + + protected static TransitionCriteria on(String event) { + return new EventIdTransitionCriteria(event); + } + + protected static String to(String stateId) { + return stateId; + } + + public static ViewSelector view(String viewName) { + return new ApplicationViewSelector(new StaticExpression(viewName)); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/EventTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/EventTests.java new file mode 100644 index 00000000..e7317444 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/EventTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + + +import junit.framework.TestCase; + +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.execution.Event; + +/** + * Tests that each of the Flow state types execute as expected when entered. + * + * @author Keith Donald + */ +public class EventTests extends TestCase { + + public void testNewEvent() { + Event event = new Event(this, "id"); + assertEquals("id", event.getId()); + assertTrue(event.getTimestamp() > 0); + assertTrue(event.getAttributes().isEmpty()); + } + + public void testEventNullSource() { + try { + new Event(null, "id"); + fail("null source"); + } catch (IllegalArgumentException e) { + + } + } + + public void testEventNullId() { + try { + new Event(this, null); + fail("null id"); + } catch (IllegalArgumentException e) { + + } + } + + public void testNewEventWithAttributes() { + LocalAttributeMap attrs = new LocalAttributeMap(); + attrs.put("name", "value"); + Event event = new Event(this, "id", attrs); + assertTrue(!event.getAttributes().isEmpty()); + assertEquals(1, event.getAttributes().size()); + } + + public void testNewEventNullAttributes() { + Event event = new Event(this, "id", null); + assertTrue(event.getAttributes().isEmpty()); + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/FlowTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/FlowTests.java new file mode 100644 index 00000000..10d05987 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/FlowTests.java @@ -0,0 +1,346 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import java.util.ArrayList; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.support.StaticExpression; +import org.springframework.binding.mapping.DefaultAttributeMapper; +import org.springframework.binding.mapping.MappingBuilder; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.webflow.TestException; +import org.springframework.webflow.action.TestMultiAction; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.engine.support.ApplicationViewSelector; +import org.springframework.webflow.engine.support.BeanFactoryFlowVariable; +import org.springframework.webflow.engine.support.DefaultTargetStateResolver; +import org.springframework.webflow.engine.support.EventIdTransitionCriteria; +import org.springframework.webflow.engine.support.SimpleFlowVariable; +import org.springframework.webflow.engine.support.TransitionExecutingStateExceptionHandler; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.execution.TestAction; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.test.MockRequestControlContext; + +/** + * Unit test for the Flow class. + * + * @author Keith Donald + */ +public class FlowTests extends TestCase { + + private Flow flow = createSimpleFlow(); + + private Flow createSimpleFlow() { + flow = new Flow("myFlow"); + ViewState state1 = new ViewState(flow, "myState1"); + state1.setViewSelector(new ApplicationViewSelector(new StaticExpression("myView"))); + state1.getTransitionSet().add(new Transition(on("submit"), to("myState2"))); + EndState state2 = new EndState(flow, "myState2"); + state2.setViewSelector(new ApplicationViewSelector(new StaticExpression("myView2"))); + flow.getGlobalTransitionSet().add(new Transition(on("globalEvent"), to("myState2"))); + return flow; + } + + public void testAddStates() { + Flow flow = new Flow("myFlow"); + new EndState(flow, "myState1"); + new EndState(flow, "myState2"); + assertEquals("Wrong start state:", "myState1", flow.getStartState().getId()); + assertEquals("State count wrong:", 2, flow.getStateCount()); + assertTrue(flow.containsState("myState1")); + assertTrue(flow.containsState("myState2")); + State state = flow.getStateInstance("myState1"); + assertEquals("Wrong flow:", "myFlow", state.getFlow().getId()); + assertEquals("Wrong state:", "myState1", flow.getState("myState1").getId()); + assertEquals("Wrong state:", "myState2", flow.getState("myState2").getId()); + } + + public void testAddDuplicateState() { + Flow flow = new Flow("myFlow"); + new EndState(flow, "myState1"); + try { + new EndState(flow, "myState1"); + fail("Duplicate state added"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + public void testAddSameStateTwice() { + Flow flow = new Flow("myFlow"); + EndState state = new EndState(flow, "myState1"); + try { + flow.add(state); + fail("Should have failed"); + } + catch (IllegalArgumentException e) { + + } + assertEquals("State count wrong:", 1, flow.getStateCount()); + } + + public void testAddStateAlreadyInOtherFlow() { + Flow otherFlow = new Flow("myOtherFlow"); + State state = new EndState(otherFlow, "myState1"); + Flow flow = new Flow("myFlow"); + try { + flow.add(state); + fail("Added state part of another flow"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + public void testGetStateNoStartState() { + Flow flow = new Flow("myFlow"); + try { + flow.getStartState(); + fail("Retrieved start state when no such state"); + } + catch (IllegalStateException e) { + // expected + } + } + + public void testGetStateNoSuchState() { + try { + flow.getState("myState3"); + fail("Returned a state that doesn't exist"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + public void testGetTransitionableState() { + assertEquals("Wrong state:", "myState1", flow.getTransitionableState("myState1").getId()); + assertEquals("Wrong state:", "myState1", flow.getState("myState1").getId()); + } + + public void testGetStateNoSuchTransitionableState() { + try { + flow.getTransitionableState("myState2"); + fail("End states aren't transtionable"); + } + catch (ClassCastException e) { + // expected + } + try { + flow.getTransitionableState("doesNotExist"); + } + catch (IllegalArgumentException e) { + // expected + } + } + + public void testAddActions() { + flow.getStartActionList().add(new TestMultiAction()); + flow.getStartActionList().add(new TestMultiAction()); + flow.getEndActionList().add(new TestMultiAction()); + assertEquals(2, flow.getStartActionList().size()); + assertEquals(1, flow.getEndActionList().size()); + } + + public void testAddInlineFlow() { + Flow inline = new Flow("inline"); + flow.addInlineFlow(inline); + assertSame(inline, flow.getInlineFlow("inline")); + assertEquals(1, flow.getInlineFlowCount()); + String[] inlined = flow.getInlineFlowIds(); + assertEquals(1, inlined.length); + assertSame(flow.getInlineFlows()[0], inline); + } + + public void testAddGlobalTransition() { + Transition t = new Transition(to("myState2")); + flow.getGlobalTransitionSet().add(t); + assertSame(t, flow.getGlobalTransitionSet().toArray()[1]); + } + + public void testStart() { + MockRequestControlContext context = new MockRequestControlContext(flow); + flow.start(context, new LocalAttributeMap()); + assertEquals("Wrong start state", "myState1", context.getCurrentState().getId()); + } + + public void testStartWithAction() { + MockRequestControlContext context = new MockRequestControlContext(flow); + TestAction action = new TestAction(); + flow.getStartActionList().add(action); + flow.start(context, new LocalAttributeMap()); + assertEquals("Wrong start state", "myState1", context.getCurrentState().getId()); + assertEquals(1, action.getExecutionCount()); + } + + public void testStartWithVariables() { + MockRequestControlContext context = new MockRequestControlContext(flow); + flow.addVariable(new SimpleFlowVariable("var1", ArrayList.class, ScopeType.FLOW)); + StaticApplicationContext beanFactory = new StaticApplicationContext(); + beanFactory.registerPrototype("bean", ArrayList.class); + flow.addVariable(new BeanFactoryFlowVariable("var2", "bean", beanFactory, ScopeType.FLOW)); + flow.start(context, new LocalAttributeMap()); + assertEquals(2, context.getFlowScope().size()); + context.getFlowScope().getRequired("var1", ArrayList.class); + context.getFlowScope().getRequired("var2", ArrayList.class); + } + + public void testStartWithMapper() { + DefaultAttributeMapper attributeMapper = new DefaultAttributeMapper(); + MappingBuilder mapping = new MappingBuilder(DefaultExpressionParserFactory.getExpressionParser()); + attributeMapper.addMapping(mapping.source("attr").target("flowScope.attr").value()); + flow.setInputMapper(attributeMapper); + MockRequestControlContext context = new MockRequestControlContext(flow); + LocalAttributeMap sessionInput = new LocalAttributeMap(); + sessionInput.put("attr", "foo"); + flow.start(context, sessionInput); + assertEquals("foo", context.getFlowScope().get("attr")); + } + + public void testStartWithMapperButNoInput() { + DefaultAttributeMapper attributeMapper = new DefaultAttributeMapper(); + MappingBuilder mapping = new MappingBuilder(DefaultExpressionParserFactory.getExpressionParser()); + attributeMapper.addMapping(mapping.source("attr").target("flowScope.attr").value()); + flow.setInputMapper(attributeMapper); + MockRequestControlContext context = new MockRequestControlContext(flow); + LocalAttributeMap sessionInput = new LocalAttributeMap(); + flow.start(context, sessionInput); + assertFalse(context.getFlowScope().contains("attr")); + } + + public void testOnEventNullCurrentState() { + MockRequestControlContext context = new MockRequestControlContext(flow); + Event event = new Event(this, "foo"); + try { + context.setLastEvent(event); + flow.onEvent(context); + } + catch (IllegalStateException e) { + + } + } + + public void testOnEventInvalidCurrentState() { + MockRequestControlContext context = new MockRequestControlContext(flow); + context.setCurrentState(flow.getStateInstance("myState2")); + Event event = new Event(this, "submit"); + context.setLastEvent(event); + try { + context.setLastEvent(event); + flow.onEvent(context); + } + catch (IllegalStateException e) { + + } + } + + public void testOnEvent() { + MockRequestControlContext context = new MockRequestControlContext(flow); + context.setCurrentState(flow.getStateInstance("myState1")); + Event event = new Event(this, "submit"); + context.setLastEvent(event); + assertTrue(context.getFlowExecutionContext().isActive()); + context.setLastEvent(event); + flow.onEvent(context); + assertTrue(!context.getFlowExecutionContext().isActive()); + } + + public void testOnEventGlobalTransition() { + MockRequestControlContext context = new MockRequestControlContext(flow); + context.setCurrentState(flow.getStateInstance("myState1")); + Event event = new Event(this, "globalEvent"); + context.setLastEvent(event); + assertTrue(context.getFlowExecutionContext().isActive()); + context.setLastEvent(event); + flow.onEvent(context); + assertTrue(!context.getFlowExecutionContext().isActive()); + } + + public void testOnEventNoTransition() { + MockRequestControlContext context = new MockRequestControlContext(flow); + context.setCurrentState(flow.getStateInstance("myState1")); + Event event = new Event(this, "bogus"); + context.setLastEvent(event); + try { + context.setLastEvent(event); + flow.onEvent(context); + } + catch (NoMatchingTransitionException e) { + + } + } + + public void testEnd() { + TestAction action = new TestAction(); + flow.getEndActionList().add(action); + MockRequestControlContext context = new MockRequestControlContext(flow); + LocalAttributeMap sessionOutput = new LocalAttributeMap(); + flow.end(context, sessionOutput); + assertEquals(1, action.getExecutionCount()); + } + + public void testEndWithOutputMapper() { + DefaultAttributeMapper attributeMapper = new DefaultAttributeMapper(); + MappingBuilder mapping = new MappingBuilder(DefaultExpressionParserFactory.getExpressionParser()); + attributeMapper.addMapping(mapping.source("flowScope.attr").target("attr").value()); + flow.setOutputMapper(attributeMapper); + MockRequestControlContext context = new MockRequestControlContext(flow); + context.getFlowScope().put("attr", "foo"); + LocalAttributeMap sessionOutput = new LocalAttributeMap(); + flow.end(context, sessionOutput); + assertEquals("foo", sessionOutput.get("attr")); + } + + public void testHandleStateException() { + flow.getExceptionHandlerSet().add( + new TransitionExecutingStateExceptionHandler().add(TestException.class, "myState2")); + MockRequestControlContext context = new MockRequestControlContext(flow); + context.setCurrentState(flow.getStateInstance("myState1")); + FlowExecutionException e = new FlowExecutionException(flow.getId(), flow.getStartState().getId(), "Oops!", + new TestException()); + ApplicationView selectedView = (ApplicationView)flow.handleException(e, context); + assertFalse(context.getFlowExecutionContext().isActive()); + assertNotNull("Should not have been null", selectedView); + assertEquals("Wrong selected view", "myView2", selectedView.getViewName()); + } + + public void testHandleStateExceptionNoMatch() { + MockRequestControlContext context = new MockRequestControlContext(flow); + FlowExecutionException e = new FlowExecutionException(flow.getId(), flow.getStartState().getId(), "Oops!", + new TestException()); + try { + flow.handleException(e, context); + } + catch (FlowExecutionException ex) { + // expected + } + } + + public TransitionCriteria on(String eventId) { + return new EventIdTransitionCriteria(eventId); + } + + protected TargetStateResolver to(String stateId) { + return new DefaultTargetStateResolver(stateId); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/SimpleFlow.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/SimpleFlow.java new file mode 100644 index 00000000..4f3710ad --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/SimpleFlow.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.binding.expression.support.StaticExpression; +import org.springframework.webflow.engine.support.ApplicationViewSelector; +import org.springframework.webflow.engine.support.DefaultTargetStateResolver; +import org.springframework.webflow.engine.support.ExternalRedirectSelector; + +public class SimpleFlow extends Flow { + public SimpleFlow() { + super("simpleFlow"); + + ViewState state1 = new ViewState(this, "view"); + state1.setViewSelector(new ApplicationViewSelector(new StaticExpression("view"))); + state1.getTransitionSet().add(new Transition(to("end"))); + + EndState state2 = new EndState(this, "end"); + state2.setViewSelector(new ExternalRedirectSelector(new StaticExpression("confirm"))); + } + + protected TargetStateResolver to(String stateId) { + return new DefaultTargetStateResolver(stateId); + } + +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/StateExceptionHandlerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/StateExceptionHandlerTests.java new file mode 100644 index 00000000..e5c3f676 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/StateExceptionHandlerTests.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import junit.framework.TestCase; + +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.support.ApplicationView; + +/** + * Unit tests for {@link org.springframework.webflow.engine.FlowExecutionExceptionHandler} related code. + * + * @author Erwin Vervaet + */ +public class StateExceptionHandlerTests extends TestCase { + + public void testHandleException() { + FlowExecutionExceptionHandlerSet handlerSet = new FlowExecutionExceptionHandlerSet(); + + handlerSet.add(new TestStateExceptionHandler(NullPointerException.class, new ApplicationView("NOK", null))); + handlerSet.add(new TestStateExceptionHandler(FlowExecutionException.class, new ApplicationView("OK", null))); + handlerSet.add(new TestStateExceptionHandler(FlowExecutionException.class, new ApplicationView("NOK", null))); + + FlowExecutionException testException = new FlowExecutionException("flowId", "stateId", "Test"); + assertNotNull( + "First handler should have been ignored since it does not handle StateException", + handlerSet.handleException(testException, null)); + assertEquals( + "Third handler should not have been reached since second handler handles excpetion and returns not-null", + "OK", ((ApplicationView)handlerSet.handleException(testException, null)).getViewName()); + } + + public void testHandleExceptionWithNulls() { + FlowExecutionExceptionHandlerSet handlerSet = new FlowExecutionExceptionHandlerSet(); + + handlerSet.add(new TestStateExceptionHandler(FlowExecutionException.class, null)); + handlerSet.add(new TestStateExceptionHandler(FlowExecutionException.class, new ApplicationView("OK", null))); + handlerSet.add(new TestStateExceptionHandler(FlowExecutionException.class, new ApplicationView("NOK", null))); + + FlowExecutionException testException = new FlowExecutionException("flowId", "stateId", "Test"); + assertNotNull( + "First handler should have been ignored since it return null", + handlerSet.handleException(testException, null)); + assertEquals( + "Third handler should not have been reached since second handler handles excpetion and returns not-null", + "OK", ((ApplicationView)handlerSet.handleException(testException, null)).getViewName()); + } + + public void testHandleExceptionNoMatch() { + FlowExecutionExceptionHandlerSet handlerSet = new FlowExecutionExceptionHandlerSet(); + + handlerSet.add(new TestStateExceptionHandler(FlowExecutionException.class, null)); + handlerSet.add(new TestStateExceptionHandler(NullPointerException.class, new ApplicationView("NOK", null))); + + FlowExecutionException testException = new FlowExecutionException("flowId", "stateId", "Test"); + assertNull( + "First handler should have been ignored since it return null, " + + "second handler should have been ignored since it does not handle the exception", + handlerSet.handleException(testException, null)); + } + + /** + * State exception handler used in tests. + */ + public static class TestStateExceptionHandler implements FlowExecutionExceptionHandler { + + private Class typeToHandle; + private ViewSelection handleResult; + + public TestStateExceptionHandler(Class typeToHandle, ViewSelection handleResult) { + this.typeToHandle = typeToHandle; + this.handleResult = handleResult; + } + + public boolean handles(FlowExecutionException exception) { + return typeToHandle.isInstance(exception); + } + + public ViewSelection handle(FlowExecutionException exception, RequestControlContext context) { + return handleResult; + } + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/StateTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/StateTests.java new file mode 100644 index 00000000..dd3deab6 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/StateTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import junit.framework.TestCase; + +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.TestAction; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.test.MockRequestControlContext; + +/** + * Tests that each of the Flow state types execute as expected when entered. + * + * @author Keith Donald + */ +public class StateTests extends TestCase { + + private Flow flow; + + private State state; + + private boolean entered; + + public void setUp() { + flow = new Flow("flow"); + state = new State(flow, "myState") { + protected ViewSelection doEnter(RequestControlContext context) throws FlowExecutionException { + entered = true; + return ViewSelection.NULL_VIEW; + } + }; + } + + public void testStateEnter() { + assertEquals("myState", state.getId()); + MockRequestControlContext context = new MockRequestControlContext(flow); + state.enter(context); + assertEquals(state, context.getCurrentState()); + assertTrue(entered); + } + + public void testStateEnterWithEntryAction() { + TestAction action = new TestAction(); + state.getEntryActionList().add(action); + MockRequestControlContext context = new MockRequestControlContext(flow); + state.enter(context); + assertEquals(state, context.getCurrentState()); + assertTrue(action.isExecuted()); + assertTrue(entered); + assertEquals(1, action.getExecutionCount()); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/SubflowStateTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/SubflowStateTests.java new file mode 100644 index 00000000..5be59f39 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/SubflowStateTests.java @@ -0,0 +1,121 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.support.StaticExpression; +import org.springframework.binding.mapping.DefaultAttributeMapper; +import org.springframework.binding.mapping.MappingBuilder; +import org.springframework.webflow.action.AttributeMapperAction; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.engine.impl.FlowExecutionImpl; +import org.springframework.webflow.engine.support.ApplicationViewSelector; +import org.springframework.webflow.engine.support.DefaultTargetStateResolver; +import org.springframework.webflow.engine.support.EventIdTransitionCriteria; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.test.MockExternalContext; +import org.springframework.webflow.test.MockParameterMap; + +/** + * Tests that each of the Flow state types execute as expected when entered. + * + * @author Keith Donald + */ +public class SubflowStateTests extends TestCase { + + public void testSubFlowState() { + Flow subFlow = new Flow("mySubFlow"); + ViewState state1 = new ViewState(subFlow, "subFlowViewState"); + state1.setViewSelector(view("mySubFlowViewName")); + state1.getTransitionSet().add(new Transition(on("submit"), to("finish"))); + new EndState(subFlow, "finish"); + + Flow flow = new Flow("myFlow"); + SubflowState state2 = new SubflowState(flow, "subFlowState", subFlow); + state2.getTransitionSet().add(new Transition(on("finish"), to("finish"))); + + EndState state3 = new EndState(flow, "finish"); + state3.setViewSelector(view("myParentFlowEndingViewName")); + + FlowExecution flowExecution = new FlowExecutionImpl(flow); + ApplicationView view = (ApplicationView)flowExecution.start(null, new MockExternalContext()); + assertEquals("mySubFlow", flowExecution.getActiveSession().getDefinition().getId()); + assertEquals("subFlowViewState", flowExecution.getActiveSession().getState().getId()); + assertEquals("mySubFlowViewName", view.getViewName()); + view = (ApplicationView)flowExecution.signalEvent("submit", new MockExternalContext()); + assertEquals("myParentFlowEndingViewName", view.getViewName()); + assertTrue(!flowExecution.isActive()); + } + + public void testSubFlowStateModelMapping() { + Flow subFlow = new Flow("mySubFlow"); + MappingBuilder mapping = new MappingBuilder(DefaultExpressionParserFactory.getExpressionParser()); + DefaultAttributeMapper inputMapper = new DefaultAttributeMapper(); + inputMapper.addMapping(mapping.source("childInputAttribute").target("flowScope.childInputAttribute").value()); + subFlow.setInputMapper(inputMapper); + ViewState state1 = new ViewState(subFlow, "subFlowViewState"); + state1.setViewSelector(view("mySubFlowViewName")); + state1.getTransitionSet().add(new Transition(on("submit"), to("finish"))); + EndState state2 = new EndState(subFlow, "finish"); + DefaultAttributeMapper outputMapper = new DefaultAttributeMapper(); + outputMapper.addMapping(mapping.source("flowScope.childInputAttribute").target("childInputAttribute").value()); + state2.setOutputMapper(outputMapper); + + Flow flow = new Flow("myFlow"); + ActionState mapperState = new ActionState(flow, "mapperState"); + DefaultAttributeMapper mapper = new DefaultAttributeMapper(); + mapper.addMapping(mapping.source("externalContext.requestParameterMap.parentInputAttribute").target( + "flowScope.parentInputAttribute").value()); + Action mapperAction = new AttributeMapperAction(mapper); + mapperState.getActionList().add(mapperAction); + mapperState.getTransitionSet().add(new Transition(on("success"), to("subFlowState"))); + + SubflowState subflowState = new SubflowState(flow, "subFlowState", subFlow); + subflowState.setAttributeMapper(new TestAttributeMapper()); + subflowState.getTransitionSet().add(new Transition(on("finish"), to("finish"))); + + EndState endState = new EndState(flow, "finish"); + endState.setViewSelector(view("myParentFlowEndingViewName")); + + FlowExecution flowExecution = new FlowExecutionImpl(flow); + MockParameterMap input = new MockParameterMap(); + input.put("parentInputAttribute", "attributeValue"); + ApplicationView view = (ApplicationView)flowExecution.start(null, new MockExternalContext(input)); + assertEquals("mySubFlow", flowExecution.getActiveSession().getDefinition().getId()); + assertEquals("subFlowViewState", flowExecution.getActiveSession().getState().getId()); + assertEquals("mySubFlowViewName", view.getViewName()); + assertEquals("attributeValue", flowExecution.getActiveSession().getScope().get("childInputAttribute")); + view = (ApplicationView)flowExecution.signalEvent("submit", new MockExternalContext()); + assertEquals("myParentFlowEndingViewName", view.getViewName()); + assertTrue(!flowExecution.isActive()); + assertEquals("attributeValue", view.getModel().get("parentOutputAttribute")); + } + + protected TransitionCriteria on(String event) { + return new EventIdTransitionCriteria(event); + } + + protected TargetStateResolver to(String stateId) { + return new DefaultTargetStateResolver(stateId); + } + + protected ViewSelector view(String viewName) { + return new ApplicationViewSelector(new StaticExpression(viewName)); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/TestAttributeMapper.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/TestAttributeMapper.java new file mode 100644 index 00000000..35fc52fa --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/TestAttributeMapper.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.execution.RequestContext; + +class TestAttributeMapper implements FlowAttributeMapper { + public MutableAttributeMap createFlowInput(RequestContext context) { + LocalAttributeMap inputMap = new LocalAttributeMap(); + inputMap.put("childInputAttribute", context.getFlowScope().get("parentInputAttribute")); + return inputMap; + } + + public void mapFlowOutput(AttributeMap subflowOutput, RequestContext context) { + MutableAttributeMap parentAttributes = context.getFlowExecutionContext().getActiveSession().getScope(); + parentAttributes.put("parentOutputAttribute", subflowOutput.get("childInputAttribute")); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/TransitionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/TransitionTests.java new file mode 100644 index 00000000..77dcd613 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/TransitionTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import junit.framework.TestCase; + +import org.springframework.webflow.engine.support.DefaultTargetStateResolver; +import org.springframework.webflow.engine.support.EventIdTransitionCriteria; +import org.springframework.webflow.execution.TestAction; +import org.springframework.webflow.test.MockRequestControlContext; + +public class TransitionTests extends TestCase { + + public void testSimpleTransition() { + Transition t = new Transition(to("target")); + Flow flow = new Flow("flow"); + ViewState source = new ViewState(flow, "source"); + TestAction action = new TestAction(); + source.getExitActionList().add(action); + ViewState target = new ViewState(flow, "target"); + MockRequestControlContext context = new MockRequestControlContext(flow); + context.setCurrentState(source); + t.execute(source, context); + assertTrue(t.matches(context)); + assertEquals(t, context.getLastTransition()); + assertEquals(context.getCurrentState(), target); + assertEquals(1, action.getExecutionCount()); + } + + public void testTransitionCriteriaDoesNotMatch() { + Transition t = new Transition(new EventIdTransitionCriteria("bogus"), to("target")); + MockRequestControlContext context = new MockRequestControlContext(new Flow("flow")); + assertFalse(t.matches(context)); + } + + public void testTransitionCannotExecute() { + Transition t = new Transition(to("target")); + t.setExecutionCriteria(new EventIdTransitionCriteria("bogus")); + Flow flow = new Flow("flow"); + ViewState source = new ViewState(flow, "source"); + TestAction action = new TestAction(); + source.getExitActionList().add(action); + new ViewState(flow, "target"); + MockRequestControlContext context = new MockRequestControlContext(flow); + context.setCurrentState(source); + t.execute(source, context); + assertTrue(t.matches(context)); + assertEquals(null, context.getLastTransition()); + assertEquals(context.getCurrentState(), source); + assertEquals(0, action.getExecutionCount()); + } + + protected TargetStateResolver to(String stateId) { + return new DefaultTargetStateResolver(stateId); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/ViewStateTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/ViewStateTests.java new file mode 100644 index 00000000..9e54a66b --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/ViewStateTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.support.StaticExpression; +import org.springframework.webflow.engine.impl.FlowExecutionImpl; +import org.springframework.webflow.engine.support.ApplicationViewSelector; +import org.springframework.webflow.engine.support.DefaultTargetStateResolver; +import org.springframework.webflow.engine.support.EventIdTransitionCriteria; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.TestAction; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.test.MockExternalContext; + +/** + * Tests that each of the Flow state types execute as expected when entered. + * + * @author Keith Donald + */ +public class ViewStateTests extends TestCase { + + public void testViewState() { + Flow flow = new Flow("myFlow"); + ViewState state = new ViewState(flow, "viewState"); + state.setViewSelector(view("myViewName")); + state.getTransitionSet().add(new Transition(on("submit"), to("finish"))); + new EndState(flow, "finish"); + FlowExecution flowExecution = new FlowExecutionImpl(flow); + ApplicationView view = (ApplicationView)flowExecution.start(null, new MockExternalContext()); + assertEquals("viewState", flowExecution.getActiveSession().getState().getId()); + assertNotNull(view); + assertEquals("myViewName", view.getViewName()); + } + + public void testViewStateMarker() { + Flow flow = new Flow("myFlow"); + ViewState state = new ViewState(flow, "viewState"); + state.getTransitionSet().add(new Transition(on("submit"), to("finish"))); + new EndState(flow, "finish"); + FlowExecution flowExecution = new FlowExecutionImpl(flow); + ViewSelection view = flowExecution.start(null, new MockExternalContext()); + assertEquals("viewState", flowExecution.getActiveSession().getState().getId()); + assertEquals(ViewSelection.NULL_VIEW, view); + } + + public void testViewStateNotRenderableSelection() { + Flow flow = new Flow("myFlow"); + ViewState state = new ViewState(flow, "viewState"); + state.setViewSelector(new ApplicationViewSelector(new StaticExpression("myView"), true)); + TestAction action = new TestAction(); + state.getRenderActionList().add(action); + state.getTransitionSet().add(new Transition(on("submit"), to("finish"))); + new EndState(flow, "finish"); + FlowExecution flowExecution = new FlowExecutionImpl(flow); + assertFalse(action.isExecuted()); + + flowExecution.start(null, new MockExternalContext()); + assertEquals("viewState", flowExecution.getActiveSession().getState().getId()); + assertFalse(action.isExecuted()); + assertEquals(action.getExecutionCount(), 0); + + flowExecution.refresh(new MockExternalContext()); + assertEquals(action.getExecutionCount(), 1); + } + + public void testViewStateRenderableSelection() { + Flow flow = new Flow("myFlow"); + ViewState state = new ViewState(flow, "viewState"); + state.setViewSelector(new ApplicationViewSelector(new StaticExpression("test"))); + TestAction action = new TestAction(); + state.getRenderActionList().add(action); + state.getTransitionSet().add(new Transition(on("submit"), to("finish"))); + new EndState(flow, "finish"); + FlowExecution flowExecution = new FlowExecutionImpl(flow); + assertFalse(action.isExecuted()); + + flowExecution.start(null, new MockExternalContext()); + assertEquals("viewState", flowExecution.getActiveSession().getState().getId()); + assertTrue(action.isExecuted()); + assertEquals(action.getExecutionCount(), 1); + + flowExecution.refresh(new MockExternalContext()); + assertEquals(action.getExecutionCount(), 2); + + } + + protected TransitionCriteria on(String event) { + return new EventIdTransitionCriteria(event); + } + + protected TargetStateResolver to(String stateId) { + return new DefaultTargetStateResolver(stateId); + } + + public static ViewSelector view(String viewName) { + return new ApplicationViewSelector(new StaticExpression(viewName)); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/AbstractFlowBuilderTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/AbstractFlowBuilderTests.java new file mode 100644 index 00000000..6747c83b --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/AbstractFlowBuilderTests.java @@ -0,0 +1,238 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import junit.framework.TestCase; + +import org.springframework.webflow.action.MultiAction; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.engine.ActionState; +import org.springframework.webflow.engine.AnnotatedAction; +import org.springframework.webflow.engine.EndState; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.FlowAttributeMapper; +import org.springframework.webflow.engine.SubflowState; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.engine.ViewState; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.engine.support.ApplicationViewSelector; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.test.MockExternalContext; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Test Java based flow builder logic (subclasses of AbstractFlowBuilder). + * + * @see org.springframework.webflow.engine.builder.AbstractFlowBuilder + * + * @author Keith Donald + * @author Rod Johnson + * @author Colin Sampaleanu + */ +public class AbstractFlowBuilderTests extends TestCase { + + private String PERSONS_LIST = "person.List"; + + private static String PERSON_DETAILS = "person.Detail"; + + private AbstractFlowBuilder builder = createBuilder(); + + protected AbstractFlowBuilder createBuilder() { + return new AbstractFlowBuilder() { + public void buildStates() { + addEndState("finish"); + } + }; + } + + public void testDependencyLookup() { + TestMasterFlowBuilderLookupById master = new TestMasterFlowBuilderLookupById(); + master.setFlowServiceLocator(new BaseFlowServiceLocator() { + public Flow getSubflow(String id) throws FlowArtifactLookupException { + if (id.equals(PERSON_DETAILS)) { + BaseFlowBuilder builder = new TestDetailFlowBuilderLookupById(); + builder.setFlowServiceLocator(this); + FlowAssembler assembler = new FlowAssembler(PERSON_DETAILS, builder); + return assembler.assembleFlow(); + } + else { + throw new FlowArtifactLookupException(id, Flow.class); + } + } + + public Action getAction(String id) throws FlowArtifactLookupException { + return new NoOpAction(); + } + + public FlowAttributeMapper getAttributeMapper(String id) throws FlowArtifactLookupException { + if (id.equals("id.attributeMapper")) { + return new PersonIdMapper(); + } + else { + throw new FlowArtifactLookupException(id, FlowAttributeMapper.class); + } + } + }); + + FlowAssembler assembler = new FlowAssembler(PERSONS_LIST, master); + Flow flow = assembler.assembleFlow(); + + assertEquals("person.List", flow.getId()); + assertTrue(flow.getStateCount() == 4); + assertTrue(flow.containsState("getPersonList")); + assertTrue(flow.getState("getPersonList") instanceof ActionState); + assertTrue(flow.containsState("viewPersonList")); + assertTrue(flow.getState("viewPersonList") instanceof ViewState); + assertTrue(flow.containsState("person.Detail")); + assertTrue(flow.getState("person.Detail") instanceof SubflowState); + assertTrue(flow.containsState("finish")); + assertTrue(flow.getState("finish") instanceof EndState); + } + + public void testNoArtifactFactorySet() { + TestMasterFlowBuilderLookupById master = new TestMasterFlowBuilderLookupById(); + try { + FlowAssembler assembler = new FlowAssembler(PERSONS_LIST, master); + assembler.assembleFlow(); + fail("Should have failed, artifact lookup not supported"); + } + catch (UnsupportedOperationException e) { + // expected + } + } + + public class TestMasterFlowBuilderLookupById extends AbstractFlowBuilder { + public void buildStates() { + addActionState("getPersonList", action("noOpAction"), transition(on(success()), to("viewPersonList"))); + addViewState("viewPersonList", "person.list.view", transition(on(submit()), to("person.Detail"))); + addSubflowState(PERSON_DETAILS, flow("person.Detail"), attributeMapper("id.attributeMapper"), transition( + on("*"), to("getPersonList"))); + addEndState("finish"); + } + } + + public class TestMasterFlowBuilderDependencyInjection extends AbstractFlowBuilder { + private NoOpAction noOpAction; + + private Flow subFlow; + + private PersonIdMapper personIdMapper; + + public void setNoOpAction(NoOpAction noOpAction) { + this.noOpAction = noOpAction; + } + + public void setPersonIdMapper(PersonIdMapper personIdMapper) { + this.personIdMapper = personIdMapper; + } + + public void setSubFlow(Flow subFlow) { + this.subFlow = subFlow; + } + + public void buildStates() { + addActionState("getPersonList", noOpAction, transition(on(success()), to("viewPersonList"))); + addViewState("viewPersonList", "person.list.view", transition(on(submit()), to("person.Detail"))); + addSubflowState(PERSON_DETAILS, subFlow, personIdMapper, transition(on("*"), to("getPersonList"))); + addEndState("finish"); + } + } + + public static class PersonIdMapper implements FlowAttributeMapper { + public MutableAttributeMap createFlowInput(RequestContext context) { + LocalAttributeMap inputMap = new LocalAttributeMap(); + inputMap.put("personId", context.getFlowScope().get("personId")); + return inputMap; + } + + public void mapFlowOutput(AttributeMap subflowOutput, RequestContext context) { + } + } + + public static class TestDetailFlowBuilderLookupById extends AbstractFlowBuilder { + public void buildStates() { + addActionState("getDetails", action("noOpAction"), transition(on(success()), to("viewDetails"))); + addViewState("viewDetails", "person.Detail.view", transition(on(submit()), to("bindAndValidateDetails"))); + addActionState("bindAndValidateDetails", action("noOpAction"), new Transition[] { + transition(on(error()), to("viewDetails")), transition(on(success()), to("finish")) }); + addEndState("finish"); + } + } + + public static class TestDetailFlowBuilderDependencyInjection extends AbstractFlowBuilder { + + private NoOpAction noOpAction; + + public void setNoOpAction(NoOpAction noOpAction) { + this.noOpAction = noOpAction; + } + + public void buildStates() { + addActionState("getDetails", noOpAction, transition(on(success()), to("viewDetails"))); + addViewState("viewDetails", "person.Detail.view", transition(on(submit()), to("bindAndValidateDetails"))); + addActionState("bindAndValidateDetails", noOpAction, new Transition[] { + transition(on(error()), to("viewDetails")), transition(on(success()), to("finish")) }); + addEndState("finish"); + } + }; + + /** + * Action bean stub that does nothing, just returns a "success" result. + */ + public static final class NoOpAction implements Action { + public Event execute(RequestContext context) throws Exception { + return new Event(this, "success"); + } + } + + public void testConfigureMultiAction() throws Exception { + MultiAction multiAction = new MultiAction(new MultiActionTarget()); + AnnotatedAction action = builder.invoke("foo", multiAction); + assertEquals("foo", action.getAttributeMap().get(AnnotatedAction.METHOD_ATTRIBUTE)); + assertEquals("success", action.execute(new MockRequestContext()).getId()); + } + + public static class MultiActionTarget { + public Event foo(RequestContext context) { + return new Event(this, "success"); + } + } + + public void testEndStateRefresh() { + FlowBuilder builder = new AbstractFlowBuilder() { + public void buildStates() throws FlowBuilderException { + addEndState("theEnd", "redirect:endView"); + } + }; + Flow testFlow = new FlowAssembler("testFlow", builder).assembleFlow(); + assertTrue(testFlow.getStartState() instanceof EndState); + assertTrue(((EndState)testFlow.getStartState()).getViewSelector() instanceof ApplicationViewSelector); + assertTrue(((ApplicationViewSelector)((EndState)testFlow.getStartState()).getViewSelector()).isRedirect()); + + FlowExecution execution = new FlowExecutionImplFactory().createFlowExecution(testFlow); + ViewSelection viewSelection = execution.start(null, new MockExternalContext()); + assertTrue("redirect: should be ignored for end states", viewSelection instanceof ApplicationView); + assertEquals("endView", ((ApplicationView)viewSelection).getViewName()); + assertFalse(execution.isActive()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/BaseFlowServiceLocatorTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/BaseFlowServiceLocatorTests.java new file mode 100644 index 00000000..12205661 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/BaseFlowServiceLocatorTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.binding.convert.support.GenericConversionService; +import org.springframework.binding.convert.support.TextToBoolean; +import org.springframework.webflow.engine.NullViewSelector; +import org.springframework.webflow.engine.ViewSelector; + +import junit.framework.TestCase; + +/** + * Test case for the {@link BaseFlowServiceLocator}. + * + * @author Erwin Vervaet + */ +public class BaseFlowServiceLocatorTests extends TestCase { + + public void testWithCustomConversionService() { + BaseFlowServiceLocator serviceLocator = new BaseFlowServiceLocator(); + + GenericConversionService conversionService = new GenericConversionService(); + conversionService.addConverter(new TextToBoolean("ja", "nee")); + conversionService.addConverter(new CustomTextToViewSelector(serviceLocator)); + + serviceLocator.setConversionService(conversionService); + + assertEquals(Boolean.TRUE, serviceLocator.getConversionService().getConversionExecutor( + String.class, Boolean.class).execute("ja")); + assertSame(NullViewSelector.INSTANCE, serviceLocator.getConversionService().getConversionExecutor( + String.class, ViewSelector.class).execute("custom:")); + } + + public static class CustomTextToViewSelector extends TextToViewSelector { + + public CustomTextToViewSelector(FlowServiceLocator flowServiceLocator) { + super(flowServiceLocator); + } + + protected ViewSelector convertEncodedViewSelector(String encodedView) { + return NullViewSelector.INSTANCE; + } + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/MyCustomStateExceptionHandler.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/MyCustomStateExceptionHandler.java new file mode 100644 index 00000000..ecdc696f --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/MyCustomStateExceptionHandler.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import org.springframework.webflow.engine.FlowExecutionExceptionHandler; +import org.springframework.webflow.engine.RequestControlContext; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.ViewSelection; + +public class MyCustomStateExceptionHandler implements FlowExecutionExceptionHandler { + + public boolean handles(FlowExecutionException e) { + return false; + } + + public ViewSelection handle(FlowExecutionException e, RequestControlContext context) { + return null; + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/RefreshableFlowDefinitionHolderTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/RefreshableFlowDefinitionHolderTests.java new file mode 100644 index 00000000..4d11f126 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/RefreshableFlowDefinitionHolderTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; + +import junit.framework.TestCase; + +import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.engine.builder.xml.XmlFlowBuilder; +import org.springframework.webflow.util.ResourceHolder; + +/** + * Unit tests for {@link RefreshableFlowDefinitionHolder}. + */ +public class RefreshableFlowDefinitionHolderTests extends TestCase { + + public void testNoRefreshOnNoChange() { + File parent = new File("src/test/java/org/springframework/webflow/engine/builder/xml"); + Resource location = new FileSystemResource(new File(parent, "flow.xml")); + XmlFlowBuilder flowBuilder = new XmlFlowBuilder(location); + FlowAssembler assembler = new FlowAssembler("flow", flowBuilder); + RefreshableFlowDefinitionHolder holder = new RefreshableFlowDefinitionHolder(assembler); + assertEquals("flow", holder.getFlowDefinitionId()); + assertSame(flowBuilder, holder.getFlowBuilder()); + assertEquals(0, holder.getLastModified()); + assertTrue(!holder.isAssembled()); + FlowDefinition flow1 = holder.getFlowDefinition(); + assertTrue(holder.isAssembled()); + long lastModified = holder.getLastModified(); + assertTrue(lastModified != -1); + assertTrue(lastModified > 0); + FlowDefinition flow2 = holder.getFlowDefinition(); + assertEquals("flow", flow2.getId()); + assertEquals(lastModified, holder.getLastModified()); + assertSame(flow1, flow2); + } + + public void testReloadOnChange() throws Exception { + MockFlowBuilder mockFlowBuilder = new MockFlowBuilder(); + FlowAssembler assembler = new FlowAssembler("mockFlow", mockFlowBuilder); + RefreshableFlowDefinitionHolder holder = new RefreshableFlowDefinitionHolder(assembler); + + mockFlowBuilder.lastModified = 0L; + assertEquals(0, mockFlowBuilder.buildCallCount); + holder.getFlowDefinition(); + assertEquals(1, mockFlowBuilder.buildCallCount); + holder.getFlowDefinition(); + assertEquals(1, mockFlowBuilder.buildCallCount); + holder.getFlowDefinition(); + assertEquals(1, mockFlowBuilder.buildCallCount); + mockFlowBuilder.lastModified = 10L; + holder.getFlowDefinition(); + assertEquals(2, mockFlowBuilder.buildCallCount); + holder.getFlowDefinition(); + assertEquals(2, mockFlowBuilder.buildCallCount); + holder.refresh(); + assertEquals(3, mockFlowBuilder.buildCallCount); + holder.refresh(); + assertEquals(4, mockFlowBuilder.buildCallCount); + } + + private class MockFlowBuilder extends AbstractFlowBuilder implements ResourceHolder { + + public int buildCallCount = 0; + public long lastModified = 0L; + + public void buildStates() throws FlowBuilderException { + addEndState("end"); + buildCallCount++; + } + + public Resource getResource() { + return new AbstractResource() { + + public File getFile() throws IOException { + return new File("mock") { + public long lastModified() { + return lastModified; + } + }; + } + + public String getDescription() { + return null; + } + + public InputStream getInputStream() throws IOException { + return null; + } + }; + } + } + + +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/SimpleFlowBuilder.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/SimpleFlowBuilder.java new file mode 100644 index 00000000..6707816a --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/SimpleFlowBuilder.java @@ -0,0 +1,22 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +public class SimpleFlowBuilder extends AbstractFlowBuilder { + public void buildStates() { + addEndState("end"); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/TextToTransitionCriteriaTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/TextToTransitionCriteriaTests.java new file mode 100644 index 00000000..9484a14d --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/TextToTransitionCriteriaTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.builder; + +import junit.framework.TestCase; + +import org.springframework.binding.convert.ConversionException; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.engine.WildcardTransitionCriteria; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.test.MockFlowServiceLocator; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Test case for {@link TextToTransitionCriteria}. + */ +public class TextToTransitionCriteriaTests extends TestCase { + + private MockFlowServiceLocator serviceLocator = new MockFlowServiceLocator(); + private TextToTransitionCriteria converter = new TextToTransitionCriteria(serviceLocator); + + public void testAny() { + String expression = "*"; + TransitionCriteria criterion = (TransitionCriteria)converter.convert(expression); + RequestContext ctx = getRequestContext(); + assertTrue("Criterion should evaluate to true", criterion.test(ctx)); + + assertSame(WildcardTransitionCriteria.INSTANCE, converter.convert("*")); + assertSame(WildcardTransitionCriteria.INSTANCE, converter.convert("")); + } + + public void testStaticEventId() { + String expression = "sample"; + TransitionCriteria criterion = (TransitionCriteria)converter.convert(expression); + RequestContext ctx = getRequestContext(); + assertTrue("Criterion should evaluate to true", criterion.test(ctx)); + } + + public void testTrueEvaluation() throws Exception { + String expression = "${flowScope.foo == 'bar'}"; + TransitionCriteria criterion = (TransitionCriteria)converter.convert(expression); + RequestContext ctx = getRequestContext(); + assertTrue("Criterion should evaluate to true", criterion.test(ctx)); + } + + public void testFalseEvaluation() throws Exception { + String expression = "${flowScope.foo != 'bar'}"; + TransitionCriteria criterion = (TransitionCriteria)converter.convert(expression); + RequestContext ctx = getRequestContext(); + assertFalse("Criterion should evaluate to false", criterion.test(ctx)); + } + + public void testNonBooleanEvaluation() throws Exception { + String expression = "${flowScope.foo}"; + TransitionCriteria criterion = (TransitionCriteria)converter.convert(expression); + RequestContext ctx = getRequestContext(); + try { + criterion.test(ctx); + fail("Non-boolean evaluations are not allowed"); + } + catch (IllegalArgumentException e) { + // success + } + } + + public void testInvalidSyntax() throws Exception { + try { + String expression = "${&foo< + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow.xml new file mode 100644 index 00000000..9eeae8ce --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/flow.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/messageSourceAwareContext.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/messageSourceAwareContext.xml new file mode 100644 index 00000000..6539d2ed --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/messageSourceAwareContext.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/messageSourceAwareFlow.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/messageSourceAwareFlow.xml new file mode 100644 index 00000000..2e6364b7 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/messageSourceAwareFlow.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/namedActionFlow.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/namedActionFlow.xml new file mode 100644 index 00000000..c012e57d --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/namedActionFlow.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/nsFlow.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/nsFlow.xml new file mode 100644 index 00000000..007120f5 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/nsFlow.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/pojoActionFlow.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/pojoActionFlow.xml new file mode 100644 index 00000000..4604889f --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/pojoActionFlow.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow1.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow1.xml new file mode 100644 index 00000000..1cded018 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow1.xml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + prop1Value + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow1Context.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow1Context.xml new file mode 100644 index 00000000..be189be3 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow1Context.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow2.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow2.xml new file mode 100644 index 00000000..68ca643b --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow2.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow2Context.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow2Context.xml new file mode 100644 index 00000000..fc1c6e22 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow2Context.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow2ParentContext.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow2ParentContext.xml new file mode 100644 index 00000000..4977be35 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow2ParentContext.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow2SubFlow1Context.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow2SubFlow1Context.xml new file mode 100644 index 00000000..9194444e --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow2SubFlow1Context.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow3.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow3.xml new file mode 100644 index 00000000..56b40bdc --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow3.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow3Context.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow3Context.xml new file mode 100644 index 00000000..fbe0cc6e --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/builder/xml/testFlow3Context.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/FlowExecutionImplFactoryTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/FlowExecutionImplFactoryTests.java new file mode 100644 index 00000000..aa53e585 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/FlowExecutionImplFactoryTests.java @@ -0,0 +1,65 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.impl; + +import junit.framework.TestCase; + +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.SimpleFlow; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.FlowExecutionListener; +import org.springframework.webflow.execution.FlowExecutionListenerAdapter; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.factory.StaticFlowExecutionListenerLoader; +import org.springframework.webflow.test.MockExternalContext; + +public class FlowExecutionImplFactoryTests extends TestCase { + private FlowExecutionImplFactory factory = new FlowExecutionImplFactory(); + + private Flow flowDefinition = new SimpleFlow(); + + private boolean starting; + + public void testDefaultFactory() { + FlowExecution execution = factory.createFlowExecution(flowDefinition); + assertFalse(execution.isActive()); + } + + public void testFactoryWithExecutionAttributes() { + MutableAttributeMap attributes = new LocalAttributeMap(); + attributes.put("foo", "bar"); + factory.setExecutionAttributes(attributes); + FlowExecution execution = factory.createFlowExecution(flowDefinition); + assertFalse(execution.isActive()); + assertEquals(attributes, execution.getAttributes()); + } + + public void testFactoryWithListener() { + FlowExecutionListener listener1 = new FlowExecutionListenerAdapter() { + public void sessionStarting(RequestContext context, FlowDefinition definition, MutableAttributeMap input) { + starting = true; + } + }; + factory.setExecutionListenerLoader(new StaticFlowExecutionListenerLoader(listener1)); + FlowExecution execution = factory.createFlowExecution(flowDefinition); + assertFalse(execution.isActive()); + execution.start(null, new MockExternalContext()); + assertTrue(starting); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/FlowExecutionImplStateRestorerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/FlowExecutionImplStateRestorerTests.java new file mode 100644 index 00000000..7374006f --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/FlowExecutionImplStateRestorerTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.impl; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; + +import junit.framework.TestCase; + +import org.springframework.beans.factory.support.StaticListableBeanFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.registry.FlowDefinitionLocator; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistryImpl; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.builder.DefaultFlowServiceLocator; +import org.springframework.webflow.engine.builder.xml.XmlFlowRegistrar; +import org.springframework.webflow.execution.FlowExecutionListener; +import org.springframework.webflow.execution.FlowExecutionListenerAdapter; +import org.springframework.webflow.execution.factory.FlowExecutionListenerLoader; +import org.springframework.webflow.test.MockExternalContext; +import org.springframework.webflow.test.MockParameterMap; + +/** + * Test case for FlowExecutionStack. + * + * @see FlowExecutionImpl + * + * @author Erwin Vervaet + */ +public class FlowExecutionImplStateRestorerTests extends TestCase { + + private FlowExecutionImpl flowExecution; + + private FlowDefinitionLocator flowLocator; + + private FlowExecutionImplStateRestorer stateRestorer; + + protected void setUp() throws Exception { + FlowDefinitionRegistry registry = new FlowDefinitionRegistryImpl(); + XmlFlowRegistrar registrar = new XmlFlowRegistrar(new DefaultFlowServiceLocator(registry, + new StaticListableBeanFactory())); + registrar.addLocation(new ClassPathResource("testFlow.xml", getClass())); + registrar.addLocation(new ClassPathResource("external-subflow.xml", getClass())); + registrar.registerFlowDefinitions(registry); + final Flow flow = (Flow)registry.getFlowDefinition("testFlow"); + flowLocator = registry; + + FlowExecutionListener listener1 = new FlowExecutionListenerAdapter() { + }; + final FlowExecutionListener[] listeners = new FlowExecutionListener[] { listener1 }; + + MutableAttributeMap attributes = new LocalAttributeMap(); + attributes.put("foo", "bar"); + flowExecution = new FlowExecutionImpl(flow, listeners, attributes); + + MutableAttributeMap conversationScope = new LocalAttributeMap(); + conversationScope.put("baz", "bear"); + flowExecution.setConversationScope(conversationScope); + + FlowExecutionListenerLoader listenerLoader = new FlowExecutionListenerLoader() { + public FlowExecutionListener[] getListeners(FlowDefinition flow) { + return listeners; + } + }; + stateRestorer = new FlowExecutionImplStateRestorer(flowLocator); + stateRestorer.setExecutionListenerLoader(listenerLoader); + stateRestorer.setExecutionAttributes(attributes); + } + + public void testRehydrate() throws Exception { + // setup some input data + MockParameterMap input = new MockParameterMap(); + input.put("name", "value"); + // start the flow execution + flowExecution.start(null, new MockExternalContext(input)); + runFlowExecutionRestoreTest(); + } + + public void testRehydrateNotStarted() throws Exception { + // don't start the flow execution + runFlowExecutionRestoreTest(); + } + + protected void runFlowExecutionRestoreTest() throws Exception { + // serialize the flowExecution + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + ObjectOutputStream oout = new ObjectOutputStream(bout); + oout.writeObject(flowExecution); + oout.flush(); + + // deserialize the flowExecution + ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray()); + ObjectInputStream oin = new ObjectInputStream(bin); + FlowExecutionImpl restoredFlowExecution = (FlowExecutionImpl)oin.readObject(); + assertNotNull(restoredFlowExecution); + assertNull(restoredFlowExecution.getDefinition()); + + stateRestorer.restoreState(restoredFlowExecution, flowExecution.getConversationScope()); + assertNotNull(restoredFlowExecution.getDefinition()); + assertEquals(flowExecution.isActive(), restoredFlowExecution.isActive()); + if (flowExecution.isActive()) { + assertEquals(flowExecution.getActiveSession().getScope().asMap(), restoredFlowExecution.getActiveSession() + .getScope().asMap()); + assertEquals(flowExecution.getActiveSession().getState().getId(), restoredFlowExecution.getActiveSession() + .getState().getId()); + assertEquals(flowExecution.getActiveSession().getDefinition().getId(), restoredFlowExecution + .getActiveSession().getDefinition().getId()); + assertSame(flowExecution.getDefinition(), restoredFlowExecution.getDefinition()); + } + assertEquals(flowExecution.getListeners().size(), restoredFlowExecution.getListeners().size()); + assertEquals(flowExecution.getConversationScope(), restoredFlowExecution.getConversationScope()); + assertEquals(flowExecution.getAttributes(), flowExecution.getAttributes()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/FlowExecutionImplTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/FlowExecutionImplTests.java new file mode 100644 index 00000000..8f72c52c --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/FlowExecutionImplTests.java @@ -0,0 +1,271 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.impl; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.support.StaticExpression; +import org.springframework.binding.mapping.DefaultAttributeMapper; +import org.springframework.binding.mapping.MappingBuilder; +import org.springframework.core.io.ClassPathResource; +import org.springframework.webflow.action.AbstractAction; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.engine.ActionState; +import org.springframework.webflow.engine.EndState; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.SubflowState; +import org.springframework.webflow.engine.TargetStateResolver; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.engine.ViewSelector; +import org.springframework.webflow.engine.ViewState; +import org.springframework.webflow.engine.builder.AbstractFlowBuilder; +import org.springframework.webflow.engine.builder.FlowAssembler; +import org.springframework.webflow.engine.builder.FlowBuilderException; +import org.springframework.webflow.engine.builder.xml.TestFlowServiceLocator; +import org.springframework.webflow.engine.builder.xml.XmlFlowBuilder; +import org.springframework.webflow.engine.builder.xml.XmlFlowBuilderTests; +import org.springframework.webflow.engine.support.ApplicationViewSelector; +import org.springframework.webflow.engine.support.DefaultTargetStateResolver; +import org.springframework.webflow.engine.support.EventIdTransitionCriteria; +import org.springframework.webflow.engine.support.TransitionExecutingStateExceptionHandler; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.FlowExecutionListener; +import org.springframework.webflow.execution.MockFlowExecutionListener; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.TestAction; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.test.MockExternalContext; + +/** + * General flow execution tests. + * + * @author Keith Donald + * @author Erwin Vervaet + * @author Ben Hale + */ +public class FlowExecutionImplTests extends TestCase { + + public void testFlowExecutionListener() { + Flow flow = new Flow("myFlow"); + DefaultAttributeMapper inputMapper = new DefaultAttributeMapper(); + MappingBuilder mapping = new MappingBuilder(DefaultExpressionParserFactory.getExpressionParser()); + inputMapper.addMapping(mapping.source("name").target("flowScope.name").value()); + flow.setInputMapper(inputMapper); + ActionState actionState = new ActionState(flow, "actionState"); + actionState.getActionList().add(new TestAction()); + actionState.getTransitionSet().add(new Transition(onEvent("success"), toState("viewState"))); + + ViewState viewState = new ViewState(flow, "viewState"); + viewState.setViewSelector(selectView("myView")); + viewState.getTransitionSet().add(new Transition(onEvent("submit"), toState("subFlowState"))); + + Flow subFlow = new Flow("mySubFlow"); + ViewState state1 = new ViewState(subFlow, "subFlowViewState"); + state1.setViewSelector(selectView("mySubFlowViewName")); + state1.getTransitionSet().add(new Transition(onEvent("submit"), toState("finish"))); + new EndState(subFlow, "finish"); + + SubflowState subflowState = new SubflowState(flow, "subFlowState", subFlow); + subflowState.getTransitionSet().add(new Transition(onEvent("finish"), toState("finish"))); + + EndState endState = new EndState(flow, "finish"); + endState.getEntryActionList().add(new AbstractAction() { + protected Event doExecute(RequestContext context) throws Exception { + throw new IllegalStateException("Whoops!"); + } + }); + TransitionExecutingStateExceptionHandler handler = new TransitionExecutingStateExceptionHandler(); + handler.add(Exception.class, "error"); + endState.getExceptionHandlerSet().add(handler); + + new EndState(flow, "error"); + + MockFlowExecutionListener flowExecutionListener = new MockFlowExecutionListener(); + FlowExecutionImpl flowExecution = new FlowExecutionImpl(flow, + new FlowExecutionListener[] { flowExecutionListener }, null); + LocalAttributeMap input = new LocalAttributeMap(); + input.put("name", "value"); + assertTrue(!flowExecutionListener.isStarted()); + flowExecution.start(input, new MockExternalContext()); + assertTrue(flowExecutionListener.isStarted()); + assertTrue(flowExecutionListener.isPaused()); + assertTrue(!flowExecutionListener.isExecuting()); + assertEquals(1, flowExecutionListener.getEventsSignaledCount()); + assertEquals(0, flowExecutionListener.getFlowNestingLevel()); + assertEquals(2, flowExecutionListener.getTransitionCount()); + assertEquals("value", flowExecution.getActiveSession().getScope().getString("name")); + flowExecution.signalEvent("submit", new MockExternalContext()); + assertTrue(!flowExecutionListener.isExecuting()); + assertEquals(2, flowExecutionListener.getEventsSignaledCount()); + assertEquals(1, flowExecutionListener.getFlowNestingLevel()); + assertEquals(4, flowExecutionListener.getTransitionCount()); + flowExecution.signalEvent("submit", new MockExternalContext()); + assertTrue(!flowExecutionListener.isExecuting()); + assertEquals(0, flowExecutionListener.getFlowNestingLevel()); + assertEquals(4, flowExecutionListener.getEventsSignaledCount()); + assertEquals(7, flowExecutionListener.getTransitionCount()); + assertEquals(1, flowExecutionListener.getExceptionsThrown()); + assertTrue(!flowExecution.isActive()); + } + + public void testLoopInFlow() throws Exception { + AbstractFlowBuilder builder = new AbstractFlowBuilder() { + public void buildStates() throws FlowBuilderException { + addViewState("viewState", "viewName", new Transition[] { transition(on(submit()), to("viewState")), + transition(on(finish()), to("endState")) }); + addEndState("endState"); + } + }; + Flow flow = new FlowAssembler("flow", builder).assembleFlow(); + FlowExecution flowExecution = new FlowExecutionImpl(flow); + ApplicationView view = (ApplicationView)flowExecution.start(null, new MockExternalContext()); + assertNotNull(view); + assertEquals("viewName", view.getViewName()); + for (int i = 0; i < 10; i++) { + view = (ApplicationView)flowExecution.signalEvent("submit", new MockExternalContext()); + assertEquals("viewName", view.getViewName()); + } + assertTrue(flowExecution.isActive()); + flowExecution.signalEvent("finish", new MockExternalContext()); + assertFalse(flowExecution.isActive()); + } + + public void testLoopInFlowWithSubFlow() throws Exception { + AbstractFlowBuilder childBuilder = new AbstractFlowBuilder() { + public void buildStates() throws FlowBuilderException { + addActionState("doOtherStuff", new AbstractAction() { + private int executionCount = 0; + + protected Event doExecute(RequestContext context) throws Exception { + executionCount++; + if (executionCount < 2) { + return success(); + } + return error(); + } + }, + new Transition[] { transition(on(success()), to(finish())), + transition(on(error()), to("stopTest")) }); + addEndState(finish()); + addEndState("stopTest"); + } + }; + final Flow childFlow = new FlowAssembler("flow", childBuilder).assembleFlow(); + AbstractFlowBuilder parentBuilder = new AbstractFlowBuilder() { + public void buildStates() throws FlowBuilderException { + addActionState("doStuff", new AbstractAction() { + protected Event doExecute(RequestContext context) throws Exception { + return success(); + } + }, transition(on(success()), to("startSubFlow"))); + addSubflowState("startSubFlow", childFlow, null, new Transition[] { + transition(on(finish()), to("startSubFlow")), transition(on("stopTest"), to("stopTest")) }); + addEndState("stopTest"); + } + }; + Flow parentFlow = new FlowAssembler("parentFlow", parentBuilder).assembleFlow(); + + FlowExecution flowExecution = new FlowExecutionImpl(parentFlow); + flowExecution.start(null, new MockExternalContext()); + assertFalse(flowExecution.isActive()); + } + + public void testExtensiveFlowNavigationScenario1() { + XmlFlowBuilder builder = new XmlFlowBuilder(new ClassPathResource("testFlow1.xml", XmlFlowBuilderTests.class), + new TestFlowServiceLocator()); + FlowAssembler assembler = new FlowAssembler("testFlow1", builder); + FlowExecution execution = new FlowExecutionImpl(assembler.assembleFlow()); + MockExternalContext context = new MockExternalContext(); + execution.start(null, context); + assertEquals("viewState1", execution.getActiveSession().getState().getId()); + assertNotNull(execution.getActiveSession().getScope().get("items")); + execution.signalEvent("event1", context); + assertTrue(!execution.isActive()); + } + + public void testExtensiveFlowNavigationScenario2() { + XmlFlowBuilder builder = new XmlFlowBuilder(new ClassPathResource("testFlow1.xml", XmlFlowBuilderTests.class), + new TestFlowServiceLocator()); + LocalAttributeMap attributes = new LocalAttributeMap(); + attributes.put("scenario2", Boolean.TRUE); + FlowAssembler assembler = new FlowAssembler("testFlow1", attributes, builder); + FlowExecution execution = new FlowExecutionImpl(assembler.assembleFlow()); + MockExternalContext context = new MockExternalContext(); + execution.start(null, context); + assertEquals("viewState2", execution.getActiveSession().getState().getId()); + assertNotNull(execution.getActiveSession().getScope().get("items")); + execution.signalEvent("event2", context); + assertTrue(!execution.isActive()); + } + + public void testFlashScope() { + FlowExecution execution = new FlowExecutionImpl(new FlashScopeFlow()); + MockExternalContext context = new MockExternalContext(); + execution.start(null, context); + execution.refresh(context); + execution.refresh(context); + execution.signalEvent("view", context); + } + + public static TransitionCriteria onEvent(String event) { + return new EventIdTransitionCriteria(event); + } + + protected TargetStateResolver toState(String stateId) { + return new DefaultTargetStateResolver(stateId); + } + + public static ViewSelector selectView(String viewName) { + return new ApplicationViewSelector(new StaticExpression(viewName)); + } + + private class FlashScopeFlow extends Flow { + + public FlashScopeFlow() { + super("flashScopeFlow"); + + ActionState state1 = new ActionState(this, "action"); + state1.getActionList().add(new Action() { + public Event execute(RequestContext context) throws Exception { + context.getFlashScope().put("flashScopedValue", "flashScopedValue"); + return new Event(this, "success"); + } + }); + state1.getTransitionSet().add(new Transition(toState("view"))); + + ViewState state2 = new ViewState(this, "view"); + state2.getEntryActionList().add(new Action() { + public Event execute(RequestContext context) throws Exception { + assertTrue(context.getFlashScope().contains("flashScopedValue")); + return new Event(this, "success"); + } + }); + state2.getTransitionSet().add(new Transition(toState("end"))); + + EndState state3 = new EndState(this, "end"); + state3.getEntryActionList().add(new Action() { + public Event execute(RequestContext context) throws Exception { + assertFalse(context.getFlashScope().contains("flashScopedValue")); + return new Event(this, "success"); + } + }); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/InfiniteLoopTestAction.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/InfiniteLoopTestAction.java new file mode 100644 index 00000000..c587e6d3 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/InfiniteLoopTestAction.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.impl; + +import org.springframework.webflow.action.MultiAction; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; + +public class InfiniteLoopTestAction extends MultiAction { + public Event method(RequestContext context) { + return success(); + } + + public Event errorMethod(RequestContext context) { + return error(); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/MiscFlowExecutionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/MiscFlowExecutionTests.java new file mode 100644 index 00000000..30cc4ad0 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/MiscFlowExecutionTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.impl; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.support.StaticExpression; +import org.springframework.binding.mapping.RequiredMappingException; +import org.springframework.core.io.ClassPathResource; +import org.springframework.webflow.action.AbstractAction; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.SubflowState; +import org.springframework.webflow.engine.ViewState; +import org.springframework.webflow.engine.builder.FlowAssembler; +import org.springframework.webflow.engine.builder.xml.XmlFlowBuilder; +import org.springframework.webflow.engine.support.ApplicationViewSelector; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.test.MockExternalContext; + +public class MiscFlowExecutionTests extends TestCase { + public void testRequestScopePutInEntryAction() { + Flow parentFlow = new Flow("parent"); + Flow flow = new Flow("test"); + new SubflowState(parentFlow, "parentState", flow); + + ViewState state = new ViewState(flow, "view"); + state.setViewSelector(new ApplicationViewSelector(new StaticExpression("myView"))); + final Object order = new Object(); + state.getEntryActionList().add(new AbstractAction() { + protected Event doExecute(RequestContext context) { + context.getRequestScope().put("order", order); + return success(); + } + }); + FlowExecution execution = new FlowExecutionImpl(parentFlow); + ApplicationView response = (ApplicationView)execution.start(null, new MockExternalContext()); + assertNotNull(response.getModel().get("order")); + assertEquals(order, response.getModel().get("order")); + } + + public void testRequiredMapping() { + XmlFlowBuilder builder = new XmlFlowBuilder(new ClassPathResource("required-mapping.xml", getClass())); + Flow flow = new FlowAssembler("myFlow", builder).assembleFlow(); + FlowExecutionImpl execution = new FlowExecutionImpl(flow); + LocalAttributeMap input = new LocalAttributeMap(); + input.put("id", "23"); + ApplicationView view = (ApplicationView)execution.start(input, new MockExternalContext()); + assertEquals(new Long(23), view.getModel().get("id")); + } + + public void testRequiredMappingException() { + XmlFlowBuilder builder = new XmlFlowBuilder(new ClassPathResource("required-mapping.xml", getClass())); + Flow flow = new FlowAssembler("myFlow", builder).assembleFlow(); + FlowExecutionImpl execution = new FlowExecutionImpl(flow); + try { + execution.start(null, new MockExternalContext()); + } catch (RequiredMappingException e) { + + } + } + + /* + public void testInfiniteLoop() { + XmlFlowBuilder builder = new XmlFlowBuilder(new ClassPathResource("infinite-loop.xml", getClass())); + Flow flow = new FlowAssembler("myFlow", builder).assembleFlow(); + FlowExecutionImpl execution = new FlowExecutionImpl(flow); + try { + execution.start(null, new MockExternalContext()); + } catch (RequiredMappingException e) { + + } + } + */ +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/NestedSubflowRestorationTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/NestedSubflowRestorationTests.java new file mode 100644 index 00000000..20609b3c --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/NestedSubflowRestorationTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.impl; + +import org.springframework.core.io.ClassPathResource; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.registry.FlowDefinitionConstructionException; +import org.springframework.webflow.definition.registry.FlowDefinitionLocator; +import org.springframework.webflow.definition.registry.FlowDefinitionResource; +import org.springframework.webflow.definition.registry.NoSuchFlowDefinitionException; +import org.springframework.webflow.engine.impl.FlowExecutionImplStateRestorer; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.repository.continuation.SerializedFlowExecutionContinuation; +import org.springframework.webflow.test.execution.AbstractXmlFlowExecutionTests; + +/** + * Tests dealing with restoration of nested subflows. + * + * @author Erwin Vervaet + */ +public class NestedSubflowRestorationTests extends AbstractXmlFlowExecutionTests implements FlowDefinitionLocator { + + protected FlowDefinitionResource getFlowDefinitionResource() { + return new FlowDefinitionResource(new ClassPathResource("nestedSubflow.xml", NestedSubflowRestorationTests.class)); + } + + public FlowDefinition getFlowDefinition(String id) + throws NoSuchFlowDefinitionException, FlowDefinitionConstructionException { + return getFlowDefinition(); + } + + public void testNestedFlows() { + startFlow(); + assertFlowExecutionActive(); + assertActiveFlowEquals("nestedSubflow"); + assertCurrentStateEquals("view1"); + signalEvent("start"); + assertFlowExecutionActive(); + assertActiveFlowEquals("subflowDef3"); + assertCurrentStateEquals("view4"); + + FlowExecution flowExecution = getFlowExecution(); + flowExecution = new SerializedFlowExecutionContinuation(flowExecution, false).unmarshal(); + flowExecution = new FlowExecutionImplStateRestorer(this).restoreState(flowExecution, null); + updateFlowExecution(flowExecution); + + signalEvent("continue"); + assertFlowExecutionEnded(); + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/external-subflow.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/external-subflow.xml new file mode 100644 index 00000000..a6e2c431 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/external-subflow.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/infinite-loop-beans.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/infinite-loop-beans.xml new file mode 100644 index 00000000..59b0f41c --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/infinite-loop-beans.xml @@ -0,0 +1,11 @@ + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/infinite-loop.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/infinite-loop.xml new file mode 100644 index 00000000..d42f1902 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/infinite-loop.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/nestedSubflow.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/nestedSubflow.xml new file mode 100644 index 00000000..e99d5a04 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/nestedSubflow.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/required-mapping.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/required-mapping.xml new file mode 100644 index 00000000..8c2eb4e6 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/required-mapping.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/testFlow.xml b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/testFlow.xml new file mode 100644 index 00000000..31ea9ebb --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/impl/testFlow.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/support/ActionTransitionCriteriaTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/ActionTransitionCriteriaTests.java new file mode 100644 index 00000000..bf105f31 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/ActionTransitionCriteriaTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import junit.framework.TestCase; + +import org.easymock.EasyMock; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Unit tests for the ActionTransitionCriteria class. + * + * @author Ulrik Sandberg + */ +public class ActionTransitionCriteriaTests extends TestCase { + + private Action actionMock; + + private ActionTransitionCriteria tested; + + protected void setUp() throws Exception { + super.setUp(); + actionMock = (Action)EasyMock.createMock(Action.class); + tested = new ActionTransitionCriteria(actionMock); + } + + public void testGetTrueEventId() { + String id = tested.getTrueEventId(); + assertEquals("success", id); + } + + public void testSetTrueEventId() { + tested.setTrueEventId("something"); + String id = tested.getTrueEventId(); + assertEquals("something", id); + } + + public void testGetAction() { + Action action = tested.getAction(); + assertSame(actionMock, action); + } + + public void testTest() throws Exception { + MockRequestContext mockRequestContext = new MockRequestContext(); + EasyMock.expect(actionMock.execute(mockRequestContext)).andReturn(new Event(this, "success")); + EasyMock.replay(new Object[] { actionMock }); + boolean result = tested.test(mockRequestContext); + EasyMock.verify(new Object[] { actionMock }); + assertEquals(true, result); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/support/ApplicationViewSelectorTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/ApplicationViewSelectorTests.java new file mode 100644 index 00000000..2960f7b7 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/ApplicationViewSelectorTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.binding.expression.support.StaticExpression; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.test.MockFlowExecutionContext; +import org.springframework.webflow.test.MockRequestContext; + +public class ApplicationViewSelectorTests extends TestCase { + ExpressionParser parser = DefaultExpressionParserFactory.getExpressionParser(); + + public void testMakeSelection() { + Expression exp = parser.parseExpression("${requestScope.viewVar}"); + ApplicationViewSelector selector = new ApplicationViewSelector(exp); + MockRequestContext context = new MockRequestContext(); + context.getRequestScope().put("viewVar", "view"); + context.getRequestScope().put("foo", "bar"); + context.getFlowScope().put("foo", "bar2"); + context.getFlowScope().put("foo2", "bar"); + context.getConversationScope().put("foo", "bar3"); + context.getConversationScope().put("foo3", "bar"); + ViewSelection selection = selector.makeEntrySelection(context); + assertTrue(selection instanceof ApplicationView); + ApplicationView view = (ApplicationView)selection; + assertEquals("view", view.getViewName()); + assertEquals("bar", view.getModel().get("foo")); + assertEquals("bar", view.getModel().get("foo2")); + assertEquals("bar", view.getModel().get("foo3")); + } + + public void testMakeNullSelection() { + ApplicationViewSelector selector = new ApplicationViewSelector(new StaticExpression(null)); + MockRequestContext context = new MockRequestContext(); + try { + selector.makeEntrySelection(context); + fail(); + } + catch (IllegalStateException e) { + //expected + } + } + + public void testMakeNullSelectionEmptyString() { + ApplicationViewSelector selector = new ApplicationViewSelector(new StaticExpression("")); + MockRequestContext context = new MockRequestContext(); + try { + selector.makeEntrySelection(context); + fail(); + } + catch (IllegalStateException e) { + //expected + } + } + + public void testIsEntrySelectionRenderable() { + ApplicationViewSelector selector = new ApplicationViewSelector(new StaticExpression(null)); + MockRequestContext context = new MockRequestContext(); + assertTrue(selector.isEntrySelectionRenderable(context)); + } + + public void testIsEntrySelectionRenderableRedirect() { + ApplicationViewSelector selector = new ApplicationViewSelector(new StaticExpression(null), true); + MockRequestContext context = new MockRequestContext(); + assertFalse(selector.isEntrySelectionRenderable(context)); + } + + public void testIsEntrySelectionRenderableAlwaysRedirectOnPause() { + ApplicationViewSelector selector = new ApplicationViewSelector(new StaticExpression(null)); + MockRequestContext requestContext = new MockRequestContext(); + MockFlowExecutionContext flowExecutionContext = new MockFlowExecutionContext(); + flowExecutionContext.putAttribute("alwaysRedirectOnPause", Boolean.TRUE); + requestContext.setFlowExecutionContext(flowExecutionContext); + assertFalse(selector.isEntrySelectionRenderable(requestContext)); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/support/AttributeExpressionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/AttributeExpressionTests.java new file mode 100644 index 00000000..de10571e --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/AttributeExpressionTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import java.util.HashMap; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.support.StaticExpression; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Unit tests for {@link AttributeExpression}. + */ +public class AttributeExpressionTests extends TestCase { + + public void testFlowScopeExpression() { + Expression exp = DefaultExpressionParserFactory.getExpressionParser().parseExpression("foo"); + AttributeExpression flowExp = new AttributeExpression(exp, ScopeType.FLOW); + MockRequestContext context = new MockRequestContext(); + context.getFlowScope().put("foo", "bar"); + assertEquals("bar", flowExp.evaluate(context, null)); + } + + public void testFlowScopeSettableExpression() { + Expression exp = DefaultExpressionParserFactory.getExpressionParser().parseSettableExpression("foo"); + AttributeExpression flowExp = new AttributeExpression(exp, ScopeType.FLOW); + MockRequestContext context = new MockRequestContext(); + context.getFlowScope().put("foo", "bar"); + flowExp.evaluateToSet(context, "newValue", null); + assertEquals("newValue", context.getFlowScope().get("foo")); + } + + public void testAttributeMapExpression() { + Expression exp = DefaultExpressionParserFactory.getExpressionParser().parseExpression("foo"); + AttributeExpression attrExp = new AttributeExpression(exp); + MutableAttributeMap attributeMap = new LocalAttributeMap(); + attributeMap.put("foo", "bar"); + assertEquals("bar", attrExp.evaluate(attributeMap, null)); + } + + public void testAttributeMapSettableExpression() { + Expression exp = DefaultExpressionParserFactory.getExpressionParser().parseSettableExpression("foo"); + AttributeExpression attrExp = new AttributeExpression(exp); + MutableAttributeMap attributeMap = new LocalAttributeMap(); + attributeMap.put("foo", "bar"); + attrExp.evaluateToSet(attributeMap, "newValue", null); + assertEquals("newValue", attributeMap.get("foo")); + } + + public void testInvalidExpressionType() { + Expression exp = new StaticExpression("value"); + AttributeExpression attrExp = new AttributeExpression(exp); + try { + attrExp.evaluateToSet(new LocalAttributeMap(), "newValue", null); + fail("we need a SettableExpression"); + } + catch (IllegalArgumentException e) { + } + } + + public void testUnsupportedTarget() { + try { + new AttributeExpression(new StaticExpression("value")).evaluate(new HashMap(), null); + fail("a Map is not supported"); + } + catch(IllegalArgumentException e) { + } + try { + Expression exp = DefaultExpressionParserFactory.getExpressionParser().parseSettableExpression("foo"); + new AttributeExpression(exp).evaluate(new HashMap(), null); + fail("a Map is not supported"); + } + catch(IllegalArgumentException e) { + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/support/BeanFactoryFlowVariableTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/BeanFactoryFlowVariableTests.java new file mode 100644 index 00000000..97403733 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/BeanFactoryFlowVariableTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import junit.framework.TestCase; + +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.test.MockRequestContext; + +public class BeanFactoryFlowVariableTests extends TestCase { + private MockRequestContext context = new MockRequestContext(); + + public void testCreateValidFlowVariable() { + StaticApplicationContext beanFactory = new StaticApplicationContext(); + beanFactory.registerPrototype("bean", Object.class); + BeanFactoryFlowVariable variable = new BeanFactoryFlowVariable("var", "bean", beanFactory, ScopeType.FLOW); + variable.create(context); + context.getFlowScope().getRequired("var"); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/support/BooleanExpressionTransitionCriteriaTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/BooleanExpressionTransitionCriteriaTests.java new file mode 100644 index 00000000..8ee9588c --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/BooleanExpressionTransitionCriteriaTests.java @@ -0,0 +1,69 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Unit tests for {@link org.springframework.webflow.engine.support.BooleanExpressionTransitionCriteria}. + */ +public class BooleanExpressionTransitionCriteriaTests extends TestCase { + + private ExpressionParser parser = DefaultExpressionParserFactory.getExpressionParser(); + + public void testMatchCriteria() { + Expression exp = parser.parseExpression("${requestScope.flag}"); + BooleanExpressionTransitionCriteria c = new BooleanExpressionTransitionCriteria(exp); + MockRequestContext context = new MockRequestContext(); + context.getRequestScope().put("flag", Boolean.TRUE); + assertEquals(true, c.test(context)); + } + + public void testNotABoolean() { + Expression exp = parser.parseExpression("${requestScope.flag}"); + BooleanExpressionTransitionCriteria c = new BooleanExpressionTransitionCriteria(exp); + MockRequestContext context = new MockRequestContext(); + context.getRequestScope().put("flag", "foo"); + try { + c.test(context); + fail("not a boolean"); + } + catch (IllegalArgumentException e) { + } + } + + public void testResult() { + Expression exp = parser.parseExpression("${#result == 'foo'}"); + BooleanExpressionTransitionCriteria c = new BooleanExpressionTransitionCriteria(exp); + MockRequestContext context = new MockRequestContext(); + context.setLastEvent(new Event(this, "foo")); + assertEquals(true, c.test(context)); + } + + public void testFunctionInvocation() { + Expression exp = parser.parseExpression("${#result.endsWith('error')}"); + BooleanExpressionTransitionCriteria c = new BooleanExpressionTransitionCriteria(exp); + MockRequestContext context = new MockRequestContext(); + context.setLastEvent(new Event(this, "error")); + assertTrue(c.test(context)); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/support/ConfigurableFlowAttributeMapperTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/ConfigurableFlowAttributeMapperTests.java new file mode 100644 index 00000000..f61b3c8e --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/ConfigurableFlowAttributeMapperTests.java @@ -0,0 +1,220 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.support.OgnlExpressionParser; +import org.springframework.binding.mapping.Mapping; +import org.springframework.binding.mapping.MappingBuilder; +import org.springframework.webflow.action.FormAction; +import org.springframework.webflow.core.collection.CollectionUtils; +import org.springframework.webflow.core.collection.LocalAttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.test.MockFlowSession; +import org.springframework.webflow.test.MockRequestContext; + +/** + * Test case for {@link ConfigurableFlowAttributeMapper}. + * + * @author Erwin Vervaet + */ +public class ConfigurableFlowAttributeMapperTests extends TestCase { + + private ConfigurableFlowAttributeMapper mapper; + + private MockRequestContext context; + + private MockFlowSession parentSession; + + private MockFlowSession subflowSession; + + private MappingBuilder mapping; + + protected void setUp() throws Exception { + mapper = new ConfigurableFlowAttributeMapper(); + mapping = new MappingBuilder(new OgnlExpressionParser()); + context = new MockRequestContext(); + parentSession = new MockFlowSession(); + subflowSession = new MockFlowSession(); + subflowSession.setParent(parentSession); + } + + public void testAttributeMapping() { + mapper.addInputAttribute("x"); + mapper.addOutputAttribute("y"); + + context.setActiveSession(parentSession); + context.getFlowScope().put("x", "xValue"); + MutableAttributeMap input = mapper.createFlowInput(context); + assertEquals(1, input.size()); + assertEquals("xValue", input.get("x")); + + parentSession.getScope().clear(); + + MutableAttributeMap subflowOutput = new LocalAttributeMap(); + subflowOutput.put("y", "yValue"); + mapper.mapFlowOutput(subflowOutput, context); + assertEquals(1, parentSession.getScope().size()); + assertEquals("yValue", parentSession.getScope().get("y")); + } + + public void testDirectMapping() { + mapper.addInputMapping(mapping.source("${flowScope.x}").target("${y}").value()); + mapper.addOutputMapping(mapping.source("y").target("flowScope.y").value()); + + context.setActiveSession(parentSession); + context.getFlowScope().put("x", "xValue"); + MutableAttributeMap input = mapper.createFlowInput(context); + assertEquals(1, input.size()); + assertEquals("xValue", input.get("y")); + + parentSession.getScope().clear(); + + MutableAttributeMap subflowOutput = new LocalAttributeMap(); + subflowOutput.put("y", "xValue"); + mapper.mapFlowOutput(subflowOutput, context); + assertEquals(1, parentSession.getScope().size()); + assertEquals("xValue", parentSession.getScope().get("y")); + } + + public void testBeanPropertyMapping() { + mapper.addInputMappings(new Mapping[] { mapping.source("flowScope.bean.prop").target("attr").value(), + mapping.source("flowScope.bean").target("otherBean").value(), + mapping.source("flowScope.otherAttr").target("otherBean.prop ").value() }); + mapper.addOutputMappings(new Mapping[] { mapping.source("bean.prop").target("flowScope.attr").value(), + mapping.source("bean").target("flowScope.otherBean").value(), + mapping.source("otherAttr").target("flowScope.otherBean.prop").value() }); + + TestBean bean = new TestBean(); + bean.setProp("value"); + + context.setActiveSession(parentSession); + context.getFlowScope().put("bean", bean); + context.getFlowScope().put("otherAttr", "otherValue"); + MutableAttributeMap input = mapper.createFlowInput(context); + assertEquals(2, input.size()); + assertEquals("value", input.get("attr")); + assertEquals("otherValue", ((TestBean)input.get("otherBean")).getProp()); + + parentSession.getScope().clear(); + bean.setProp("value"); + + MutableAttributeMap subflowOutput = new LocalAttributeMap(); + subflowOutput.put("bean", bean); + subflowOutput.put("otherAttr", "otherValue"); + mapper.mapFlowOutput(subflowOutput, context); + assertEquals(2, parentSession.getScope().size()); + assertEquals("value", parentSession.getScope().get("attr")); + assertEquals("otherValue", ((TestBean)parentSession.getScope().get("otherBean")).getProp()); + } + + public void testExpressionMapping() { + mapper.addInputMappings(new Mapping[] { mapping.source("${requestScope.a}").target("b").value(), + mapping.source("${flowScope.x}").target("y").value() }); + mapper.addOutputMappings(new Mapping[] { mapping.source("b").target("flowScope.c").value(), + mapping.source("y").target("flowScope.z").value() }); + + context.setActiveSession(parentSession); + context.getRequestScope().put("a", "aValue"); + context.getFlowScope().put("x", "xValue"); + MutableAttributeMap input = mapper.createFlowInput(context); + assertEquals(2, input.size()); + assertEquals("aValue", input.get("b")); + assertEquals("xValue", input.get("y")); + + parentSession.getScope().clear(); + + MutableAttributeMap subflowOutput = new LocalAttributeMap(); + subflowOutput.put("b", "aValue"); + subflowOutput.put("y", "xValue"); + mapper.mapFlowOutput(subflowOutput, context); + assertEquals(2, parentSession.getScope().size()); + assertEquals("aValue", parentSession.getScope().get("c")); + assertEquals("xValue", parentSession.getScope().get("z")); + } + + public void testNullMapping() { + mapper.addInputMappings(new Mapping[] { mapping.source("${flowScope.x}").target("y").value(), + mapping.source("${flowScope.a}").target("b").value() }); + mapper.addOutputMappings(new Mapping[] { mapping.source("y").target("flowScope.c").value(), + mapping.source("b").target("flowScope.z").value() }); + + parentSession.getScope().put("x", null); + + context.setActiveSession(parentSession); + MutableAttributeMap input = mapper.createFlowInput(context); + assertEquals(0, input.size()); + assertFalse(input.contains("y")); + assertFalse(input.contains("b")); + + parentSession.getScope().clear(); + + mapper.mapFlowOutput(CollectionUtils.EMPTY_ATTRIBUTE_MAP, context); + assertEquals(0, parentSession.getScope().size()); + assertFalse(parentSession.getScope().contains("c")); + assertFalse(parentSession.getScope().contains("z")); + } + + public void testFormActionInCombinationWithMapping() throws Exception { + context.setLastEvent(new Event(this, "start")); + + context.setActiveSession(parentSession); + assertTrue(context.getFlowScope().size() == 0); + + FormAction action = new FormAction(); + action.setFormObjectName("command"); + action.setFormObjectClass(TestBean.class); + action.setFormObjectScope(ScopeType.FLOW); + action.setFormErrorsScope(ScopeType.FLOW); + context.setAttribute("method", "setupForm"); + + action.execute(context); + + assertEquals(4, context.getFlowScope().size()); + assertNotNull(context.getFlowScope().get("command")); + + mapper.addInputMapping(mapping.source("${flowScope.command}").target("command").value()); + MutableAttributeMap input = mapper.createFlowInput(context); + + assertEquals(1, input.size()); + assertSame(parentSession.getScope().get("command"), input.get("command")); + assertTrue(subflowSession.getScope().size() == 0); + subflowSession.getScope().replaceWith(input); + + context.setActiveSession(subflowSession); + assertEquals(1, context.getFlowScope().size()); + + action.execute(context); + + assertEquals(4, context.getFlowScope().size()); + assertSame(parentSession.getScope().get("command"), context.getFlowScope().get("command")); + } + + public static class TestBean { + private String prop; + + public String getProp() { + return prop; + } + + public void setProp(String prop) { + this.prop = prop; + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/support/DefaultTargetStateResolverTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/DefaultTargetStateResolverTests.java new file mode 100644 index 00000000..f2ce4a21 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/DefaultTargetStateResolverTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.engine.builder.AbstractFlowBuilder; +import org.springframework.webflow.engine.builder.FlowAssembler; +import org.springframework.webflow.engine.builder.FlowBuilderException; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.test.execution.AbstractFlowExecutionTests; + +/** + * Unit tests for {@link DefaultTargetStateResolver}. + * + * @author Erwin Vervaet + */ +public class DefaultTargetStateResolverTests extends AbstractFlowExecutionTests { + + private boolean fail = false; + + protected FlowDefinition getFlowDefinition() { + return new FlowAssembler("testFlow", new TestFlowBuilder()).assembleFlow(); + } + + public void testNonNullSourceState() { + fail = false; + ViewSelection viewSelection = startFlow(); + assertFlowExecutionActive(); + assertCurrentStateEquals("stateA"); + assertEquals("stateAView", ((ApplicationView)viewSelection).getViewName()); + viewSelection = signalEvent("aEvent"); + assertFlowExecutionActive(); + assertCurrentStateEquals("stateB"); + assertEquals("stateBView", ((ApplicationView)viewSelection).getViewName()); + viewSelection = signalEvent("bEvent"); + assertFlowExecutionEnded(); + assertTrue(viewSelection == ViewSelection.NULL_VIEW); + } + + public void testNullSourceState() { + fail = true; + ViewSelection viewSelection = startFlow(); + assertFlowExecutionEnded(); + assertTrue(viewSelection == ViewSelection.NULL_VIEW); + } + + private class TestFlowBuilder extends AbstractFlowBuilder { + + public void buildStartActions() throws FlowBuilderException { + getFlow().getStartActionList().add(new Action() { + public Event execute(RequestContext context) throws Exception { + if (fail) { + throw new UnsupportedOperationException(); + } + return new Event(this, "success"); + } + }); + } + + public void buildExceptionHandlers() throws FlowBuilderException { + TransitionExecutingStateExceptionHandler handler = new TransitionExecutingStateExceptionHandler(); + handler.add(UnsupportedOperationException.class, "stateC"); + getFlow().getExceptionHandlerSet().add(handler); + } + + public void buildStates() throws FlowBuilderException { + addViewState("stateA", "stateAView", transition(on("aEvent"), to("stateB"))); + addViewState("stateB", "stateBView", transition(on("bEvent"), to("stateC"))); + addEndState("stateC"); + } + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/support/EventIdTransitionCriteriaTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/EventIdTransitionCriteriaTests.java new file mode 100644 index 00000000..e3b87bcb --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/EventIdTransitionCriteriaTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import junit.framework.TestCase; + +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.test.MockRequestContext; + +public class EventIdTransitionCriteriaTests extends TestCase { + public void testTestCriteria() { + EventIdTransitionCriteria c = new EventIdTransitionCriteria("foo"); + MockRequestContext context = new MockRequestContext(); + context.setLastEvent(new Event(this, "foo")); + assertEquals(true, c.test(context)); + context.setLastEvent(new Event(this, "FOO")); + assertEquals(false, c.test(context)); // case sensitive + context.setLastEvent(new Event(this, "bar")); + assertEquals(false, c.test(context)); + } + + public void testNullLastEventId() { + EventIdTransitionCriteria c = new EventIdTransitionCriteria("foo"); + MockRequestContext context = new MockRequestContext(); + context.setLastEvent(null); + assertEquals(false, c.test(context)); + } + + public void testIllegalArg(){ + try { + new EventIdTransitionCriteria(null); + fail("was null"); + } catch (IllegalArgumentException e) { + + } + try { + new EventIdTransitionCriteria(""); + fail("was blank"); + } catch (IllegalArgumentException e) { + + } + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/support/FlowDefinitionRedirectSelectorTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/FlowDefinitionRedirectSelectorTests.java new file mode 100644 index 00000000..58a6b4cc --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/FlowDefinitionRedirectSelectorTests.java @@ -0,0 +1,55 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.ExpressionParser; +import org.springframework.webflow.core.DefaultExpressionParserFactory; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; +import org.springframework.webflow.test.MockRequestContext; + +public class FlowDefinitionRedirectSelectorTests extends TestCase { + ExpressionParser parser = DefaultExpressionParserFactory.getExpressionParser(); + + public void testMakeSelection() { + Expression exp = parser.parseExpression("${requestScope.flowIdVar}?a=b&c=${requestScope.bar}"); + FlowDefinitionRedirectSelector selector = new FlowDefinitionRedirectSelector(exp); + MockRequestContext context = new MockRequestContext(); + context.getRequestScope().put("flowIdVar", "foo"); + context.getRequestScope().put("bar", "baz"); + ViewSelection selection = selector.makeEntrySelection(context); + assertTrue(selection instanceof FlowDefinitionRedirect); + FlowDefinitionRedirect redirect = (FlowDefinitionRedirect)selection; + assertEquals("foo", redirect.getFlowDefinitionId()); + assertEquals("b", redirect.getExecutionInput().get("a")); + assertEquals("baz", redirect.getExecutionInput().get("c")); + } + + public void testMakeSelectionInvalidVariable() { + Expression exp = parser.parseExpression("${flowScope.flowId}"); + FlowDefinitionRedirectSelector selector = new FlowDefinitionRedirectSelector(exp); + MockRequestContext context = new MockRequestContext(); + try { + ViewSelection selection = selector.makeEntrySelection(context); + assertTrue(selection instanceof FlowDefinitionRedirect); + } catch (IllegalStateException e) { + + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/support/NotTransitionCriteriaTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/NotTransitionCriteriaTests.java new file mode 100644 index 00000000..a799cf60 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/NotTransitionCriteriaTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import org.springframework.webflow.engine.WildcardTransitionCriteria; +import org.springframework.webflow.test.MockRequestContext; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link NotTransitionCriteria}. + * + * @author Erwin Vervaet + */ +public class NotTransitionCriteriaTests extends TestCase { + + public void testNull() { + try { + new NotTransitionCriteria(null); + fail(); + } + catch (IllegalArgumentException e) { + } + } + + public void testNegation() { + assertFalse(new NotTransitionCriteria(WildcardTransitionCriteria.INSTANCE).test(new MockRequestContext())); + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/support/NullViewSelectionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/NullViewSelectionTests.java new file mode 100644 index 00000000..1119136a --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/NullViewSelectionTests.java @@ -0,0 +1,35 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import junit.framework.TestCase; + +import org.springframework.webflow.engine.NullViewSelector; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.test.MockRequestContext; + +public class NullViewSelectionTests extends TestCase { + + private MockRequestContext context = new MockRequestContext(); + + public void testMakeSelection() { + assertEquals(ViewSelection.NULL_VIEW, NullViewSelector.INSTANCE.makeEntrySelection(context)); + } + + public void testMakeRefreshSelection() { + assertEquals(ViewSelection.NULL_VIEW, NullViewSelector.INSTANCE.makeRefreshSelection(context)); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/support/SimpleFlowVariableTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/SimpleFlowVariableTests.java new file mode 100644 index 00000000..43b65686 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/SimpleFlowVariableTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import java.util.ArrayList; + +import junit.framework.TestCase; + +import org.springframework.webflow.execution.ScopeType; +import org.springframework.webflow.test.MockRequestContext; + +public class SimpleFlowVariableTests extends TestCase { + private MockRequestContext context = new MockRequestContext(); + + public void testCreateValidFlowVariableCustomScope() { + SimpleFlowVariable variable = new SimpleFlowVariable("var", ArrayList.class, ScopeType.REQUEST); + variable.create(context); + assertTrue(context.getRequestScope().contains("var")); + context.getRequestScope().getRequired("var", ArrayList.class); + } + + public void testCreateVariableNoDefaultConstructor() { + SimpleFlowVariable variable = new SimpleFlowVariable("var", Integer.class, ScopeType.FLOW); + try { + variable.create(context); + fail("should have failed"); + } catch (Exception e) { + + } + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/support/TransitionCriteriaChainTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/TransitionCriteriaChainTests.java new file mode 100644 index 00000000..529d6115 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/TransitionCriteriaChainTests.java @@ -0,0 +1,119 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import org.springframework.webflow.engine.AnnotatedAction; +import org.springframework.webflow.engine.TransitionCriteria; +import org.springframework.webflow.execution.Action; +import org.springframework.webflow.execution.Event; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.support.EventFactorySupport; +import org.springframework.webflow.test.MockRequestContext; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link TransitionCriteriaChain}. + * + * @author Erwin Vervaet + */ +public class TransitionCriteriaChainTests extends TestCase { + + private TransitionCriteriaChain chain; + private MockRequestContext context; + + protected void setUp() throws Exception { + chain = new TransitionCriteriaChain(); + context = new MockRequestContext(); + } + + public void testEmptyChain() { + assertTrue(chain.test(context)); + } + + public void testAllTrue() { + TestTransitionCriteria criteria1 = new TestTransitionCriteria(true); + TestTransitionCriteria criteria2 = new TestTransitionCriteria(true); + TestTransitionCriteria criteria3 = new TestTransitionCriteria(true); + chain.add(criteria1); + chain.add(criteria2); + chain.add(criteria3); + assertTrue(chain.test(context)); + assertTrue(criteria1.tested); + assertTrue(criteria2.tested); + assertTrue(criteria3.tested); + } + + public void testWithFalse() { + TestTransitionCriteria criteria1 = new TestTransitionCriteria(true); + TestTransitionCriteria criteria2 = new TestTransitionCriteria(false); + TestTransitionCriteria criteria3 = new TestTransitionCriteria(true); + chain.add(criteria1); + chain.add(criteria2); + chain.add(criteria3); + assertFalse(chain.test(context)); + assertTrue(criteria1.tested); + assertTrue(criteria2.tested); + assertFalse(criteria3.tested); + } + + public void testCriteriaChainForNoActions() { + TransitionCriteria actionChain = TransitionCriteriaChain.criteriaChainFor(null); + assertTrue(actionChain.test(context)); + } + + public void testCriteriaChainForActions() { + AnnotatedAction[] actions = new AnnotatedAction[] { + new AnnotatedAction(new TestAction(true)), + new AnnotatedAction(new TestAction(false)) + }; + TransitionCriteria actionChain = TransitionCriteriaChain.criteriaChainFor(actions); + assertFalse(actionChain.test(context)); + } + + private static class TestTransitionCriteria implements TransitionCriteria { + + public boolean tested = false; + private boolean result; + + public TestTransitionCriteria(boolean result) { + this.result = result; + } + + public boolean test(RequestContext context) { + tested = true; + return result; + } + } + + private static class TestAction implements Action { + + private boolean result; + + public TestAction(boolean result) { + this.result = result; + } + + public Event execute(RequestContext context) throws Exception { + if (result) { + return new EventFactorySupport().success(this); + } + else { + return new EventFactorySupport().error(this); + } + } + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/engine/support/TransitionExecutingStateExceptionHandlerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/TransitionExecutingStateExceptionHandlerTests.java new file mode 100644 index 00000000..f36335a9 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/engine/support/TransitionExecutingStateExceptionHandlerTests.java @@ -0,0 +1,120 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.engine.support; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.support.StaticExpression; +import org.springframework.webflow.TestException; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.engine.EndState; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.RequestControlContext; +import org.springframework.webflow.engine.TargetStateResolver; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.engine.TransitionableState; +import org.springframework.webflow.engine.impl.FlowExecutionImpl; +import org.springframework.webflow.execution.FlowExecutionException; +import org.springframework.webflow.execution.FlowExecutionListener; +import org.springframework.webflow.execution.FlowExecutionListenerAdapter; +import org.springframework.webflow.execution.FlowSession; +import org.springframework.webflow.execution.RequestContext; +import org.springframework.webflow.execution.ViewSelection; +import org.springframework.webflow.test.MockExternalContext; + +public class TransitionExecutingStateExceptionHandlerTests extends TestCase { + + Flow flow; + + TransitionableState state; + + protected void setUp() { + flow = new Flow("myFlow"); + state = new TransitionableState(flow, "state1") { + protected ViewSelection doEnter(RequestControlContext context) { + throw new FlowExecutionException(getFlow().getId(), getId(), "Oops!", new TestException()); + } + }; + state.getTransitionSet().add(new Transition(to("end"))); + } + + public void testTransitionExecutorHandlesExceptionExactMatch() { + TransitionExecutingStateExceptionHandler handler = new TransitionExecutingStateExceptionHandler(); + handler.add(TestException.class, "state"); + FlowExecutionException e = new FlowExecutionException(state.getOwner().getId(), state.getId(), "Oops", + new TestException()); + assertTrue("Doesn't handle state exception", handler.handles(e)); + + e = new FlowExecutionException(state.getOwner().getId(), state.getId(), "Oops", new Exception()); + assertFalse("Shouldn't handle exception", handler.handles(e)); + } + + public void testTransitionExecutorHandlesExceptionSuperclassMatch() { + TransitionExecutingStateExceptionHandler handler = new TransitionExecutingStateExceptionHandler(); + handler.add(Exception.class, "state"); + FlowExecutionException e = new FlowExecutionException(state.getOwner().getId(), state.getId(), "Oops", + new TestException()); + assertTrue("Doesn't handle state exception", handler.handles(e)); + e = new FlowExecutionException(state.getOwner().getId(), state.getId(), "Oops", new RuntimeException()); + assertTrue("Doesn't handle state exception", handler.handles(e)); + } + + public void testFlowStateExceptionHandlingTransition() { + EndState state2 = new EndState(flow, "end"); + state2.setViewSelector(new ApplicationViewSelector(new StaticExpression("view"))); + TransitionExecutingStateExceptionHandler handler = new TransitionExecutingStateExceptionHandler(); + handler.add(TestException.class, "end"); + flow.getExceptionHandlerSet().add(handler); + FlowExecutionListener listener = new FlowExecutionListenerAdapter() { + public void sessionEnding(RequestContext context, FlowSession session, MutableAttributeMap output) { + assertTrue(context.getFlashScope().contains("stateException")); + assertTrue(context.getFlashScope().contains("rootCauseException")); + assertTrue(context.getFlashScope().get("rootCauseException") instanceof TestException); + } + }; + FlowExecutionImpl execution = new FlowExecutionImpl(flow, new FlowExecutionListener[] { listener }, null); + execution.start(null, new MockExternalContext()); + assertTrue("Should have ended", !execution.isActive()); + } + + public void testStateExceptionHandlingTransitionNoSuchState() { + TransitionExecutingStateExceptionHandler handler = new TransitionExecutingStateExceptionHandler(); + handler.add(TestException.class, "end"); + flow.getExceptionHandlerSet().add(handler); + FlowExecutionImpl execution = new FlowExecutionImpl(flow); + try { + execution.start(null, new MockExternalContext()); + fail("Should have failed no such state"); + } + catch (IllegalArgumentException e) { + } + } + + public void testStateExceptionHandlingRethrow() { + FlowExecutionImpl execution = new FlowExecutionImpl(flow); + try { + execution.start(null, new MockExternalContext()); + fail("Should have rethrown"); + } + catch (FlowExecutionException e) { + // expected + } + } + + protected TargetStateResolver to(String stateId) { + return new DefaultTargetStateResolver(stateId); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/MockFlowExecutionListener.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/MockFlowExecutionListener.java new file mode 100644 index 00000000..060e8212 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/MockFlowExecutionListener.java @@ -0,0 +1,232 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + */ +package org.springframework.webflow.execution; + +import org.springframework.util.Assert; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.core.collection.MutableAttributeMap; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.StateDefinition; + +/** + * Mock implementation of the FlowExecutionListener interface for + * use in unit tests. + * + * @author Keith Donald + * @author Erwin Vervaet + */ +public class MockFlowExecutionListener extends FlowExecutionListenerAdapter { + + private boolean sessionStarting; + + private boolean started; + + private boolean executing; + + private boolean paused; + + private int flowNestingLevel; + + private boolean requestInProcess; + + private int requestsSubmitted; + + private int requestsProcessed; + + private int eventsSignaled; + + private boolean stateEntering; + + private int stateTransitions; + + private boolean sessionEnding; + + private int exceptionsThrown; + + /** + * Is the flow execution running: it has started but not yet ended. + */ + public boolean isStarted() { + return started; + } + + /** + * Is the flow execution executing? + */ + public boolean isExecuting() { + return executing; + } + + /** + * Is the flow execution paused? + */ + public boolean isPaused() { + return paused; + } + + /** + * Returns the nesting level of the currently active flow in the flow + * execution. The root flow is at level 0, a sub flow of the root flow is at + * level 1, and so on. + */ + public int getFlowNestingLevel() { + return flowNestingLevel; + } + + /** + * Checks if a request is in process. A request is in process if it was + * submitted but has not yet completed processing. + */ + public boolean isRequestInProcess() { + return requestInProcess; + } + + /** + * Returns the number of requests submitted so far. + */ + public int getRequestsSubmittedCount() { + return requestsSubmitted; + } + + /** + * Returns the number of requests processed so far. + */ + public int getRequestsProcessedCount() { + return requestsProcessed; + } + + /** + * Returns the number of events signaled so far. + */ + public int getEventsSignaledCount() { + return eventsSignaled; + } + + /** + * Returns the number of state transitions executed so far. + */ + public int getTransitionCount() { + return stateTransitions; + } + + /** + * Returns the number of exceptions thrown. + */ + public int getExceptionsThrown() { + return exceptionsThrown; + } + + public void requestSubmitted(RequestContext context) { + Assert.state(!requestInProcess, "There is already a request being processed"); + requestsSubmitted++; + requestInProcess = true; + } + + public void sessionStarting(RequestContext context, FlowDefinition definition, MutableAttributeMap input) { + if (!context.getFlowExecutionContext().isActive()) { + Assert.state(!started, "The flow execution was already started"); + flowNestingLevel = 0; + eventsSignaled = 0; + stateTransitions = 0; + } + sessionStarting = true; + } + + public void sessionStarted(RequestContext context, FlowSession session) { + Assert.state(sessionStarting, "The session should've been starting..."); + sessionStarting = false; + if (session.isRoot()) { + Assert.state(!started, "The flow execution was already started"); + started = true; + executing = true; + } + else { + assertStarted(); + flowNestingLevel++; + } + } + + public void requestProcessed(RequestContext context) { + Assert.state(requestInProcess, "There is no request being processed"); + requestsProcessed++; + requestInProcess = false; + } + + public void eventSignaled(RequestContext context, Event event) { + eventsSignaled++; + } + + public void stateEntering(RequestContext context, StateDefinition state) throws EnterStateVetoException { + stateEntering = true; + } + + public void stateEntered(RequestContext context, StateDefinition newState, StateDefinition previousState) { + Assert.state(stateEntering, "State should've entering..."); + stateEntering = false; + stateTransitions++; + } + + public void paused(RequestContext context, ViewSelection selectedView) { + executing = false; + paused = true; + } + + public void resumed(RequestContext context) { + executing = true; + paused = false; + } + + public void sessionEnding(RequestContext context, FlowSession session, MutableAttributeMap output) { + sessionEnding = true; + } + + public void sessionEnded(RequestContext context, FlowSession session, AttributeMap output) { + assertStarted(); + Assert.state(sessionEnding, "Should have been ending"); + sessionEnding = false; + if (session.isRoot()) { + Assert.state(flowNestingLevel == 0, "The flow execution should have ended"); + started = false; + executing = false; + } + else { + flowNestingLevel--; + Assert.state(started, "The flow execution prematurely ended"); + } + } + + public void exceptionThrown(RequestContext context, FlowExecutionException exception) { + exceptionsThrown++; + } + + /** + * Make sure the flow execution has already been started. + */ + protected void assertStarted() { + Assert.state(started, "The flow execution has not yet been started"); + } + + /** + * Reset all state collected by this listener. + */ + public void reset() { + started = false; + executing = false; + requestsSubmitted = 0; + requestsProcessed = 0; + exceptionsThrown = 0; + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/TestAction.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/TestAction.java new file mode 100644 index 00000000..80c97a6e --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/TestAction.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution; + +import org.springframework.util.StringUtils; +import org.springframework.webflow.action.AbstractAction; + +/** + * Test action for use in unit tests. + */ +public class TestAction extends AbstractAction { + + private Event result = new Event(this, "success"); + + private boolean executed; + + private int executionCount; + + public TestAction() { + + } + + public TestAction(String result) { + if (StringUtils.hasText(result)) { + this.result = new Event(this, result); + } + else { + this.result = null; + } + } + + public boolean isExecuted() { + return executed; + } + + public int getExecutionCount() { + return executionCount; + } + + protected Event doExecute(RequestContext context) throws Exception { + executed = true; + executionCount++; + return result; + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/factory/ConditionalFlowExecutionListenerLoaderTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/factory/ConditionalFlowExecutionListenerLoaderTests.java new file mode 100644 index 00000000..67c89f77 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/factory/ConditionalFlowExecutionListenerLoaderTests.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.factory; + +import junit.framework.TestCase; + +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.execution.FlowExecutionListener; +import org.springframework.webflow.execution.FlowExecutionListenerAdapter; + +/** + * Unit tests for {@link ConditionalFlowExecutionListenerLoader}. + */ +public class ConditionalFlowExecutionListenerLoaderTests extends TestCase { + + private ConditionalFlowExecutionListenerLoader loader = new ConditionalFlowExecutionListenerLoader(); + + public void testAddListener() { + FlowExecutionListener l1 = new FlowExecutionListenerAdapter() { + }; + FlowExecutionListener l2 = new FlowExecutionListenerAdapter() { + }; + loader.addListener(l1); + assertTrue(loader.containsListener(l1)); + loader.addListener(l2); + assertTrue(loader.containsListener(l2)); + FlowExecutionListener[] listeners = loader.getListeners(new Flow("foo")); + assertEquals(2, listeners.length); + assertSame(l1, listeners[0]); + assertSame(l2, listeners[1]); + loader.removeListener(l1); + assertFalse(loader.containsListener(l1)); + loader.removeListener(l2); + assertEquals(0, loader.getListeners(new Flow("flow")).length); + } + + public void testAddListenerWithCriteria() { + FlowExecutionListener l1 = new FlowExecutionListenerAdapter() { + }; + FlowExecutionListener l2 = new FlowExecutionListenerAdapter() { + }; + loader.addListener(l1); + assertTrue(loader.containsListener(l1)); + assertFalse(loader.containsListener(l2)); + final Flow theFlow = new Flow("foo"); + loader.addListener(l2, new FlowExecutionListenerCriteria() { + public boolean appliesTo(FlowDefinition flow) { + assertSame(theFlow, flow); + return false; + } + }); + FlowExecutionListener[] listeners = loader.getListeners(theFlow); + assertEquals(1, listeners.length); + assertSame(l1, listeners[0]); + } + + public void testAddListenerGroup() { + FlowExecutionListener l1 = new FlowExecutionListenerAdapter() { + }; + FlowExecutionListener l2 = new FlowExecutionListenerAdapter() { + }; + FlowExecutionListener l3 = new FlowExecutionListenerAdapter() { + }; + FlowExecutionListener l4 = new FlowExecutionListenerAdapter() { + }; + loader.addListener(l1); + loader.addListener(l2); + loader.addListeners(new FlowExecutionListener[] { l3, l4 }, new FlowExecutionListenerCriteriaFactory() + .flow("bogus")); + assertTrue(loader.containsListener(l1)); + assertTrue(loader.containsListener(l2)); + assertTrue(loader.containsListener(l3)); + assertTrue(loader.containsListener(l4)); + FlowExecutionListener[] listeners = loader.getListeners(new Flow("foo")); + assertEquals(2, listeners.length); + assertSame(l1, listeners[0]); + assertSame(l2, listeners[1]); + } + + public void testNullFlowDefinition() { + try { + loader.getListeners(null); + fail("Should have failed"); + } + catch (IllegalArgumentException e) { + + } + + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/factory/FlowExecutionListenerCriteriaFactoryTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/factory/FlowExecutionListenerCriteriaFactoryTests.java new file mode 100644 index 00000000..9d290abe --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/factory/FlowExecutionListenerCriteriaFactoryTests.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.factory; + +import junit.framework.TestCase; + +import org.springframework.webflow.engine.Flow; + +/** + * Unit tests for {@link FlowExecutionListenerCriteriaFactory}. + */ +public class FlowExecutionListenerCriteriaFactoryTests extends TestCase { + + private FlowExecutionListenerCriteriaFactory factory = new FlowExecutionListenerCriteriaFactory(); + + public void testAllFlows() { + FlowExecutionListenerCriteria c = factory.allFlows(); + assertEquals(true, c.appliesTo(new Flow("foo"))); + } + + public void testFlowMatch() { + FlowExecutionListenerCriteria c = factory.flow("foo"); + assertEquals(true, c.appliesTo(new Flow("foo"))); + assertEquals(false, c.appliesTo(new Flow("baz"))); + } + + public void testMultipleFlowMatch() { + FlowExecutionListenerCriteria c = factory.flows(new String[] { "foo", "bar" }); + assertEquals(true, c.appliesTo(new Flow("foo"))); + assertEquals(true, c.appliesTo(new Flow("bar"))); + assertEquals(false, c.appliesTo(new Flow("baz"))); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/factory/StaticFlowExecutionListenerLoaderTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/factory/StaticFlowExecutionListenerLoaderTests.java new file mode 100644 index 00000000..8d8a6f00 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/factory/StaticFlowExecutionListenerLoaderTests.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.factory; + +import junit.framework.TestCase; + +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.execution.FlowExecutionListener; +import org.springframework.webflow.execution.FlowExecutionListenerAdapter; + +/** + * Unit tests for {@link StaticFlowExecutionListenerLoader}. + */ +public class StaticFlowExecutionListenerLoaderTests extends TestCase { + + private FlowExecutionListenerLoader loader = StaticFlowExecutionListenerLoader.EMPTY_INSTANCE; + + public void testEmptyListenerArray() { + assertEquals(0, loader.getListeners(new Flow("foo")).length); + assertEquals(0, loader.getListeners(null).length); + } + + public void testStaticListener() { + final FlowExecutionListener listener1 = new FlowExecutionListenerAdapter() { + }; + loader = new StaticFlowExecutionListenerLoader(listener1); + assertEquals(listener1, loader.getListeners(new Flow("foo"))[0]); + } + + public void testStaticListeners() { + final FlowExecutionListener listener1 = new FlowExecutionListenerAdapter() { + }; + final FlowExecutionListener listener2 = new FlowExecutionListenerAdapter() { + }; + + loader = new StaticFlowExecutionListenerLoader(new FlowExecutionListener[] { listener1, listener2 }); + assertEquals(listener1, loader.getListeners(new Flow("foo"))[0]); + assertEquals(listener2, loader.getListeners(new Flow("foo"))[1]); + } + +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/continuation/ClientContinuationFlowExecutionRepositoryTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/continuation/ClientContinuationFlowExecutionRepositoryTests.java new file mode 100644 index 00000000..8a0f43a7 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/continuation/ClientContinuationFlowExecutionRepositoryTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.continuation; + +import junit.framework.TestCase; + +import org.springframework.webflow.context.ExternalContextHolder; +import org.springframework.webflow.conversation.impl.SessionBindingConversationManager; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistryImpl; +import org.springframework.webflow.definition.registry.StaticFlowDefinitionHolder; +import org.springframework.webflow.engine.SimpleFlow; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.engine.impl.FlowExecutionImplStateRestorer; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.repository.FlowExecutionKey; +import org.springframework.webflow.execution.repository.FlowExecutionLock; +import org.springframework.webflow.execution.repository.NoSuchFlowExecutionException; +import org.springframework.webflow.execution.repository.support.FlowExecutionStateRestorer; +import org.springframework.webflow.test.MockExternalContext; + +/** + * Unit tests for {@link ClientContinuationFlowExecutionRepository}. + */ +public class ClientContinuationFlowExecutionRepositoryTests extends TestCase { + + private ClientContinuationFlowExecutionRepository repository; + + private FlowExecution execution; + + private FlowExecutionKey key; + + protected void setUp() throws Exception { + FlowDefinitionRegistry registry = new FlowDefinitionRegistryImpl(); + registry.registerFlowDefinition(new StaticFlowDefinitionHolder(new SimpleFlow())); + execution = new FlowExecutionImplFactory().createFlowExecution(registry.getFlowDefinition("simpleFlow")); + FlowExecutionStateRestorer stateRestorer = new FlowExecutionImplStateRestorer(registry); + repository = new ClientContinuationFlowExecutionRepository(stateRestorer, new SessionBindingConversationManager()); + ExternalContextHolder.setExternalContext(new MockExternalContext()); + } + + public void testPutExecution() { + key = repository.generateKey(execution); + assertNotNull(key); + repository.putFlowExecution(key, execution); + FlowExecution persisted = repository.getFlowExecution(key); + assertNotNull(persisted); + } + + public void testGetNextKey() { + key = repository.generateKey(execution); + assertNotNull(key); + repository.putFlowExecution(key, execution); + FlowExecutionKey nextKey = repository.getNextKey(execution, key); + repository.putFlowExecution(nextKey, execution); + FlowExecution persisted = repository.getFlowExecution(nextKey); + assertNotNull(persisted); + } + + public void testGetNextKeyVerifyKeyChanged() { + key = repository.generateKey(execution); + assertNotNull(key); + repository.putFlowExecution(key, execution); + FlowExecutionKey nextKey = repository.getNextKey(execution, key); + repository.putFlowExecution(nextKey, execution); + repository.getFlowExecution(key); + repository.getFlowExecution(nextKey); + } + + public void testRemove() { + testPutExecution(); + repository.removeFlowExecution(key); + try { + repository.getFlowExecution(key); + fail("should've throw nsfee"); + } + catch (NoSuchFlowExecutionException e) { + + } + } + + public void testLock() { + testPutExecution(); + FlowExecutionLock lock = repository.getLock(key); + lock.lock(); + repository.getFlowExecution(key); + lock.unlock(); + } + + public void testLockLock() { + testPutExecution(); + FlowExecutionLock lock = repository.getLock(key); + lock.lock(); + lock.lock(); + repository.getFlowExecution(key); + lock.unlock(); + lock.unlock(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/continuation/ContinuationFlowExecutionRepositoryTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/continuation/ContinuationFlowExecutionRepositoryTests.java new file mode 100644 index 00000000..6af42082 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/continuation/ContinuationFlowExecutionRepositoryTests.java @@ -0,0 +1,112 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.continuation; + +import junit.framework.TestCase; + +import org.springframework.webflow.context.ExternalContextHolder; +import org.springframework.webflow.conversation.impl.SessionBindingConversationManager; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistryImpl; +import org.springframework.webflow.definition.registry.StaticFlowDefinitionHolder; +import org.springframework.webflow.engine.SimpleFlow; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.engine.impl.FlowExecutionImplStateRestorer; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.repository.FlowExecutionKey; +import org.springframework.webflow.execution.repository.FlowExecutionLock; +import org.springframework.webflow.execution.repository.NoSuchFlowExecutionException; +import org.springframework.webflow.execution.repository.support.FlowExecutionStateRestorer; +import org.springframework.webflow.test.MockExternalContext; + +/** + * Unit tests for {@link ContinuationFlowExecutionRepository}. + */ +public class ContinuationFlowExecutionRepositoryTests extends TestCase { + + private ContinuationFlowExecutionRepository repository; + + private FlowExecution execution; + + private FlowExecutionKey key; + + protected void setUp() throws Exception { + FlowDefinitionRegistry registry = new FlowDefinitionRegistryImpl(); + registry.registerFlowDefinition(new StaticFlowDefinitionHolder(new SimpleFlow())); + execution = new FlowExecutionImplFactory().createFlowExecution(registry.getFlowDefinition("simpleFlow")); + FlowExecutionStateRestorer stateRestorer = new FlowExecutionImplStateRestorer(registry); + repository = new ContinuationFlowExecutionRepository(stateRestorer, new SessionBindingConversationManager()); + ExternalContextHolder.setExternalContext(new MockExternalContext()); + } + + public void testPutExecution() { + key = repository.generateKey(execution); + assertNotNull(key); + repository.putFlowExecution(key, execution); + FlowExecution persisted = repository.getFlowExecution(key); + assertNotNull(persisted); + } + + public void testGetNextKey() { + key = repository.generateKey(execution); + assertNotNull(key); + repository.putFlowExecution(key, execution); + FlowExecutionKey nextKey = repository.getNextKey(execution, key); + repository.putFlowExecution(nextKey, execution); + FlowExecution persisted = repository.getFlowExecution(nextKey); + assertNotNull(persisted); + } + + public void testGetNextKeyVerifyKeyChanged() { + key = repository.generateKey(execution); + assertNotNull(key); + repository.putFlowExecution(key, execution); + FlowExecutionKey nextKey = repository.getNextKey(execution, key); + repository.putFlowExecution(nextKey, execution); + repository.getFlowExecution(key); + repository.getFlowExecution(nextKey); + } + + public void testRemove() { + testPutExecution(); + repository.removeFlowExecution(key); + try { + repository.getFlowExecution(key); + fail("should've throw nsfee"); + } + catch (NoSuchFlowExecutionException e) { + + } + } + + public void testLock() { + testPutExecution(); + FlowExecutionLock lock = repository.getLock(key); + lock.lock(); + repository.getFlowExecution(key); + lock.unlock(); + } + + public void testLockLock() { + testPutExecution(); + FlowExecutionLock lock = repository.getLock(key); + lock.lock(); + lock.lock(); + repository.getFlowExecution(key); + lock.unlock(); + lock.unlock(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/continuation/FlowExecutionContinuationGroupTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/continuation/FlowExecutionContinuationGroupTests.java new file mode 100644 index 00000000..0ce64513 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/continuation/FlowExecutionContinuationGroupTests.java @@ -0,0 +1,198 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.continuation; + +import junit.framework.TestCase; + +import org.springframework.webflow.config.FlowExecutorFactoryBean; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.context.ExternalContextHolder; +import org.springframework.webflow.conversation.impl.SessionBindingConversationManager; +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.definition.registry.FlowDefinitionLocator; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistryImpl; +import org.springframework.webflow.definition.registry.StaticFlowDefinitionHolder; +import org.springframework.webflow.engine.builder.AbstractFlowBuilder; +import org.springframework.webflow.engine.builder.FlowAssembler; +import org.springframework.webflow.engine.builder.FlowBuilderException; +import org.springframework.webflow.engine.impl.FlowExecutionImplStateRestorer; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.repository.FlowExecutionKey; +import org.springframework.webflow.execution.repository.NoSuchFlowExecutionException; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.execution.support.FlowExecutionRedirect; +import org.springframework.webflow.executor.FlowExecutor; +import org.springframework.webflow.executor.ResponseInstruction; +import org.springframework.webflow.executor.support.RequestParameterFlowExecutorArgumentHandler; +import org.springframework.webflow.test.MockExternalContext; + +/** + * Unit tests for {@link FlowExecutionContinuationGroup}. + * + * @author Erwin Vervaet + */ +public class FlowExecutionContinuationGroupTests extends TestCase { + + public void testUpdateFlowExecution() { + FlowExecutionContinuationGroup group = new FlowExecutionContinuationGroup(-1); + assertEquals(0, group.getContinuationCount()); + FlowExecutionContinuation continuation1 = new TestFlowExecutionContinuation(); + group.add("1", continuation1); + assertEquals(1, group.getContinuationCount()); + assertSame(continuation1, group.get("1")); + FlowExecutionContinuation continuation2 = new TestFlowExecutionContinuation(); + group.add("2", continuation2); + assertEquals(2, group.getContinuationCount()); + assertSame(continuation1, group.get("1")); + assertSame(continuation2, group.get("2")); + FlowExecutionContinuation updatedContinuation2 = new TestFlowExecutionContinuation(); + group.add("2", updatedContinuation2); + assertEquals(2, group.getContinuationCount()); + assertSame(continuation1, group.get("1")); + assertSame(updatedContinuation2, group.get("2")); + } + + public void testUpdateFlowExecutionWithMaxContinuations() { + FlowExecutionContinuationGroup group = new FlowExecutionContinuationGroup(2); + FlowExecutionContinuation continuation1 = new TestFlowExecutionContinuation(); + group.add("1", continuation1); + FlowExecutionContinuation continuation2 = new TestFlowExecutionContinuation(); + group.add("2", continuation2); + assertEquals(2, group.getContinuationCount()); + assertSame(continuation1, group.get("1")); + assertSame(continuation2, group.get("2")); + FlowExecutionContinuation updatedContinuation2 = new TestFlowExecutionContinuation(); + group.add("2", updatedContinuation2); + assertEquals(2, group.getContinuationCount()); + assertSame(continuation1, group.get("1")); + assertSame(updatedContinuation2, group.get("2")); + FlowExecutionContinuation continuation3 = new TestFlowExecutionContinuation(); + group.add("3", continuation3); + assertEquals(2, group.getContinuationCount()); + try { + group.get("1"); + fail(); + } + catch (ContinuationNotFoundException e) { + // expected + } + assertSame(updatedContinuation2, group.get("2")); + assertSame(continuation3, group.get("3")); + updatedContinuation2 = new TestFlowExecutionContinuation(); + group.add("2", updatedContinuation2); + FlowExecutionContinuation continuation4 = new TestFlowExecutionContinuation(); + group.add("4", continuation4); + assertEquals(2, group.getContinuationCount()); + try { + group.get("3"); + fail(); + } + catch (ContinuationNotFoundException e) { + // expected + } + assertSame(updatedContinuation2, group.get("2")); + assertSame(continuation4, group.get("4")); + } + + public void testViaFlowExecutor() throws Exception { + FlowDefinitionRegistry registry = new FlowDefinitionRegistryImpl(); + FlowDefinition testFlow = new FlowAssembler("testFlow", new TestFlowBuilder()).assembleFlow(); + registry.registerFlowDefinition(new StaticFlowDefinitionHolder(testFlow)); + FlowExecutorFactoryBean flowExecutorFactory = new FlowExecutorFactoryBean(); + flowExecutorFactory.setDefinitionLocator(registry); + flowExecutorFactory.afterPropertiesSet(); + FlowExecutor flowExecutor = (FlowExecutor)flowExecutorFactory.getObject(); + + MockExternalContext externalContext = new MockExternalContext(); + + //obtain continuation group + ResponseInstruction response = flowExecutor.launch("testFlow", externalContext); + externalContext.putRequestParameter("_flowExecutionKey", response.getFlowExecutionKey()); + FlowExecutionContinuationGroup group = new GroupGetter(registry).getContinuationGroup(externalContext); + assertNotNull(group); + + assertTrue(response.getViewSelection() instanceof FlowExecutionRedirect); + assertEquals(1, group.getContinuationCount()); + externalContext.putRequestParameter("_flowExecutionKey", response.getFlowExecutionKey()); + response = flowExecutor.refresh(response.getFlowExecutionKey(), externalContext); + assertEquals("viewName", ((ApplicationView)response.getViewSelection()).getViewName()); + assertEquals(1, group.getContinuationCount()); + + externalContext.putRequestParameter("_flowExecutionKey", response.getFlowExecutionKey()); + response = flowExecutor.resume(response.getFlowExecutionKey(), "next", externalContext); + assertTrue(response.getViewSelection() instanceof FlowExecutionRedirect); + assertEquals(2, group.getContinuationCount()); + externalContext.putRequestParameter("_flowExecutionKey", response.getFlowExecutionKey()); + response = flowExecutor.refresh(response.getFlowExecutionKey(), externalContext); + assertEquals("nextViewName", ((ApplicationView)response.getViewSelection()).getViewName()); + assertEquals(2, group.getContinuationCount()); + + externalContext.putRequestParameter("_flowExecutionKey", response.getFlowExecutionKey()); + response = flowExecutor.refresh(response.getFlowExecutionKey(), externalContext); + assertEquals("nextViewName", ((ApplicationView)response.getViewSelection()).getViewName()); + assertEquals(2, group.getContinuationCount()); + + externalContext.putRequestParameter("_flowExecutionKey", response.getFlowExecutionKey()); + response = flowExecutor.resume(response.getFlowExecutionKey(), "end", externalContext); + + try { + new GroupGetter(registry).getContinuationGroup(externalContext); + fail(); + } + catch (NoSuchFlowExecutionException e) { + // expected + } + } + + private static class TestFlowExecutionContinuation extends FlowExecutionContinuation { + + public FlowExecution unmarshal() throws ContinuationUnmarshalException { + return null; + } + + public byte[] toByteArray() { + return new byte[0]; + } + } + + private static class TestFlowBuilder extends AbstractFlowBuilder { + public void buildStates() throws FlowBuilderException { + addViewState("viewState", "viewName", transition(on("next"), to("nextViewState"))); + addViewState("nextViewState", "nextViewName", transition(on("end"), to("endState"))); + addEndState("endState"); + } + } + + private static class GroupGetter extends ContinuationFlowExecutionRepository { + + public GroupGetter(FlowDefinitionLocator definitionLocator) { + super(new FlowExecutionImplStateRestorer(definitionLocator), new SessionBindingConversationManager()); + } + + public FlowExecutionContinuationGroup getContinuationGroup(ExternalContext externalContext) { + ExternalContextHolder.setExternalContext(externalContext); + try { + FlowExecutionKey key = parseFlowExecutionKey( + new RequestParameterFlowExecutorArgumentHandler().extractFlowExecutionKey(externalContext)); + return getContinuationGroup(key); + } + finally { + ExternalContextHolder.setExternalContext(null); + } + } + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/continuation/SerializedFlowExecutionContinuationTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/continuation/SerializedFlowExecutionContinuationTests.java new file mode 100644 index 00000000..c2a982a1 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/continuation/SerializedFlowExecutionContinuationTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.continuation; + +import java.io.ByteArrayInputStream; +import java.io.ObjectInputStream; + +import junit.framework.TestCase; + +import org.springframework.webflow.definition.FlowDefinition; +import org.springframework.webflow.engine.SimpleFlow; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.test.MockExternalContext; + +/** + * Unit tests for {@link SerializedFlowExecutionContinuation}. + * + * @author Keith Donald + */ +public class SerializedFlowExecutionContinuationTests extends TestCase { + + public void testCreate() throws Exception { + FlowDefinition flow = new SimpleFlow(); + FlowExecution execution = new FlowExecutionImplFactory().createFlowExecution(flow); + execution.start(null, new MockExternalContext()); + SerializedFlowExecutionContinuation c = new SerializedFlowExecutionContinuation(execution, true); + assertTrue(c.isCompressed()); + byte[] array = c.toByteArray(); + execution = c.unmarshal(); + + ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(array)); + try { + c = (SerializedFlowExecutionContinuation)ois.readObject(); + assertTrue(c.isCompressed()); + execution = c.unmarshal(); + } + finally { + ois.close(); + } + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/support/CompositeFlowExecutionKeyTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/support/CompositeFlowExecutionKeyTests.java new file mode 100644 index 00000000..581228e3 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/support/CompositeFlowExecutionKeyTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.support; + +import junit.framework.TestCase; + +import org.springframework.webflow.conversation.impl.SimpleConversationId; + +/** + * Unit tests for {@link CompositeFlowExecutionKey}. + */ +public class CompositeFlowExecutionKeyTests extends TestCase { + + public void testValidKey() { + CompositeFlowExecutionKey key = new CompositeFlowExecutionKey(new SimpleConversationId("foo"), "bar"); + assertEquals("_cfoo_kbar", key.toString()); + } + + public void testKeyEquals() { + CompositeFlowExecutionKey key = new CompositeFlowExecutionKey(new SimpleConversationId("foo"), "bar"); + CompositeFlowExecutionKey key2 = new CompositeFlowExecutionKey(new SimpleConversationId("foo"), "bar"); + assertEquals(key, key2); + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/support/SimpleFlowExecutionRepositoryTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/support/SimpleFlowExecutionRepositoryTests.java new file mode 100644 index 00000000..45247ad2 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/support/SimpleFlowExecutionRepositoryTests.java @@ -0,0 +1,126 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.repository.support; + +import junit.framework.TestCase; + +import org.springframework.webflow.context.ExternalContextHolder; +import org.springframework.webflow.conversation.impl.SessionBindingConversationManager; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistry; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistryImpl; +import org.springframework.webflow.definition.registry.StaticFlowDefinitionHolder; +import org.springframework.webflow.engine.SimpleFlow; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.engine.impl.FlowExecutionImplStateRestorer; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.repository.FlowExecutionKey; +import org.springframework.webflow.execution.repository.FlowExecutionLock; +import org.springframework.webflow.execution.repository.NoSuchFlowExecutionException; +import org.springframework.webflow.execution.repository.PermissionDeniedFlowExecutionAccessException; +import org.springframework.webflow.test.MockExternalContext; + +/** + * Unit tests for {@link SimpleFlowExecutionRepository}. + */ +public class SimpleFlowExecutionRepositoryTests extends TestCase { + + private SimpleFlowExecutionRepository repository; + + private FlowExecution execution; + + private FlowExecutionKey key; + + protected void setUp() throws Exception { + FlowDefinitionRegistry registry = new FlowDefinitionRegistryImpl(); + registry.registerFlowDefinition(new StaticFlowDefinitionHolder(new SimpleFlow())); + execution = new FlowExecutionImplFactory().createFlowExecution(registry.getFlowDefinition("simpleFlow")); + FlowExecutionStateRestorer stateRestorer = new FlowExecutionImplStateRestorer(registry); + repository = new SimpleFlowExecutionRepository(stateRestorer, new SessionBindingConversationManager()); + ExternalContextHolder.setExternalContext(new MockExternalContext()); + } + + public void testPutExecution() { + key = repository.generateKey(execution); + assertNotNull(key); + repository.putFlowExecution(key, execution); + FlowExecution persisted = repository.getFlowExecution(key); + assertNotNull(persisted); + assertSame(execution, persisted); + } + + public void testGetNextKey() { + key = repository.generateKey(execution); + assertNotNull(key); + repository.putFlowExecution(key, execution); + FlowExecutionKey nextKey = repository.getNextKey(execution, key); + repository.putFlowExecution(nextKey, execution); + FlowExecution persisted = repository.getFlowExecution(nextKey); + assertNotNull(persisted); + assertSame(execution, persisted); + } + + public void testGetNextKeyVerifyKeyChanged() { + key = repository.generateKey(execution); + assertNotNull(key); + repository.putFlowExecution(key, execution); + FlowExecutionKey nextKey = repository.getNextKey(execution, key); + repository.putFlowExecution(nextKey, execution); + try { + repository.getFlowExecution(key); + fail("Should've failed"); + } + catch (PermissionDeniedFlowExecutionAccessException e) { + + } + } + + public void testGetNextKeyVerifyKeyStaysSame() { + repository.setAlwaysGenerateNewNextKey(false); + key = repository.generateKey(execution); + FlowExecutionKey nextKey = repository.getNextKey(execution, key); + assertSame(key, nextKey); + } + + public void testRemove() { + testPutExecution(); + repository.removeFlowExecution(key); + try { + repository.getFlowExecution(key); + fail("should've throw nsfee"); + } + catch (NoSuchFlowExecutionException e) { + + } + } + + public void testLock() { + testPutExecution(); + FlowExecutionLock lock = repository.getLock(key); + lock.lock(); + repository.getFlowExecution(key); + lock.unlock(); + } + + public void testLockLock() { + testPutExecution(); + FlowExecutionLock lock = repository.getLock(key); + lock.lock(); + lock.lock(); + repository.getFlowExecution(key); + lock.unlock(); + lock.unlock(); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/support/ApplicationViewTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/support/ApplicationViewTests.java new file mode 100644 index 00000000..8a8f9204 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/support/ApplicationViewTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.support; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link ApplicationView}. + */ +public class ApplicationViewTests extends TestCase { + + public void testConstructAndAccess() { + Map model = new HashMap(); + model.put("name", "value"); + ApplicationView view = new ApplicationView("view", model); + assertEquals("view", view.getViewName()); + assertEquals(1, view.getModel().size()); + assertEquals("value", model.get("name")); + try { + view.getModel().put("foo", "bar"); + } catch (UnsupportedOperationException e) { + + } + } + + public void testNullParams() { + ApplicationView view = new ApplicationView(null, null); + assertEquals(0, view.getModel().size()); + assertEquals(null, view.getViewName()); + ApplicationView view2 = new ApplicationView(null, null); + assertEquals(view, view2); + } + + public void testMapLookup() { + ApplicationView view = new ApplicationView("view", null); + Map map = new HashMap(); + map.put("view", view); + assertSame(view, map.get("view")); + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/support/EventFactorySupportTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/support/EventFactorySupportTests.java new file mode 100644 index 00000000..ed1660f4 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/support/EventFactorySupportTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.support; + +import junit.framework.TestCase; + +import org.springframework.webflow.execution.Event; + +/** + * Unit tests for {@link EventFactorySupport}. + */ +public class EventFactorySupportTests extends TestCase { + + private EventFactorySupport support = new EventFactorySupport(); + + private Object source = new Object(); + + protected void setUp() throws Exception { + } + + public void testSuccess() { + Event e = support.success(source); + assertEquals("success", e.getId()); + assertSame(source, e.getSource()); + } + + public void testSuccessWithResult() { + Object result = new Object(); + Event e = support.success(source, result); + assertEquals("success", e.getId()); + assertSame(source, e.getSource()); + assertSame(result, e.getAttributes().get("result")); + } + + public void testError() { + Event e = support.error(source); + assertEquals("error", e.getId()); + assertSame(source, e.getSource()); + } + + public void testErrorWithException() { + Exception ex = new Exception(); + Event e = support.error(source, ex); + assertEquals("error", e.getId()); + assertSame(source, e.getSource()); + assertSame(ex, e.getAttributes().get("exception")); + } + + public void testYes() { + Event e = support.yes(source); + assertEquals("yes", e.getId()); + assertSame(source, e.getSource()); + } + + public void testNo() { + Event e = support.no(source); + assertEquals("no", e.getId()); + assertSame(source, e.getSource()); + } + + public void testBooleanTrueEvent() { + Event e = support.event(source, true); + assertEquals("yes", e.getId()); + assertSame(source, e.getSource()); + } + + public void testBooleanFalseEvent() { + Event e = support.event(source, false); + assertEquals("no", e.getId()); + assertSame(source, e.getSource()); + } + + public void testEvent() { + Event e = support.event(source, "no"); + assertEquals("no", e.getId()); + assertSame(source, e.getSource()); + } + + public void testEventWithAttrs() { + Event e = support.event(source, "no", "foo", "bar"); + assertEquals("no", e.getId()); + assertEquals("bar", e.getAttributes().get("foo")); + assertSame(source, e.getSource()); + } + +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/support/ExternalRedirectTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/support/ExternalRedirectTests.java new file mode 100644 index 00000000..eea82e86 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/support/ExternalRedirectTests.java @@ -0,0 +1,34 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.support; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link ExternalRedirect}. + */ +public class ExternalRedirectTests extends TestCase { + + private ExternalRedirect redirect; + + protected void setUp() throws Exception { + } + + public void testStaticExpression() { + redirect = new ExternalRedirect("my/url"); + assertEquals("my/url", redirect.getUrl()); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/support/FlowDefinitionRedirectTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/support/FlowDefinitionRedirectTests.java new file mode 100644 index 00000000..228f4aa6 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/support/FlowDefinitionRedirectTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.execution.support; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link FlowDefinitionRedirect}. + */ +public class FlowDefinitionRedirectTests extends TestCase { + + public void testConstructAndAccess() { + Map input = new HashMap(); + input.put("name", "value"); + FlowDefinitionRedirect redirect = new FlowDefinitionRedirect("foo", input); + assertEquals("foo", redirect.getFlowDefinitionId()); + assertEquals(1, redirect.getExecutionInput().size()); + assertEquals("value", redirect.getExecutionInput().get("name")); + try { + redirect.getExecutionInput().put("foo", "bar"); + } catch (UnsupportedOperationException e) { + + } + } + + public void testNullParams() { + try { + new FlowDefinitionRedirect(null, null); + fail("was null"); + } catch (IllegalArgumentException e) { + + } + + } + + public void testMapLookup() { + FlowDefinitionRedirect redirect = new FlowDefinitionRedirect("foo", null); + Map map = new HashMap(); + map.put("redirect", redirect); + assertSame(redirect, map.get("redirect")); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/ClientContinuationFlowExecutorIntegrationTests.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/ClientContinuationFlowExecutorIntegrationTests.java new file mode 100644 index 00000000..c3c10517 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/ClientContinuationFlowExecutorIntegrationTests.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor; + +public class ClientContinuationFlowExecutorIntegrationTests extends FlowExecutorIntegrationTests { + protected String[] getConfigLocations() { + return new String[] { "org/springframework/webflow/executor/context.xml", + "org/springframework/webflow/executor/repository-client.xml" }; + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/ContinuationFlowExecutorIntegrationTests.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/ContinuationFlowExecutorIntegrationTests.java new file mode 100644 index 00000000..f1c84db7 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/ContinuationFlowExecutorIntegrationTests.java @@ -0,0 +1,23 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor; + +public class ContinuationFlowExecutorIntegrationTests extends FlowExecutorIntegrationTests { + protected String[] getConfigLocations() { + return new String[] { "org/springframework/webflow/executor/context.xml", + "org/springframework/webflow/executor/repository-continuation.xml" }; + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/FlowExecutorIntegrationTests.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/FlowExecutorIntegrationTests.java new file mode 100644 index 00000000..a903d279 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/FlowExecutorIntegrationTests.java @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; +import org.springframework.test.AbstractDependencyInjectionSpringContextTests; +import org.springframework.webflow.context.ExternalContext; +import org.springframework.webflow.context.servlet.ServletExternalContext; +import org.springframework.webflow.definition.registry.NoSuchFlowDefinitionException; +import org.springframework.webflow.engine.NoMatchingTransitionException; +import org.springframework.webflow.execution.repository.NoSuchFlowExecutionException; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.test.MockExternalContext; + +public class FlowExecutorIntegrationTests extends AbstractDependencyInjectionSpringContextTests { + + private FlowExecutor flowExecutor; + + public void setFlowExecutor(FlowExecutor flowExecutor) { + this.flowExecutor = flowExecutor; + } + + protected String[] getConfigLocations() { + return new String[] { "org/springframework/webflow/executor/context.xml", "org/springframework/webflow/executor/repository-simple.xml" }; + } + + public void testConfigurationOk() { + assertNotNull(flowExecutor); + } + + public void testLaunchFlow() { + ExternalContext context = new ServletExternalContext(new MockServletContext(), new MockHttpServletRequest(), + new MockHttpServletResponse()); + ResponseInstruction response = flowExecutor.launch("flow", context); + assertTrue(response.getFlowExecutionContext().isActive()); + assertEquals("viewState1", response.getFlowExecutionContext().getActiveSession().getState().getId()); + assertTrue(response.isApplicationView()); + ApplicationView view = (ApplicationView)response.getViewSelection(); + assertEquals("view1", view.getViewName()); + assertEquals(0, view.getModel().size()); + } + + public void testLaunchNoSuchFlow() { + try { + ExternalContext context = new ServletExternalContext(new MockServletContext(), + new MockHttpServletRequest(), new MockHttpServletResponse()); + flowExecutor.launch("bogus", context); + fail("no such flow expected"); + } + catch (NoSuchFlowDefinitionException e) { + assertEquals("bogus", e.getFlowId()); + } + } + + public void testLaunchAndSignalEvent() { + ExternalContext context = new ServletExternalContext(new MockServletContext(), new MockHttpServletRequest(), + new MockHttpServletResponse()); + ResponseInstruction response = flowExecutor.launch("flow", context); + String key = response.getFlowExecutionKey(); + assertEquals("viewState1", response.getFlowExecutionContext().getActiveSession().getState().getId()); + response = flowExecutor.resume(key, "event1", context); + assertTrue(response.getFlowExecutionContext().isActive()); + assertEquals("viewState2", response.getFlowExecutionContext().getActiveSession().getState().getId()); + assertTrue(response.isApplicationView()); + assertNotNull(response.getFlowExecutionKey()); + ApplicationView view = (ApplicationView)response.getViewSelection(); + assertEquals("view2", view.getViewName()); + assertEquals(0, view.getModel().size()); + response = flowExecutor.resume(response.getFlowExecutionKey(), "event1", context); + view = (ApplicationView)response.getViewSelection(); + assertFalse(response.getFlowExecutionContext().isActive()); + assertTrue(response.isApplicationView()); + assertNull(response.getFlowExecutionKey()); + assertEquals("endView1", view.getViewName()); + assertEquals(0, view.getModel().size()); + try { + flowExecutor.resume(key, "event1", context); + fail("Should've been removed"); + } + catch (NoSuchFlowExecutionException e) { + + } + } + + public void testRefresh() { + ExternalContext context = new ServletExternalContext(new MockServletContext(), new MockHttpServletRequest(), + new MockHttpServletResponse()); + ResponseInstruction response = flowExecutor.launch("flow", context); + ResponseInstruction response2 = flowExecutor.refresh(response.getFlowExecutionKey(), context); + assertEquals(response, response2); + } + + public void testNoSuchFlowExecution() { + try { + flowExecutor.resume("_cbogus_kbogus", "bogus", new MockExternalContext()); + fail("Should've failed"); + } + catch (NoSuchFlowExecutionException e) { + assertEquals("_cbogus_kbogus", e.getFlowExecutionKey().toString()); + } + } + + public void testSignalEventNoMatchingTransition() { + ExternalContext context = new ServletExternalContext(new MockServletContext(), new MockHttpServletRequest(), + new MockHttpServletResponse()); + ResponseInstruction response = flowExecutor.launch("flow", context); + String key = response.getFlowExecutionKey(); + try { + flowExecutor.resume(key, "bogus", context); + fail("Should've been removed"); + } + catch (NoMatchingTransitionException e) { + assertEquals("flow", e.getFlowId()); + assertEquals("viewState1", e.getStateId()); + assertEquals("bogus", e.getEvent().getId()); + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/context.xml b/spring-webflow/src/test/java/org/springframework/webflow/executor/context.xml new file mode 100644 index 00000000..8bcc893a --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/context.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/flow.xml b/spring-webflow/src/test/java/org/springframework/webflow/executor/flow.xml new file mode 100644 index 00000000..e2a95d00 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/flow.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/FlowNavigationHandlerArgumentExtractorTests.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/FlowNavigationHandlerArgumentExtractorTests.java new file mode 100644 index 00000000..959c59be --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/FlowNavigationHandlerArgumentExtractorTests.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import junit.framework.TestCase; + +import org.springframework.webflow.executor.support.FlowExecutorArgumentExtractionException; + +public class FlowNavigationHandlerArgumentExtractorTests extends TestCase { + + private FlowNavigationHandlerArgumentExtractor extractor = new FlowNavigationHandlerArgumentExtractor(); + + public void testExtractFlowId() { + JsfExternalContext context = new JsfExternalContext(new MockFacesContext(), "action", "flowId:foo"); + String flowId = extractor.extractFlowId(context); + assertEquals("Wrong flow id", "foo", flowId); + } + + public void testExtractFlowIdWrongFormat() { + JsfExternalContext context = new JsfExternalContext(new MockFacesContext(), "action", "flow:foo"); + try { + extractor.extractFlowId(context); + fail(); + } + catch (FlowExecutorArgumentExtractionException e) { + // expected + } + } + + public void testExtractEventId() { + JsfExternalContext context = new JsfExternalContext(new MockFacesContext(), "action", "submit"); + String eventId = extractor.extractEventId(context); + assertEquals("Wrong event id", "submit", eventId); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/FlowPropertyResolverTests.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/FlowPropertyResolverTests.java new file mode 100644 index 00000000..8198024c --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/FlowPropertyResolverTests.java @@ -0,0 +1,136 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import javax.faces.el.EvaluationException; +import javax.faces.el.PropertyNotFoundException; +import javax.faces.el.PropertyResolver; +import javax.faces.el.ReferenceSyntaxException; + +import junit.framework.TestCase; + +import org.easymock.EasyMock; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.test.MockFlowSession; + +/** + * @author Colin Sampaleanu + * @since 1.0 + */ +public class FlowPropertyResolverTests extends TestCase { + + private FlowPropertyResolver resolver; + + private FlowExecution flowEx; + + protected void setUp() throws Exception { + resolver = new FlowPropertyResolver(new OriginalPropertyResolver()); + flowEx = (FlowExecution)EasyMock.createMock(FlowExecution.class); + } + + protected void tearDown() throws Exception { + resolver = null; + } + + public void testGetTypeBaseIndex() { + Class type = resolver.getType(flowEx, 22); + assertNull("can't get property from flow via index", type); + } + + public void testGetTypeBaseProperty() { + MockFlowSession flowSession = new MockFlowSession(); + flowSession.getScope().put("name", "joe"); + flowEx.getActiveSession(); + EasyMock.expectLastCall().andReturn(flowSession); + EasyMock.replay(new Object[] { flowEx }); + Class type = resolver.getType(flowEx, "name"); + assertTrue("returned type must match property type", type.equals(String.class)); + } + + public void testGetValueBaseIndex() { + try { + resolver.getValue(flowEx, 2); + fail("not legal to get flow property by index"); + } + catch (ReferenceSyntaxException e) { + // expected + } + } + + public void testGetValueBaseProperty() { + MockFlowSession flowSession = new MockFlowSession(); + flowSession.getScope().put("name", "joe"); + flowEx.getActiveSession(); + EasyMock.expectLastCall().andReturn(flowSession); + EasyMock.replay(new Object[] { flowEx }); + Object value = resolver.getValue(flowEx, "name"); + assertTrue("must return expected property", value.equals("joe")); + } + + public void testSetValueBaseIndex() { + try { + resolver.setValue(flowEx, 2, "whatever"); + fail("not legal to set flow property by index"); + } + catch (ReferenceSyntaxException e) { + // expected + } + } + + public void testSetValueBaseProperty() { + MockFlowSession flowSession = new MockFlowSession(); + flowEx.getActiveSession(); + EasyMock.expectLastCall().andReturn(flowSession); + EasyMock.replay(new Object[] { flowEx }); + resolver.setValue(flowEx, "name", "joe"); + assertTrue(flowSession.getScope().get("name").equals("joe")); + } + + private static class OriginalPropertyResolver extends PropertyResolver { + + public Class getType(Object base, int index) throws EvaluationException, PropertyNotFoundException { + return Object.class; + } + + public Class getType(Object base, Object property) throws EvaluationException, PropertyNotFoundException { + return Object.class; + } + + public Object getValue(Object base, int index) throws EvaluationException, PropertyNotFoundException { + return new String("Some value"); + } + + public Object getValue(Object base, Object property) throws EvaluationException, PropertyNotFoundException { + return new String("Some value"); + } + + public boolean isReadOnly(Object base, int index) throws EvaluationException, PropertyNotFoundException { + return false; + } + + public boolean isReadOnly(Object base, Object property) throws EvaluationException, PropertyNotFoundException { + return false; + } + + public void setValue(Object base, int index, Object value) throws EvaluationException, + PropertyNotFoundException { + } + + public void setValue(Object base, Object property, Object value) throws EvaluationException, + PropertyNotFoundException { + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/FlowVariableResolverTests.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/FlowVariableResolverTests.java new file mode 100644 index 00000000..abd4dbab --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/FlowVariableResolverTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import javax.faces.context.FacesContext; +import javax.faces.el.EvaluationException; +import javax.faces.el.VariableResolver; + +import junit.framework.TestCase; + +import org.easymock.EasyMock; +import org.springframework.webflow.execution.FlowExecution; +import org.springframework.webflow.execution.repository.FlowExecutionKey; + +/** + * Unit tests for the FlowVariableResolver class. + * + * @author Ulrik Sandberg + */ +public class FlowVariableResolverTests extends TestCase { + + private FlowVariableResolver tested; + + private TestableVariableResolver variableResolver; + + private MockFacesContext mockFacesContext; + + private MockJsfExternalContext mockJsfExternalContext; + + protected void setUp() throws Exception { + super.setUp(); + mockFacesContext = new MockFacesContext(); + mockJsfExternalContext = new MockJsfExternalContext(); + mockFacesContext.setExternalContext(mockJsfExternalContext); + variableResolver = new TestableVariableResolver(); + tested = new FlowVariableResolver(variableResolver); + } + + public void testResolveVariableNotFlowScope() { + Object result = tested.resolveVariable(mockFacesContext, "some name"); + assertTrue("not resolved using delegate", variableResolver.resolvedUsingDelegate); + assertSame(variableResolver.expected, result); + } + + public void testResolveVariableFlowScopeWithNoThreadLocal() { + try { + tested.resolveVariable(mockFacesContext, "flowScope"); + fail("EvaluationException expected"); + } + catch (EvaluationException expected) { + assertEquals( + "'flowScope' variable prefix specified, but a FlowExecution is not bound to current thread context as it should be", + expected.getMessage()); + } + assertFalse("resolved using delegate", variableResolver.resolvedUsingDelegate); + } + + public void testResolveVariableFlowScopeWithThreadLocal() { + FlowExecution flowExecutionMock = (FlowExecution)EasyMock.createMock(FlowExecution.class); + FlowExecutionKey key = null; + FlowExecutionHolder holder = new FlowExecutionHolder(key, flowExecutionMock); + FlowExecutionHolderUtils.setFlowExecutionHolder(holder, mockFacesContext); + EasyMock.replay(new Object[] { flowExecutionMock }); + + Object result = tested.resolveVariable(mockFacesContext, "flowScope"); + + EasyMock.verify(new Object[] { flowExecutionMock }); + assertFalse("resolved using delegate", variableResolver.resolvedUsingDelegate); + assertSame(flowExecutionMock, result); + } + + private static class TestableVariableResolver extends VariableResolver { + private boolean resolvedUsingDelegate; + + private Object expected = new Object(); + + public Object resolveVariable(FacesContext arg0, String arg1) throws EvaluationException { + resolvedUsingDelegate = true; + return expected; + } + } +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/MockApplication.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/MockApplication.java new file mode 100644 index 00000000..02e3449d --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/MockApplication.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.webflow.executor.jsf; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Locale; + +import javax.faces.FacesException; +import javax.faces.application.Application; +import javax.faces.application.NavigationHandler; +import javax.faces.application.StateManager; +import javax.faces.application.ViewHandler; +import javax.faces.component.UIComponent; +import javax.faces.context.FacesContext; +import javax.faces.convert.Converter; +import javax.faces.el.MethodBinding; +import javax.faces.el.PropertyResolver; +import javax.faces.el.ReferenceSyntaxException; +import javax.faces.el.ValueBinding; +import javax.faces.el.VariableResolver; +import javax.faces.event.ActionListener; +import javax.faces.validator.Validator; + +public class MockApplication extends Application { + private ViewHandler viewHandler; + + public ActionListener getActionListener() { + return null; + } + + public void setActionListener(ActionListener listener) { + } + + public Locale getDefaultLocale() { + return null; + } + + public void setDefaultLocale(Locale locale) { + } + + public String getDefaultRenderKitId() { + return null; + } + + public void setDefaultRenderKitId(String renderKitId) { + } + + public String getMessageBundle() { + return null; + } + + public void setMessageBundle(String bundle) { + } + + public NavigationHandler getNavigationHandler() { + return null; + } + + public void setNavigationHandler(NavigationHandler handler) { + } + + public PropertyResolver getPropertyResolver() { + return null; + } + + public void setPropertyResolver(PropertyResolver resolver) { + } + + public VariableResolver getVariableResolver() { + return null; + } + + public void setVariableResolver(VariableResolver resolver) { + } + + public ViewHandler getViewHandler() { + return viewHandler; + } + + public void setViewHandler(ViewHandler handler) { + viewHandler = handler; + } + + public StateManager getStateManager() { + return null; + } + + public void setStateManager(StateManager manager) { + } + + public void addComponent(String componentType, String componentClass) { + } + + public UIComponent createComponent(String componentType) throws FacesException { + return null; + } + + public UIComponent createComponent(ValueBinding componentBinding, FacesContext context, String componentType) + throws FacesException { + return null; + } + + public Iterator getComponentTypes() { + return null; + } + + public void addConverter(String converterId, String converterClass) { + } + + public void addConverter(Class targetClass, String converterClass) { + } + + public Converter createConverter(String converterId) { + return null; + } + + public Converter createConverter(Class targetClass) { + return null; + } + + public Iterator getConverterIds() { + return null; + } + + public Iterator getConverterTypes() { + return null; + } + + public MethodBinding createMethodBinding(String ref, Class[] params) throws ReferenceSyntaxException { + return null; + } + + public Iterator getSupportedLocales() { + return null; + } + + public void setSupportedLocales(Collection locales) { + } + + public void addValidator(String validatorId, String validatorClass) { + } + + public Validator createValidator(String validatorId) throws FacesException { + return null; + } + + public Iterator getValidatorIds() { + return null; + } + + public ValueBinding createValueBinding(String ref) throws ReferenceSyntaxException { + return null; + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/MockFacesContext.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/MockFacesContext.java new file mode 100644 index 00000000..5d63e367 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/MockFacesContext.java @@ -0,0 +1,138 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import java.util.Iterator; + +import javax.faces.application.Application; +import javax.faces.application.FacesMessage; +import javax.faces.application.FacesMessage.Severity; +import javax.faces.component.UIViewRoot; +import javax.faces.context.ExternalContext; +import javax.faces.context.FacesContext; +import javax.faces.context.ResponseStream; +import javax.faces.context.ResponseWriter; +import javax.faces.render.RenderKit; + +/** + * Mock implementation of the FacesContext class to facilitate + * standalone Action unit tests. + *

    + * NOT intended to be used for anything but standalone unit tests. This is a + * simple state holder, a stub implementation, at least if you follow Martin + * Fowler's reasoning. This class is called MockFacesContext to be + * consistent with the naming convention in the rest of the Spring framework + * (e.g. MockHttpServletRequest, ...). + * + * @see javax.faces.context.FacesContext + * + * @author Ulrik Sandberg + */ +public class MockFacesContext extends FacesContext { + private ExternalContext externalContext; + + private Application application; + + private UIViewRoot viewRoot; + + public Application getApplication() { + return application; + } + + /** + * Set the application to be used by this faces context. + * @param application the applicaiton to set. + */ + public void setApplication(Application application) { + this.application = application; + } + + public Iterator getClientIdsWithMessages() { + return null; + } + + public ExternalContext getExternalContext() { + return externalContext; + } + + /** + * Set the external context of this faces context. + * @param externalContext the external context to set. + */ + public void setExternalContext(ExternalContext externalContext) { + this.externalContext = externalContext; + } + + public Severity getMaximumSeverity() { + return null; + } + + public Iterator getMessages() { + return null; + } + + public Iterator getMessages(String arg0) { + return null; + } + + public RenderKit getRenderKit() { + return null; + } + + public boolean getRenderResponse() { + return false; + } + + public boolean getResponseComplete() { + return false; + } + + public ResponseStream getResponseStream() { + return null; + } + + public void setResponseStream(ResponseStream arg0) { + + } + + public ResponseWriter getResponseWriter() { + return null; + } + + public void setResponseWriter(ResponseWriter arg0) { + } + + public UIViewRoot getViewRoot() { + return viewRoot; + } + + public void setViewRoot(UIViewRoot viewRoot) { + this.viewRoot = viewRoot; + } + + public void addMessage(String arg0, FacesMessage arg1) { + } + + public void release() { + } + + public void renderResponse() { + } + + public void responseComplete() { + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/MockJsfExternalContext.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/MockJsfExternalContext.java new file mode 100644 index 00000000..822d481a --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/MockJsfExternalContext.java @@ -0,0 +1,190 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.webflow.executor.jsf; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.Principal; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import javax.faces.context.ExternalContext; + +public class MockJsfExternalContext extends ExternalContext { + + private Map applicationMap = new HashMap(); + + private Map sessionMap = new HashMap(); + + private Map requestMap = new HashMap(); + + private Map requestParameterMap = Collections.EMPTY_MAP; + + public void dispatch(String arg0) throws IOException { + } + + public String encodeActionURL(String arg0) { + return null; + } + + public String encodeNamespace(String arg0) { + return null; + } + + public String encodeResourceURL(String arg0) { + return null; + } + + public Map getApplicationMap() { + return applicationMap; + } + + public String getAuthType() { + return null; + } + + public Object getContext() { + return null; + } + + public String getInitParameter(String arg0) { + return null; + } + + public Map getInitParameterMap() { + return null; + } + + public String getRemoteUser() { + return null; + } + + public Object getRequest() { + return null; + } + + public String getRequestContextPath() { + return null; + } + + public Map getRequestCookieMap() { + return null; + } + + public Map getRequestHeaderMap() { + return null; + } + + public Map getRequestHeaderValuesMap() { + return null; + } + + public Locale getRequestLocale() { + return null; + } + + public Iterator getRequestLocales() { + return null; + } + + public Map getRequestMap() { + return requestMap; + } + + /** + * Set the request map for this external context. + * @param requestMap The requestMap to set. + */ + public void setRequestMap(Map requestMap) { + this.requestMap = requestMap; + } + + public Map getRequestParameterMap() { + return requestParameterMap; + } + + /** + * Set the request parameter map for this external context. + * @param requestParameterMap the request parameter map to set. + */ + public void setRequestParameterMap(Map requestParameterMap) { + this.requestParameterMap = requestParameterMap; + } + + public Iterator getRequestParameterNames() { + return requestParameterMap.keySet().iterator(); + } + + public Map getRequestParameterValuesMap() { + return null; + } + + public String getRequestPathInfo() { + return null; + } + + public String getRequestServletPath() { + return null; + } + + public URL getResource(String arg0) throws MalformedURLException { + return null; + } + + public InputStream getResourceAsStream(String arg0) { + return null; + } + + public Set getResourcePaths(String arg0) { + return null; + } + + public Object getResponse() { + return null; + } + + public Object getSession(boolean arg0) { + return null; + } + + public Map getSessionMap() { + return sessionMap; + } + + public Principal getUserPrincipal() { + return null; + } + + public boolean isUserInRole(String arg0) { + return false; + } + + public void log(String arg0) { + } + + public void log(String arg0, Throwable arg1) { + } + + public void redirect(String arg0) throws IOException { + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/MockViewHandler.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/MockViewHandler.java new file mode 100644 index 00000000..69eff044 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/jsf/MockViewHandler.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.jsf; + +import java.io.IOException; +import java.util.Locale; + +import javax.faces.FacesException; +import javax.faces.application.ViewHandler; +import javax.faces.component.UIViewRoot; +import javax.faces.context.FacesContext; + +public class MockViewHandler extends ViewHandler { + private UIViewRoot viewRoot; + + public Locale calculateLocale(FacesContext context) { + return null; + } + + public String calculateRenderKitId(FacesContext context) { + return null; + } + + public UIViewRoot createView(FacesContext context, String viewId) { + return viewRoot; + } + + /** + * Set the view root that this mpck is supposed to create. + * @param viewRoot the view to set. + */ + public void setCreateView(UIViewRoot viewRoot) { + this.viewRoot = viewRoot; + } + + public String getActionURL(FacesContext context, String viewId) { + return null; + } + + public String getResourceURL(FacesContext context, String path) { + return null; + } + + public void renderView(FacesContext context, UIViewRoot viewToRender) throws IOException, FacesException { + } + + public UIViewRoot restoreView(FacesContext context, String viewId) { + return null; + } + + public void writeState(FacesContext context) throws IOException { + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/mvc/FlowControllerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/mvc/FlowControllerTests.java new file mode 100644 index 00000000..f8716bac --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/mvc/FlowControllerTests.java @@ -0,0 +1,77 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.mvc; + +import junit.framework.TestCase; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.servlet.ModelAndView; +import org.springframework.web.servlet.view.RedirectView; +import org.springframework.webflow.conversation.impl.SessionBindingConversationManager; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistryImpl; +import org.springframework.webflow.definition.registry.StaticFlowDefinitionHolder; +import org.springframework.webflow.engine.SimpleFlow; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.engine.impl.FlowExecutionImplStateRestorer; +import org.springframework.webflow.execution.repository.FlowExecutionRepository; +import org.springframework.webflow.execution.repository.support.SimpleFlowExecutionRepository; +import org.springframework.webflow.executor.FlowExecutorImpl; + +/** + * Unit tests for {@link FlowController}. + */ +public class FlowControllerTests extends TestCase { + + private FlowController controller = new FlowController(); + + public void setUp() { + controller.setServletContext(new MockServletContext()); + + FlowDefinitionRegistryImpl registry = new FlowDefinitionRegistryImpl(); + registry.registerFlowDefinition(new StaticFlowDefinitionHolder(new SimpleFlow())); + FlowExecutionImplFactory factory = new FlowExecutionImplFactory(); + FlowExecutionRepository repository = new SimpleFlowExecutionRepository(new FlowExecutionImplStateRestorer( + registry), new SessionBindingConversationManager()); + controller.setFlowExecutor(new FlowExecutorImpl(registry, factory, repository)); + } + + public void testLaunch() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + request.addParameter("_flowId", "simpleFlow"); + ModelAndView mv = controller.handleRequestInternal(request, response); + assertEquals("view", mv.getViewName()); + } + + public void testResume() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setContextPath("/app"); + MockHttpServletResponse response = new MockHttpServletResponse(); + request.addParameter("_flowId", "simpleFlow"); + ModelAndView mv = controller.handleRequestInternal(request, response); + request.addParameter("_flowExecutionKey", (String)mv.getModel().get("flowExecutionKey")); + request.addParameter("_eventId", "submit"); + mv = controller.handleRequest(request, response); + assertNull(mv.getViewName()); + assertTrue(mv.getView() instanceof RedirectView); + RedirectView rv = (RedirectView)mv.getView(); + assertEquals("confirm", rv.getUrl()); + assertNull(mv.getModel().get("flowExecutionKey")); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/mvc/PortletFlowControllerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/mvc/PortletFlowControllerTests.java new file mode 100644 index 00000000..b5532abc --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/mvc/PortletFlowControllerTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.mvc; + +import junit.framework.TestCase; + +import org.springframework.mock.web.portlet.MockActionRequest; +import org.springframework.mock.web.portlet.MockActionResponse; +import org.springframework.mock.web.portlet.MockPortletContext; +import org.springframework.mock.web.portlet.MockRenderRequest; +import org.springframework.mock.web.portlet.MockRenderResponse; +import org.springframework.web.portlet.ModelAndView; +import org.springframework.webflow.conversation.impl.SessionBindingConversationManager; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistryImpl; +import org.springframework.webflow.definition.registry.StaticFlowDefinitionHolder; +import org.springframework.webflow.engine.SimpleFlow; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.engine.impl.FlowExecutionImplStateRestorer; +import org.springframework.webflow.execution.repository.FlowExecutionRepository; +import org.springframework.webflow.execution.repository.support.SimpleFlowExecutionRepository; +import org.springframework.webflow.executor.FlowExecutorImpl; + +/** + * Unit tests for {@link PortletFlowController}. + */ +public class PortletFlowControllerTests extends TestCase { + + private PortletFlowController controller = new PortletFlowController(); + + public void setUp() { + controller.setPortletContext(new MockPortletContext()); + + FlowDefinitionRegistryImpl registry = new FlowDefinitionRegistryImpl(); + registry.registerFlowDefinition(new StaticFlowDefinitionHolder(new SimpleFlow())); + FlowExecutionImplFactory factory = new FlowExecutionImplFactory(); + FlowExecutionRepository repository = new SimpleFlowExecutionRepository(new FlowExecutionImplStateRestorer( + registry), new SessionBindingConversationManager()); + controller.setFlowExecutor(new FlowExecutorImpl(registry, factory, repository)); + } + + public void testLaunch() throws Exception { + MockRenderRequest request = new MockRenderRequest(); + MockRenderResponse response = new MockRenderResponse(); + request.addParameter("_flowId", "simpleFlow"); + ModelAndView mv = controller.handleRenderRequest(request, response); + assertEquals("view", mv.getViewName()); + } + + public void testResume() throws Exception { + MockRenderRequest renderRequest = new MockRenderRequest(); + MockRenderResponse renderResponse = new MockRenderResponse(); + renderRequest.addParameter("_flowId", "simpleFlow"); + ModelAndView mv = controller.handleRenderRequest(renderRequest, renderResponse); + assertEquals("view", mv.getViewName()); + assertNotNull(mv.getModel().get("flowExecutionKey")); + + MockActionRequest actionRequest = new MockActionRequest(); + actionRequest.setSession(renderRequest.getPortletSession()); + actionRequest.setContextPath("/app"); + MockActionResponse actionResponse = new MockActionResponse(); + actionRequest.addParameter("_flowExecutionKey", (String)mv.getModel().get("flowExecutionKey")); + actionRequest.addParameter("_eventId", "submit"); + try { + controller.handleActionRequest(actionRequest, actionResponse); + } + catch (IllegalArgumentException e) { + + } + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/repository-client.xml b/spring-webflow/src/test/java/org/springframework/webflow/executor/repository-client.xml new file mode 100644 index 00000000..4b0301ae --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/repository-client.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/repository-continuation.xml b/spring-webflow/src/test/java/org/springframework/webflow/executor/repository-continuation.xml new file mode 100644 index 00000000..ce0784bf --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/repository-continuation.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/repository-simple.xml b/spring-webflow/src/test/java/org/springframework/webflow/executor/repository-simple.xml new file mode 100644 index 00000000..03682ecc --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/repository-simple.xml @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/struts/FlowActionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/struts/FlowActionTests.java new file mode 100644 index 00000000..a4cf8c01 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/struts/FlowActionTests.java @@ -0,0 +1,85 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.struts; + +import junit.framework.TestCase; + +import org.apache.struts.action.ActionForm; +import org.apache.struts.action.ActionForward; +import org.apache.struts.action.ActionMapping; +import org.apache.struts.action.ActionServlet; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.mock.web.MockServletContext; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.StaticWebApplicationContext; +import org.springframework.web.struts.SpringBindingActionForm; +import org.springframework.webflow.conversation.impl.SessionBindingConversationManager; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistryImpl; +import org.springframework.webflow.definition.registry.StaticFlowDefinitionHolder; +import org.springframework.webflow.engine.SimpleFlow; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.engine.impl.FlowExecutionImplStateRestorer; +import org.springframework.webflow.execution.repository.FlowExecutionRepository; +import org.springframework.webflow.execution.repository.support.SimpleFlowExecutionRepository; +import org.springframework.webflow.executor.FlowExecutorImpl; + +/** + * Unit tests for {@link FlowAction}. + */ +public class FlowActionTests extends TestCase { + + private FlowAction action; + + public void setUp() { + action = new FlowAction() { + protected WebApplicationContext initWebApplicationContext(ActionServlet actionServlet) + throws IllegalStateException { + StaticWebApplicationContext context = new StaticWebApplicationContext(); + context.setServletContext(new MockServletContext()); + return context; + } + }; + + FlowDefinitionRegistryImpl registry = new FlowDefinitionRegistryImpl(); + registry.registerFlowDefinition(new StaticFlowDefinitionHolder(new SimpleFlow())); + FlowExecutionImplFactory factory = new FlowExecutionImplFactory(); + FlowExecutionRepository repository = new SimpleFlowExecutionRepository(new FlowExecutionImplStateRestorer( + registry), new SessionBindingConversationManager()); + action.setFlowExecutor(new FlowExecutorImpl(registry, factory, repository)); + + action.setServlet(new ActionServlet()); + } + + public void testLaunch() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + request.addParameter("_flowId", "simpleFlow"); + ActionMapping mapping = new ActionMapping(); + mapping.addForwardConfig(new ActionForward("view", "/view.jsp", false)); + ActionForm form = new SpringBindingActionForm(); + ActionForward forward = action.execute(mapping, form, request, response); + assertEquals("view", forward.getName()); + } + + public void testResume() throws Exception { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setMethod("POST"); + request.setContextPath("/app"); + new MockHttpServletResponse(); + request.addParameter("_flowId", "simpleFlow"); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/support/FlowRequestHandlerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/support/FlowRequestHandlerTests.java new file mode 100644 index 00000000..df0df272 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/support/FlowRequestHandlerTests.java @@ -0,0 +1,99 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.support; + +import junit.framework.TestCase; + +import org.springframework.webflow.conversation.impl.SessionBindingConversationManager; +import org.springframework.webflow.definition.registry.FlowDefinitionRegistryImpl; +import org.springframework.webflow.definition.registry.StaticFlowDefinitionHolder; +import org.springframework.webflow.engine.EndState; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.engine.TargetStateResolver; +import org.springframework.webflow.engine.Transition; +import org.springframework.webflow.engine.ViewState; +import org.springframework.webflow.engine.impl.FlowExecutionImplFactory; +import org.springframework.webflow.engine.impl.FlowExecutionImplStateRestorer; +import org.springframework.webflow.engine.support.DefaultTargetStateResolver; +import org.springframework.webflow.execution.repository.support.SimpleFlowExecutionRepository; +import org.springframework.webflow.executor.FlowExecutorImpl; +import org.springframework.webflow.executor.ResponseInstruction; +import org.springframework.webflow.test.MockExternalContext; + +/** + * Unit tests for {@link FlowRequestHandler}. + */ +public class FlowRequestHandlerTests extends TestCase { + + private FlowRequestHandler handler; + + private MockExternalContext context = new MockExternalContext(); + + protected void setUp() throws Exception { + FlowDefinitionRegistryImpl registry = new FlowDefinitionRegistryImpl(); + Flow flow = new Flow("flow"); + ViewState view = new ViewState(flow, "view"); + view.getTransitionSet().add(new Transition(to("end"))); + new EndState(flow, "end"); + registry.registerFlowDefinition(new StaticFlowDefinitionHolder(flow)); + FlowExecutorImpl executor = new FlowExecutorImpl(registry, new FlowExecutionImplFactory(), + new SimpleFlowExecutionRepository(new FlowExecutionImplStateRestorer(registry), + new SessionBindingConversationManager())); + handler = new FlowRequestHandler(executor); + } + + public void testLaunch() { + context.putRequestParameter("_flowId", "flow"); + ResponseInstruction response = handler.handleFlowRequest(context); + assertTrue(response.isNull()); + assertTrue(response.getFlowExecutionContext().isActive()); + assertEquals("flow", response.getFlowExecutionContext().getDefinition().getId()); + assertEquals("view", response.getFlowExecutionContext().getActiveSession().getState().getId()); + } + + public void testResumeOnEvent() { + context.putRequestParameter("_flowId", "flow"); + ResponseInstruction response = handler.handleFlowRequest(context); + + String flowExecutionKey = response.getFlowExecutionKey(); + context.putRequestParameter("_flowExecutionKey", flowExecutionKey); + context.putRequestParameter("_eventId", "submit"); + response = handler.handleFlowRequest(context); + + assertTrue(response.isNull()); + assertTrue(!response.getFlowExecutionContext().isActive()); + assertEquals("flow", response.getFlowExecutionContext().getDefinition().getId()); + + } + + public void testRefreshFlowExecution() { + context.putRequestParameter("_flowId", "flow"); + ResponseInstruction response = handler.handleFlowRequest(context); + + String flowExecutionKey = response.getFlowExecutionKey(); + context.putRequestParameter("_flowExecutionKey", flowExecutionKey); + response = handler.handleFlowRequest(context); + + assertTrue(response.isNull()); + assertTrue(response.getFlowExecutionContext().isActive()); + assertEquals("flow", response.getFlowExecutionContext().getDefinition().getId()); + assertEquals("view", response.getFlowExecutionContext().getActiveSession().getState().getId()); + } + + protected TargetStateResolver to(String stateId) { + return new DefaultTargetStateResolver(stateId); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/support/RequestParameterFlowExecutorArgumentHandlerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/support/RequestParameterFlowExecutorArgumentHandlerTests.java new file mode 100644 index 00000000..ac720750 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/support/RequestParameterFlowExecutorArgumentHandlerTests.java @@ -0,0 +1,216 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.support; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.springframework.webflow.execution.FlowExecutionContext; +import org.springframework.webflow.execution.support.ExternalRedirect; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; +import org.springframework.webflow.test.MockExternalContext; +import org.springframework.webflow.test.MockFlowExecutionContext; + +/** + * Unit tests for {@link RequestParameterFlowExecutorArgumentHandler}. + */ +public class RequestParameterFlowExecutorArgumentHandlerTests extends TestCase { + + private MockExternalContext context; + + private FlowExecutorArgumentHandler argumentHandler; + + private String flowExecutionKey; + + public void setUp() { + context = new MockExternalContext(); + argumentHandler = new RequestParameterFlowExecutorArgumentHandler(); + flowExecutionKey = "_c12345_k12345"; + } + + public void testExtractFlowId() { + context.putRequestParameter("_flowId", "flow"); + assertEquals("flow", argumentHandler.extractFlowId(context)); + } + + public void testExtractFlowIdDefault() { + argumentHandler.setDefaultFlowId("flow"); + assertEquals("flow", argumentHandler.extractFlowId(new MockExternalContext())); + } + + public void testExtractFlowIdNoIdProvided() { + try { + argumentHandler.extractFlowId(context); + fail("no flow id provided"); + } + catch (FlowExecutorArgumentExtractionException e) { + + } + } + + public void testExtractFlowExecutionKey() { + context.putRequestParameter("_flowExecutionKey", "_c12345_k12345"); + assertEquals(flowExecutionKey, argumentHandler.extractFlowExecutionKey(context)); + } + + public void testExtractFlowExecutionNoKeyProvided() { + try { + argumentHandler.extractFlowExecutionKey(context); + fail("no flow execution key provided"); + } + catch (FlowExecutorArgumentExtractionException e) { + + } + } + + public void testExtractEventId() { + context.putRequestParameter("_eventId", "submit"); + assertEquals("submit", argumentHandler.extractEventId(context)); + } + + public void testExtractEventIdButtonNameFormat() { + context.putRequestParameter("_eventId_submit", "not important"); + context.putRequestParameter("_somethingElse", "not important"); + assertEquals("submit", argumentHandler.extractEventId(context)); + } + + public void testExtractEventIdNoIdProvided() { + try { + argumentHandler.extractEventId(context); + fail("no event id provided"); + } + catch (FlowExecutorArgumentExtractionException e) { + + } + } + + public void testCreateFlowUrl() { + /* + * Scenario: + * Context root: /app + * Dispatcher mapping in web.xml: *.htm + * Controller mapping: /flows.htm + * So full request URI will be + * /app/flows.htm + */ + context.setContextPath("/app"); + context.setDispatcherPath("/flows.htm"); + FlowDefinitionRedirect redirect = new FlowDefinitionRedirect("flow", null); + String url = argumentHandler.createFlowDefinitionUrl(redirect, context); + assertEquals("/app/flows.htm?_flowId=flow", url); + } + + public void testCreateFlowUrlRequestPath() { + /* + * Scenario: + * Context root: /app + * Dispatcher mapping in web.xml: /system/* + * Controller mapping: /flows.htm + * So full request URI will be + * /app/system/flows.htm + */ + context.setContextPath("/app"); + context.setDispatcherPath("/system"); + context.setRequestPathInfo("/flows.htm"); + FlowDefinitionRedirect redirect = new FlowDefinitionRedirect("flow", null); + String url = argumentHandler.createFlowDefinitionUrl(redirect, context); + assertEquals("/app/system/flows.htm?_flowId=flow", url); + } + + public void testCreateFlowUrlWithInput() { + context.setContextPath("/app"); + context.setDispatcherPath("/flows.htm"); + Map input = new HashMap(); + input.put("foo", "bar"); + input.put("baz", new Integer(3)); + FlowDefinitionRedirect redirect = new FlowDefinitionRedirect("flow", input); + String url = argumentHandler.createFlowDefinitionUrl(redirect, context); + assertEquals("/app/flows.htm?_flowId=flow&foo=bar&baz=3", url); + } + + public void testCreateFlowExecutionUrl() { + /* + * Scenario: + * Context root: /app + * Dispatcher mapping in web.xml: *.htm + * Controller mapping: /flows.htm + * So full request URI will be + * /app/flows.htm + */ + context.setContextPath("/app"); + context.setDispatcherPath("/flows.htm"); + FlowExecutionContext flowExecution = new MockFlowExecutionContext(); + String url = argumentHandler.createFlowExecutionUrl(flowExecutionKey, flowExecution, context); + assertEquals("/app/flows.htm?_flowExecutionKey=_c12345_k12345", url); + } + + public void testCreateFlowExecutionUrlRequestPath() { + /* + * Scenario: + * Context root: /app + * Dispatcher mapping in web.xml: /system/* + * Controller mapping: /flows.htm + * So full request URI will be + * /app/system/flows.htm + */ + context.setContextPath("/app"); + context.setDispatcherPath("/system"); + context.setRequestPathInfo("/flows.htm"); + FlowExecutionContext flowExecution = new MockFlowExecutionContext(); + String url = argumentHandler.createFlowExecutionUrl(flowExecutionKey, flowExecution, context); + assertEquals("/app/system/flows.htm?_flowExecutionKey=_c12345_k12345", url); + } + + public void testCreateExternalUrlAbsolute() { + context.setContextPath("/app"); + context.setDispatcherPath("/flows.htm"); + ExternalRedirect redirect = new ExternalRedirect("/a/url"); + argumentHandler.setRedirectContextRelative(false); + String url = argumentHandler.createExternalUrl(redirect, flowExecutionKey, context); + assertEquals("/a/url?_flowExecutionKey=_c12345_k12345", url); + } + + public void testCreateExternalUrlContextRelative() { + context.setContextPath("/app"); + context.setDispatcherPath("/flows.htm"); + ExternalRedirect redirect = new ExternalRedirect("/a/url"); + String url = argumentHandler.createExternalUrl(redirect, flowExecutionKey, context); + assertEquals("/app/a/url?_flowExecutionKey=_c12345_k12345", url); + } + + public void testCreateExternalUrlNoKey() { + context.setContextPath("/app"); + context.setDispatcherPath("/flows"); + ExternalRedirect redirect = new ExternalRedirect("/a/url"); + String url = argumentHandler.createExternalUrl(redirect, null, context); + assertEquals("/app/a/url", url); + } + + public void testCreateExternalUrlNoKeyRelativeUrl() { + context.setContextPath("/app"); + context.setDispatcherPath("/flows"); + ExternalRedirect redirect = new ExternalRedirect("a/url"); + String url = argumentHandler.createExternalUrl(redirect, null, context); + assertEquals("a/url", url); + } + + public void testAccidentalParameterArraySubmit() { + context.putRequestParameter("_flowExecutionKey", new String[] { "_c12345_k12345", "_c12345_k12345" }); + assertEquals(flowExecutionKey, argumentHandler.extractFlowExecutionKey(context)); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/executor/support/RequestPathFlowExecutorArgumentHandlerTests.java b/spring-webflow/src/test/java/org/springframework/webflow/executor/support/RequestPathFlowExecutorArgumentHandlerTests.java new file mode 100644 index 00000000..fa5ef9ab --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/executor/support/RequestPathFlowExecutorArgumentHandlerTests.java @@ -0,0 +1,106 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.executor.support; + +import java.util.HashMap; +import java.util.Map; + +import junit.framework.TestCase; + +import org.springframework.webflow.execution.FlowExecutionContext; +import org.springframework.webflow.execution.support.FlowDefinitionRedirect; +import org.springframework.webflow.test.MockExternalContext; +import org.springframework.webflow.test.MockFlowExecutionContext; + +/** + * Unit tests for {@link RequestPathFlowExecutorArgumentHandler}. + */ +public class RequestPathFlowExecutorArgumentHandlerTests extends TestCase { + + private MockExternalContext context = new MockExternalContext(); + + private RequestPathFlowExecutorArgumentHandler argumentHandler; + + private String flowExecutionKey; + + public void setUp() { + argumentHandler = new RequestPathFlowExecutorArgumentHandler(); + flowExecutionKey = "_c12345_k12345"; + } + + public void testExtractFlowId() { + MockExternalContext context = new MockExternalContext(); + context.setRequestPathInfo("flow"); + assertEquals("flow", argumentHandler.extractFlowId(context)); + } + + public void testExtractFlowIdDefault() { + argumentHandler.setDefaultFlowId("flow"); + assertEquals("flow", argumentHandler.extractFlowId(new MockExternalContext())); + } + + public void testExtractFlowIdNoRequestPath() { + try { + argumentHandler.extractFlowId(new MockExternalContext()); + fail("should've failed"); + } + catch (FlowExecutorArgumentExtractionException e) { + } + } + + public void testCreateFlowUrl() { + context.setContextPath("/app"); + context.setDispatcherPath("/flows"); + FlowDefinitionRedirect redirect = new FlowDefinitionRedirect("flow", null); + String url = argumentHandler.createFlowDefinitionUrl(redirect, context); + assertEquals("/app/flows/flow", url); + } + + public void testCreateFlowUrlInput() { + context.setContextPath("/app"); + context.setDispatcherPath("/flows"); + Map input = new HashMap(); + input.put("foo", "bar"); + input.put("baz", new Integer(3)); + FlowDefinitionRedirect redirect = new FlowDefinitionRedirect("flow", input); + String url = argumentHandler.createFlowDefinitionUrl(redirect, context); + assertEquals("/app/flows/flow?foo=bar&baz=3", url); + } + + public void testCreateFlowExecutionUrl() { + context.setContextPath("/app"); + context.setDispatcherPath("/flows"); + FlowExecutionContext flowExecution = new MockFlowExecutionContext(); + String url = argumentHandler.createFlowExecutionUrl(flowExecutionKey, flowExecution, context); + assertEquals("/app/flows/k/_c12345_k12345", url); + } + + public void testIsFlowExecutionKeyPresent() { + context.setContextPath("/app"); + context.setDispatcherPath("/flows"); + context.setRequestPathInfo("/k/_c12345_k12345"); + assertTrue(argumentHandler.isFlowExecutionKeyPresent(context)); + context.setRequestPathInfo("/sellitem"); + assertFalse(argumentHandler.isFlowExecutionKeyPresent(context)); + } + + public void testExtractFlowExecutionKey() { + context.setContextPath("/app"); + context.setDispatcherPath("/flows"); + context.setRequestPathInfo("/k/_c12345_k12345"); + assertEquals("_c12345_k12345", argumentHandler.extractFlowExecutionKey(context)); + } +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/test/SearchFlowExecutionTests.java b/spring-webflow/src/test/java/org/springframework/webflow/test/SearchFlowExecutionTests.java new file mode 100644 index 00000000..7298acc1 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/test/SearchFlowExecutionTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.test; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.binding.mapping.AttributeMapper; +import org.springframework.binding.mapping.MappingContext; +import org.springframework.core.io.ClassPathResource; +import org.springframework.webflow.core.collection.AttributeMap; +import org.springframework.webflow.definition.registry.FlowDefinitionResource; +import org.springframework.webflow.engine.EndState; +import org.springframework.webflow.engine.Flow; +import org.springframework.webflow.execution.support.ApplicationView; +import org.springframework.webflow.test.execution.AbstractXmlFlowExecutionTests; + +/** + * Sample {@link AbstractXmlFlowExecutionTests} subclass. + */ +public class SearchFlowExecutionTests extends AbstractXmlFlowExecutionTests { + + public void testStartFlow() { + ApplicationView view = applicationView(startFlow()); + assertCurrentStateEquals("enterCriteria"); + assertViewNameEquals("searchCriteria", view); + assertModelAttributeNotNull("searchCriteria", view); + } + + public void testCriteriaSubmitSuccess() { + startFlow(); + MockParameterMap parameters = new MockParameterMap(); + parameters.put("firstName", "Keith"); + parameters.put("lastName", "Donald"); + ApplicationView view = applicationView(signalEvent("search", parameters)); + assertCurrentStateEquals("displayResults"); + assertViewNameEquals("searchResults", view); + assertModelAttributeCollectionSize(1, "results", view); + } + + public void testNewSearch() { + testCriteriaSubmitSuccess(); + ApplicationView view = applicationView(signalEvent("newSearch")); + assertCurrentStateEquals("enterCriteria"); + assertViewNameEquals("searchCriteria", view); + } + + public void testSelectValidResult() { + testCriteriaSubmitSuccess(); + MockParameterMap parameters = new MockParameterMap(); + parameters.put("id", "1"); + ApplicationView view = applicationView(signalEvent("select", parameters)); + assertCurrentStateEquals("displayResults"); + assertViewNameEquals("searchResults", view); + assertModelAttributeCollectionSize(1, "results", view); + } + + protected FlowDefinitionResource getFlowDefinitionResource() { + return new FlowDefinitionResource("search-flow", + new ClassPathResource("search-flow.xml", SearchFlowExecutionTests.class)); + } + + protected void registerMockServices(MockFlowServiceLocator serviceRegistry) { + Flow mockDetailFlow = new Flow("detail-flow"); + mockDetailFlow.setInputMapper(new AttributeMapper() { + public void map(Object source, Object target, MappingContext context) { + assertEquals("id of value 1 not provided as input by calling search flow", new Long(1), ((AttributeMap)source).get("id")); + } + }); + // test responding to finish result + new EndState(mockDetailFlow, "finish"); + + serviceRegistry.registerSubflow(mockDetailFlow); + serviceRegistry.registerBean("phonebook", new TestPhoneBook()); + } + + public static class TestPhoneBook { + + public List search(Object criteria) { + ArrayList res = new ArrayList(); + res.add(new Object()); + return res; + } + + public Object getPerson(Long id) { + return new Object(); + } + + public Object getPerson(String userId) { + return new Object(); + } + + } + +} \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/test/search-flow-beans.xml b/spring-webflow/src/test/java/org/springframework/webflow/test/search-flow-beans.xml new file mode 100644 index 00000000..b0d8710f --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/test/search-flow-beans.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/test/search-flow.xml b/spring-webflow/src/test/java/org/springframework/webflow/test/search-flow.xml new file mode 100644 index 00000000..9a7559a2 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/test/search-flow.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/util/DispatchMethodInvokerTest.java b/spring-webflow/src/test/java/org/springframework/webflow/util/DispatchMethodInvokerTest.java new file mode 100644 index 00000000..d96ac1ee --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/util/DispatchMethodInvokerTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2006 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.webflow.util; + +import junit.framework.TestCase; + +/** + * Unit tests for {@link DispatchMethodInvoker}. + * + * @author Ben Hale + */ +public class DispatchMethodInvokerTest extends TestCase { + + private class MockClass { + private boolean methodCalled = false; + + public boolean getMethodCalled() { + return methodCalled; + } + + public void argumentMethod(Object o) { + methodCalled = true; + } + + public void noArgumentMethod() { + methodCalled = true; + } + + public void exceptionMethod(Object o) throws Exception { + throw new Exception("expected exception"); + } + } + + private MockClass mockClass; + + protected void setUp() { + mockClass = new MockClass(); + } + + public void testInvokeWithExplicitParameters() throws Exception { + DispatchMethodInvoker invoker = new DispatchMethodInvoker(mockClass, new Class[] { Object.class }); + invoker.invoke("argumentMethod", new Object[] { "testValue" }); + assertTrue("Method should have been called successfully", mockClass.getMethodCalled()); + } + + public void testInvokeWithAssignableParameters() throws Exception { + DispatchMethodInvoker invoker = new DispatchMethodInvoker(mockClass, new Class[] { String.class }); + invoker.invoke("argumentMethod", new Object[] { "testValue" }); + assertTrue("Method should have been called successfully", mockClass.getMethodCalled()); + } + + public void testInvokeWithNoParameters() throws Exception { + DispatchMethodInvoker invoker = new DispatchMethodInvoker(mockClass, new Class[0]); + invoker.invoke("noArgumentMethod", new Object[0]); + assertTrue("Method should have been called successfully", mockClass.getMethodCalled()); + } + + public void testInvokeWithException() { + DispatchMethodInvoker invoker = new DispatchMethodInvoker(mockClass, new Class[] { Object.class }); + try { + invoker.invoke("exceptionMethod", new Object[] { "testValue" }); + fail("Should have thrown an exception"); + } + catch (Exception e) { + } + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/util/ReflectionUtilsTests.java b/spring-webflow/src/test/java/org/springframework/webflow/util/ReflectionUtilsTests.java new file mode 100644 index 00000000..3e8af00c --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/util/ReflectionUtilsTests.java @@ -0,0 +1,72 @@ +package org.springframework.webflow.util; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.Date; + +import junit.framework.TestCase; + +/** + * Test case for {@link org.springframework.webflow.util.ReflectionUtils}. + * + * @author Erwin Vervaet + */ +public class ReflectionUtilsTests extends TestCase { + + public void testInvokeStatic() throws Exception { + Method currentTimeMillis = System.class.getMethod("currentTimeMillis", null); + Object res = ReflectionUtils.invokeMethod(currentTimeMillis, null); + assertNotNull(res); + assertTrue(res instanceof Long); + } + + public void testInvoke() throws Exception { + Method substring = String.class.getMethod("substring", new Class[] { Integer.TYPE }); + Object res = ReflectionUtils.invokeMethod(substring, "abc123", new Object[] { new Integer(3) }); + assertNotNull(res); + assertTrue(res instanceof String); + assertEquals("123", res); + } + + public void testInvokeProblem() throws Exception { + Method substring = String.class.getMethod("substring", new Class[] { Integer.TYPE }); + try { + ReflectionUtils.invokeMethod(substring, new Date()); + fail(); + } + catch (RuntimeException e) { + } + + try { + ReflectionUtils.invokeMethod(substring, "abc"); + } + catch (RuntimeException e) { + } + } + + public void testInvokeRuntimeException() throws Exception { + Method substring = String.class.getMethod("substring", new Class[] { Integer.TYPE }); + try { + ReflectionUtils.invokeMethod(substring, "abc", new Object[] { new Integer(10) }); + fail(); + } + catch (IndexOutOfBoundsException e) { + } + } + + public void testInvokeCheckedException() throws Exception { + Method m = ReflectionUtilsTests.class.getMethod("methodThatThrowsCheckedException", null); + try { + ReflectionUtils.invokeMethod(m, null); + fail(); + } + catch (RuntimeException e) { + } + } + + public static void methodThatThrowsCheckedException() throws IOException { + new FileInputStream(new File("bogus")); + } +} diff --git a/webflow-architecture.xml b/webflow-architecture.xml new file mode 100644 index 00000000..d4b30803 --- /dev/null +++ b/webflow-architecture.xml @@ -0,0 +1,1168 @@ + + + + + + assert + + + + Spring Web Flow Layers + + + + java.lang.* + + + java.io.** + + + java.net.** + + + java.util** + + + java.text.** + + + java.math.** + + + java.lang.ref.** + + + + + + + + + + + + + + + + + + Spring Data Binding::Conversion + + + + org.springframework.binding.convert + + + org.springframework.binding.convert.** + + + org.springframework.binding.method + + + + + + + + Spring Data Binding::Mapping + + + + org.springframework.binding.mapping + + + + + + + + Spring Data Binding::Expression Evaluation + + + + org.springframework.binding.expression + + + org.springframework.binding.expression.** + + + + + + + + Spring Data Binding::Formatting + + + + org.springframework.binding.format + + + org.springframework.binding.format.** + + + + + + + + Spring Data Binding::Collection + + + + org.springframework.binding.collection + + + + + + + Spring Data Binding + + + true + + + 669 + + + 118 + + + + + + + + + external::reflection + + + + java.lang.reflect + + + + 179 + + + 263 + + + + + + + external::bean-support + + + + java.beans + + + + 626 + + + 262 + + + + + + + external::junit + + + + junit.** + + + + 296 + + + 263 + + + + + + + external::log4j + + + + org.apache.log4j** + + + + 463 + + + 262 + + + + + + + external::xml-support + + + + javax.xml.** + + + org.xml.** + + + org.dom4j** + + + org.w3c.dom** + + + + 10 + + + 263 + + + + + + + external::servlet-support + + + + javax.servlet** + + + + 343 + + + 200 + + + + + + + external::Spring Core + + + + org.springframework.core.** + + + org.springframework.core + + + org.springframework.util.** + + + org.springframework.util + + + + + + + + external::Spring Beans + + + + org.springframework.beans.** + + + org.springframework.beans + + + + + + + + external::Spring Web + + + + org.springframework.web.** + + + org.springframework.web + + + + + + + + external::Spring Context + + + + org.springframework.context.** + + + org.springframework.context + + + org.springframework.validation.** + + + org.springframework.validation + + + + + + + + external::JSF + + + + javax.faces.** + + + javax.faces + + + + + + + + external::Struts + + + + org.apache.struts.action + + + org.apache.struts.config + + + + + + + + external::java-security + + + + java.security + + + + + + + + external::portlet-support + + + + javax.portlet + + + + + + + + external::ognl + + + + ognl + + + + + + + + external::commons-codec + + + + org.apache.commons.codec.binary + + + + + + + + external::commons-logging + + + + org.apache.commons.logging + + + + + + + + external::util-concurrent + + + + EDU.oswego.cs.dl.util.concurrent + + + + + + + external + + + true + + + 671 + + + 322 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Execution Engine::Flow Builder + + + + org.springframework.webflow.engine.builder + + + org.springframework.webflow.engine.builder.** + + + org.springframework.webflow.engine.registry + + + + + + + + + + + + + + + + + + + + + + Execution Core::Utilities + + + + org.springframework.webflow.util + + + + + + + + + + + + + + + + + + + Execution Core::Flow Definition Registry + + + + org.springframework.webflow.definition.registry + + + + + + + + + + + + + + + + + + + Execution Core::Core + + + + org.springframework.webflow.core + + + org.springframework.webflow.core.** + + + + + + + + + + + + + + + + + + + Execution Core::Conversation Management + + + + org.springframework.webflow.conversation.** + + + org.springframework.webflow.conversation + + + + + + + + + + + + + + + + Execution Core::Action + + + + org.springframework.webflow.action + + + org.springframework.webflow.action.** + + + + + + + + + + + + + + + + + + + Execution Core::Flow Execution + + + + org.springframework.webflow.execution + + + org.springframework.webflow.execution.support + + + + + + + + + + + + + + + + + + + Execution Core::Flow Execution Factory + + + + org.springframework.webflow.execution.factory + + + org.springframework.webflow.execution.factory.** + + + + + + + + + + + + + + + + + + + Execution Core::Flow Execution Repository + + + + org.springframework.webflow.execution.repository + + + org.springframework.webflow.execution.repository.** + + + + + + + Execution Core::External Context::Support + + + + org.springframework.webflow.context.support.** + + + + + + + Execution Core::Flow Definition + + + + org.springframework.webflow + + + org.springframework.webflow.definition + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Executor::Core Executor Elements + + + + org.springframework.webflow.executor + + + org.springframework.webflow.executor.support + + + + + + + + + + + + + + + + Executor::JSF + + + + org.springframework.webflow.executor.jsf + + + + + + + + + + Execution Core::External Context::Servlet + + + + org.springframework.webflow.context.servlet.ServletExternalContext + + + + + + + + + + Execution Engine::Engine Implementation + + + + org.springframework.webflow.engine + + + org.springframework.webflow.engine.support + + + org.springframework.webflow.engine.impl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Executor::Struts + + + + org.springframework.webflow.executor.struts + + + + + + + + + + + + + Execution Core::External Context::Portlet + + + + org.springframework.webflow.context.portlet.PortletExternalContext + + + + + + + + + + Test::Test Support + + + + org.springframework.webflow.test + + + org.springframework.webflow.test.** + + + + + + + Test + + + 451 + + + 324 + + + + + + + + + + + + + + + + + + + + + + Execution Engine + + + 451 + + + 111 + + + + + + + + + + + + System Configuration::Config + + + + org.springframework.webflow.config + + + + + + + System Configuration + + + 214 + + + 9 + + + + + + + + + + + + + + + + + Executor::Spring MVC + + + + org.springframework.webflow.executor.mvc + + + + + + + + + + + + + Executor + + + 9 + + + 113 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Execution Core::External Context::ExternalContext + + + + org.springframework.webflow.context.* + + + + + + + + + + + + + + + + Execution Core::External Context + + + + org.springframework.webflow.context + + + org.springframework.webflow.context.** + + + + + + + + + + + + + Execution Core + + + false + + + 220 + + + 325 + + + + + + + + + + + + + + + + + + + + + architecture-template + + + C:/development/spring-projects/spring-webflow + + + C:/development/spring-projects/spring-webflow/webflow-architecture.xml + + + diff --git a/webflow-workspace.xml b/webflow-workspace.xml new file mode 100644 index 00000000..738fadd8 --- /dev/null +++ b/webflow-workspace.xml @@ -0,0 +1,28 @@ + + + + + Web Flow Workspace + + + C:/development/spring-projects/spring-webflow + + + C:/development/spring-projects/spring-webflow/web-flow-workspace.xml + + + + ./spring-webflow/src/main/java + + + ./spring-webflow/target/classes + + + ./spring-binding/target/classes + + + ./spring-binding/src/main/java + + + +