[java-idp-plugin-oidc-rp] branch main updated: Add test classes to RP project

Phil Smart philip.smart at jisc.ac.uk
Wed Jul 13 13:41:51 UTC 2022


This is an automated email from the git hooks/post-receive script.

philsmart pushed a commit to branch main
in repository java-idp-plugin-oidc-rp.

View the commit online:
http://git.shibboleth.net/view/?p=java-idp-plugin-oidc-rp.git;a=commit;h=0b3c4e6492b402b739465122c8fbd72b45f6d54f

The following commit(s) were added to refs/heads/main by this push:
     new 0b3c4e6  Add test classes to RP project
0b3c4e6 is described below

commit 0b3c4e6492b402b739465122c8fbd72b45f6d54f
Author: Phil Smart <philip.smart at jisc.ac.uk>
AuthorDate: Wed Jul 13 14:41:45 2022 +0100

    Add test classes to RP project
    
    Remove reliance on external test project for the time being
    Some of these classes are duplicates of what are used in the Duo plugin
---
 idp-oidc-rp-impl/pom.xml                           |    6 -
 .../flow/AbstractAuthnXmlFlowExecutionTests.java   |  585 +++++++++++
 .../test/flow/mock/IdPPropertyConfigurer.java      |   38 +
 .../authn/test/flow/mock/MockFlowBuilder.java      |   73 ++
 .../CustomAbstractXmlFlowExecutionTests.java       |  103 ++
 .../test/spring/CustomFlowModelFlowBuilder.java    | 1016 ++++++++++++++++++++
 .../spring/CustomFlowRelativeResourceLoader.java   |   81 ++
 .../test/spring/CustomLocalFlowBuilderContext.java |  110 +++
 .../authn/test/spring/CustomSubflowExpression.java |   51 +
 .../idp/plugin/authn/test/spring/package-info.java |   19 +
 10 files changed, 2076 insertions(+), 6 deletions(-)

diff --git a/idp-oidc-rp-impl/pom.xml b/idp-oidc-rp-impl/pom.xml
index 5e68638..d9b1433 100644
--- a/idp-oidc-rp-impl/pom.xml
+++ b/idp-oidc-rp-impl/pom.xml
@@ -117,12 +117,6 @@
             <scope>provided</scope>
         </dependency>
         <!-- Test dependency -->
-        <dependency>
-            <groupId>net.shibboleth.idp.plugin.authn.test</groupId>
-            <artifactId>idp-webflow-execution-test</artifactId>
-            <version>0.0.1-SNAPSHOT</version>
-            <type>test-jar</type>
-        </dependency>
         <dependency>
             <groupId>${spring.groupId}</groupId>
             <artifactId>spring-test</artifactId>
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/flow/AbstractAuthnXmlFlowExecutionTests.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/flow/AbstractAuthnXmlFlowExecutionTests.java
new file mode 100644
index 0000000..1f8ac43
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/flow/AbstractAuthnXmlFlowExecutionTests.java
@@ -0,0 +1,585 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.test.flow;
+
+import java.time.Duration;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Properties;
+import java.util.Set;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.mockito.Mockito;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.ConversionServiceFactoryBean;
+import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
+import org.springframework.context.support.StaticApplicationContext;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.core.env.Environment;
+import org.springframework.core.env.StandardEnvironment;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.EncodedResource;
+import org.springframework.mock.env.MockPropertySource;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.webflow.config.FlowDefinitionResource;
+import org.springframework.webflow.config.FlowDefinitionResourceFactory;
+import org.springframework.webflow.engine.Flow;
+import org.springframework.webflow.test.MockExternalContext;
+import org.springframework.webflow.test.MockFlowBuilderContext;
+import org.springframework.webflow.test.execution.AbstractExternalizedFlowExecutionTests;
+
+import com.google.common.net.HttpHeaders;
+
+import net.shibboleth.ext.spring.config.BooleanToPredicateConverter;
+import net.shibboleth.ext.spring.config.FunctionToFunctionConverter;
+import net.shibboleth.ext.spring.config.IdentifiableBeanPostProcessor;
+import net.shibboleth.ext.spring.config.PredicateToPredicateConverter;
+import net.shibboleth.ext.spring.config.StringBooleanToPredicateConverter;
+import net.shibboleth.ext.spring.config.StringToIPRangeConverter;
+import net.shibboleth.ext.spring.config.StringToResourceConverter;
+import net.shibboleth.ext.spring.util.AnnotationParameterNameDiscoverer;
+import net.shibboleth.idp.authn.AuthenticationFlowDescriptor;
+import net.shibboleth.idp.authn.context.AuthenticationContext;
+import net.shibboleth.idp.authn.context.SubjectCanonicalizationContext;
+import net.shibboleth.idp.plugin.authn.test.spring.CustomAbstractXmlFlowExecutionTests;
+import net.shibboleth.idp.plugin.authn.test.spring.CustomFlowModelFlowBuilder;
+import net.shibboleth.idp.profile.context.navigate.WebflowRequestContextProfileRequestContextLookup;
+import net.shibboleth.idp.session.IdPSession;
+import net.shibboleth.idp.session.context.SessionContext;
+import net.shibboleth.idp.ui.context.RelyingPartyUIContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullElements;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.security.impl.SecureRandomIdentifierGenerationStrategy;
+import net.shibboleth.utilities.java.support.xml.DOMTypeSupport;
+
+
+/**
+ * <p>
+ * Base test class for testing webflows in isolation from the rest of the IdP.
+ * </p>
+ * 
+ * <p>
+ * Each flow test reinitialises the mock beans. See {@link #setUp()} and
+ * {@link #registerMockBeanDefinitions(MockFlowBuilderContext)}.
+ * </p>
+ * 
+ * <p>
+ * Extends the {@link CustomAbstractXmlFlowExecutionTests} over the standard
+ * {@link AbstractExternalizedFlowExecutionTests} for the sole reason of being able to set a
+ * {@link CustomFlowModelFlowBuilder} with a placeholder prefix of <code>%{</code> to be compatible with those used by
+ * the IdP. We also set this prefix in various other locations, but those are either ignored by the inbuilt SWF flow
+ * builder when it substitutes the placeholder inside the bean import, or do not share the same Mock properties source
+ * as is set in the {@link #registerMockPropertySource(MockFlowBuilderContext)} method. If a better way is found to
+ * support that, switch back to the {@link AbstractExternalizedFlowExecutionTests}.
+ * </p>
+ * <p>
+ * In addition, the {@link AnnotationParameterNameDiscoverer} class is also registered with the
+ * {@link DefaultListableBeanFactory} for the context created by the {@link CustomFlowModelFlowBuilder}.
+ * 
+ * <p>
+ * Note, you can not use test context creation annotations e.g. @SpringJUnitWebConfig to help build the application
+ * context because the {@link AbstractExternalizedFlowExecutionTests} creates a new {@link StaticApplicationContext}
+ * when building the flow (see buildFlow). This is different from the {@link ApplicationContext} created by
+ * {@link ContextConfiguration} annotation processors. As a result, those bean configurations and environment variables
+ * need to be injected inside {@link #configureFlowBuilderContext(MockFlowBuilderContext)}
+ * </p>
+ * 
+ * <p>
+ * These test use JUnit (for SWF test compatibility), and work with the TestNG to JUnit bridge
+ * </p>
+ */
+public abstract class AbstractAuthnXmlFlowExecutionTests extends CustomAbstractXmlFlowExecutionTests{
+    
+    /** Mock request. */
+    protected MockHttpServletRequest mockRequest;
+
+    /** Mock response. */
+    protected MockHttpServletResponse mockResponse;
+    
+    /** Mock external context. */
+    protected MockExternalContext externalContext;
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(AbstractAuthnXmlFlowExecutionTests.class);
+    
+    /** The path resource to the authentication flow to be tested. */
+    @Nonnull private String flowPath;
+    
+    /**
+     *  Map of additional flow resources used to support the construction of the test flow.
+     *  These are only possible parents of the flow, and can not be used to load other flows.
+     *  <p>The key is the path to the resource, the value is the flowID.</p>
+     */
+    @Nonnull private Map<String,String> flowModelResources;
+    
+    /** Mock flows to add as stub subflows when executing the main test flow. */
+    @Nonnull private List<Flow> subflows;
+    
+    /** Map of properties to set.*/
+    @Nonnull private Map<String,String> mockProperties;
+    
+    /** The mocked entityID of this IdP instance.*/
+    @Nonnull private final String entityId;
+    
+  
+ 
+    /**
+     * 
+     * Constructor.
+     *
+     * @param idpEntityID The mocked entityID of this IdP instance
+     */
+    public AbstractAuthnXmlFlowExecutionTests(@Nonnull final String idpEntityID) {        
+        flowModelResources = Collections.emptyMap();
+        subflows = Collections.emptyList();    
+        mockProperties = Collections.emptyMap();
+        entityId = Objects.requireNonNull(idpEntityID);
+    }
+    
+    /**
+     * Set the flow model resources (parent flows) that support the assembly of the flow to test.
+     * 
+     * @param testFlowModelResources the parent flows.
+     */
+    public void setFlowModelResources(@Nonnull final Map<String,String> testFlowModelResources) {
+        assertNotNull(testFlowModelResources);        
+        flowModelResources = testFlowModelResources;
+    }
+    
+    /**
+     * Set the path to the flow to test.
+     * 
+     * @param testFlowPath the path to the file to test.
+     */
+    public void setFlowPath(@Nonnull @NotEmpty final String testFlowPath) {
+        assertNotNull(testFlowPath);
+        flowPath = testFlowPath;
+    }
+    
+    /**
+     * Set the mock properties to use within a property source.
+     * 
+     * @param properties the mock properties.
+     */
+    public void setMockProperties(@Nonnull final Map<String,String> properties) {
+        assertNotNull(properties);
+        mockProperties = properties;
+    }
+    
+    /**
+     * Set the mock subflows to be used with the flow to test.
+     * 
+     * @param mockSubflows the mock subflows.
+     */
+    public void setSubflows(@Nonnull @NonnullElements final List<Flow> mockSubflows) {
+        assertNotNull(mockSubflows);
+        subflows = mockSubflows;
+    }
+    
+   
+    /**
+     * Add HTTP basic authentication headers to the request.
+     * 
+     * @param username the username
+     * @param password the password
+     */
+    public void addHttpBasicAuthHeader(@Nonnull final String username, @Nonnull final String password) {
+        assertNotNull(username);
+        assertNotNull(password);
+        mockRequest.addHeader(HttpHeaders.AUTHORIZATION, "Basic "+ 
+                Base64.getEncoder().encodeToString((username+":"+password).getBytes()));        
+    }
+    
+    /**
+     * Setup the mock objects before each method.
+     * @throws Exception on error
+     * @throws SecurityException on error
+     * @throws NoSuchFieldException on error
+     */
+    @Override public void setUp() throws NoSuchFieldException, SecurityException, Exception {
+        mockRequest = new MockHttpServletRequest();        
+        mockResponse = new MockHttpServletResponse();
+        externalContext = new MockExternalContext();
+        externalContext.setNativeRequest(mockRequest);
+        externalContext.setNativeResponse(mockResponse);
+    }
+    
+
+    @Override
+    protected FlowDefinitionResource getResource(final FlowDefinitionResourceFactory resourceFactory) {
+        return resourceFactory.createClassPathResource(flowPath, getClass());
+    }
+
+    @Override
+    protected FlowDefinitionResource[] getModelResources(final FlowDefinitionResourceFactory resourceFactory) {
+        
+        if (!flowModelResources.isEmpty()) {
+
+            final FlowDefinitionResource[] resources = new FlowDefinitionResource[flowModelResources.size()];
+            
+            int rIndex = 0;
+            for (final Map.Entry<String, String> entry : flowModelResources.entrySet()) {
+                resources[rIndex++] = resourceFactory.createResource(entry.getKey(), null,entry.getValue());
+            }            
+            return resources;            
+        }        
+        return null;      
+    }
+    
+    @Override
+    protected void configureFlowBuilderContext(final MockFlowBuilderContext builderContext) {
+        registerMockBeanDefinitions(builderContext);   
+        registerMockPropertySource(builderContext);
+        registerMockSubflows(builderContext);
+    }
+    
+    /**
+     * Register the subflows with the builder context.
+     * 
+     * @param builderContext the builder context to register the subflows with.
+     */
+    private void registerMockSubflows(@Nonnull final MockFlowBuilderContext builderContext) {        
+        assertNotNull(builderContext);       
+        
+        subflows.forEach(flow -> builderContext.registerSubflow(flow));        
+    }
+    
+    /**
+     * Register Shibboleth IdP requred mock beans that are not necessary to configure for authentication testing
+     * but are required for authentication flows to proceed. 
+     * <p>
+     * Project specific tests should override this method, making sure to also call super().
+     * </p>
+     * 
+     * @param builderContext the context used to register the beans.
+     */
+    protected void registerMockBeanDefinitions(@Nonnull final MockFlowBuilderContext builderContext) {
+        
+        builderContext.registerBean("shibboleth.DefaultIdentifierGenerationStrategy", 
+                new SecureRandomIdentifierGenerationStrategy());
+        builderContext.registerBean("shibboleth.HttpServletRequest",mockRequest);
+        builderContext.registerBean("shibboleth.HttpServletResponse", mockResponse);      
+        builderContext.registerBean("shibboleth.PostLoginSubjectCanonicalizationFlows", Collections.emptyList());
+
+        //create a real JSON parser
+        builderContext.registerBean("shibboleth.JSONObjectMapper", new com.fasterxml.jackson.databind.ObjectMapper());
+        
+        //create converter for Durations
+        final ConversionServiceFactoryBean conversationService = new ConversionServiceFactoryBean();
+        //unable to register the spring-ext converter even though it is the same!    
+        conversationService.setConverters(createConverters());
+        conversationService.afterPropertiesSet();
+        builderContext.registerBean("conversionService", conversationService.getObject());
+        
+        
+        //register shibboleth function constants.
+        addBeanDefinition(builderContext, "shibboleth.Functions.Constant",BeanDefinitionBuilder.
+                genericBeanDefinition(net.shibboleth.utilities.java.support.logic.FunctionSupport.class)
+                .setFactoryMethod("constant").setAbstract(true).getBeanDefinition());
+        
+        
+        addBeanDefinition(builderContext, "shibboleth.Functions.Compose",BeanDefinitionBuilder.
+                genericBeanDefinition(net.shibboleth.utilities.java.support.logic.FunctionSupport.class)
+                .setFactoryMethod("compose").setAbstract(true).getBeanDefinition());
+        
+        addBeanDefinition(builderContext, "shibboleth.ContextFunctions.Expression",BeanDefinitionBuilder.
+                genericBeanDefinition(net.shibboleth.utilities.java.support.logic.FunctionSupport.class)
+                .setFactoryMethod("compose").setAbstract(true).getBeanDefinition());
+        
+        
+        addBeanDefinition(builderContext, "shibboleth.MessageContextLookup.Outbound",BeanDefinitionBuilder.
+                genericBeanDefinition(org.opensaml.profile.context.navigate.OutboundMessageContextLookup.class)
+                .getBeanDefinition());           
+        
+        addBeanDefinition(builderContext, "shibboleth.MessageContextLookup.Inbound",BeanDefinitionBuilder.
+                genericBeanDefinition(org.opensaml.profile.context.navigate.InboundMessageContextLookup.class)
+                .getBeanDefinition()); 
+       
+        addBeanDefinition(builderContext, "shibboleth.ResponderIdLookup.Simple",BeanDefinitionBuilder.
+                genericBeanDefinition(net.shibboleth.idp.profile.context.navigate.ResponderIdLookupFunction.class)
+                .getBeanDefinition()); 
+       
+        addBeanDefinition(builderContext, "shibboleth.RelyingPartyIdLookup.Simple",BeanDefinitionBuilder.
+                genericBeanDefinition(net.shibboleth.idp.profile.context.navigate.RelyingPartyIdLookupFunction.class)
+                .getBeanDefinition()); 
+        
+        
+        addBeanDefinition(builderContext, "shibboleth.Functions.Expression",BeanDefinitionBuilder.
+                genericBeanDefinition(net.shibboleth.ext.spring.util.SpringExpressionFunction.class)
+                .setAbstract(true).getBeanDefinition());        
+        
+        addBeanDefinition(builderContext, "shibboleth.BiFunctions.Expression",BeanDefinitionBuilder.
+                genericBeanDefinition(net.shibboleth.ext.spring.util.SpringExpressionBiFunction.class)
+                .setAbstract(true).getBeanDefinition());      
+        
+        
+        addBeanDefinition(builderContext, "shibboleth.ChildLookup.RelyingParty",BeanDefinitionBuilder.
+                genericBeanDefinition(org.opensaml.messaging.context.navigate.ChildContextLookup.class)
+                .addConstructorArgValue(net.shibboleth.idp.profile.context.RelyingPartyContext.class).getBeanDefinition());
+        
+        addBeanDefinition(builderContext, "shibboleth.ChildLookup.ProfileRequestContext",BeanDefinitionBuilder.
+                genericBeanDefinition(org.opensaml.messaging.context.navigate.ChildContextLookup.class)
+                .addConstructorArgValue(org.opensaml.profile.context.ProfileRequestContext.class).getBeanDefinition());
+        
+        addBeanDefinition(builderContext, "shibboleth.ChildLookup.AuthenticationContext",BeanDefinitionBuilder.
+                genericBeanDefinition(org.opensaml.messaging.context.navigate.ChildContextLookup.class)
+                .addConstructorArgValue(net.shibboleth.idp.authn.context.AuthenticationContext.class).getBeanDefinition());
+        
+        addBeanDefinition(builderContext, "shibboleth.Functions.ProxyProfileRequestAuthenticationContext",BeanDefinitionBuilder.
+                genericBeanDefinition(net.shibboleth.utilities.java.support.logic.FunctionSupport.class)
+                .setParentName("shibboleth.Functions.Compose")
+                .addConstructorArgReference("shibboleth.ChildLookup.ProfileRequestContext")
+                .addConstructorArgReference("shibboleth.ChildLookup.AuthenticationContext")
+                .getBeanDefinition());
+        
+        addBeanDefinition(builderContext, "shibboleth.ChildLookup.ProxyProfileRequestContext",BeanDefinitionBuilder.
+                genericBeanDefinition(net.shibboleth.utilities.java.support.logic.FunctionSupport.class)
+                .setParentName("shibboleth.Functions.Compose")
+                .addConstructorArgReference("shibboleth.Functions.ProxyProfileRequestAuthenticationContext")
+                .addConstructorArgValue(new WebflowRequestContextProfileRequestContextLookup()).getBeanDefinition());
+                       
+    }
+    
+    
+    /**
+     * Adds the bean to the {@link StaticApplicationContext} contained in the builder context.
+     * 
+     * @param builderContext to add the bean to.
+     * @param beanName the name of the bean.
+     * @param bean the bean.
+     */
+    protected void addBeanDefinition(@Nonnull final MockFlowBuilderContext builderContext, 
+            @Nonnull final String beanName, @Nonnull final BeanDefinition bean) {
+        
+        assertNotNull(builderContext);
+        assertNotNull(beanName);
+        assertNotNull(bean);
+        assertTrue( builderContext.getApplicationContext() instanceof ConfigurableApplicationContext);
+        
+        final BeanFactory factory = ((ConfigurableApplicationContext) builderContext.
+                getApplicationContext()).getBeanFactory();
+        
+        assertNotNull(factory);
+        assertTrue(factory instanceof DefaultListableBeanFactory);
+        
+        ((DefaultListableBeanFactory)factory).registerBeanDefinition(beanName, bean);
+        
+    }
+    
+    /**
+     * Adds a singleton bean to the {@link StaticApplicationContext} contained in the builder context.
+     * 
+     * @param builderContext to add the bean to.
+     * @param beanName the name of the bean.
+     * @param bean the bean.
+     */
+    protected void addBeanSingleton(@Nonnull final MockFlowBuilderContext builderContext, 
+            @Nonnull final String beanName, @Nonnull final Object bean) {
+        
+        assertNotNull(builderContext);
+        assertNotNull(beanName);
+        assertNotNull(bean);
+        assertTrue( builderContext.getApplicationContext() instanceof ConfigurableApplicationContext);
+        
+        final BeanFactory factory = ((ConfigurableApplicationContext) builderContext.
+                getApplicationContext()).getBeanFactory();
+        
+        assertNotNull(factory);
+        assertTrue(factory instanceof DefaultListableBeanFactory);
+        
+        ((DefaultListableBeanFactory)factory).registerSingleton(beanName, bean);
+        
+    }
+    
+    /**
+     * Load beans into the builder context from the given resource e.g. a classpath resource. The loader
+     * used here is separate to the main webflow builder and as such some things need setting up specific
+     * to the correct loading of resources in the given xml file.
+     * 
+     * @param builderContext the builder context.
+     * @param xmlFile the xml file.
+     * @param additionalProperties properties that need to be injected into the bean factory.
+     */
+    protected void loadBeanDefinitionsFromXmlFile(@Nonnull final MockFlowBuilderContext builderContext,
+            @Nonnull final Resource xmlFile, @Nullable final Map<String,String> additionalProperties) {
+        assertNotNull(builderContext);
+        assertTrue(xmlFile.exists());
+        
+        assertTrue( builderContext.getApplicationContext() instanceof ConfigurableApplicationContext);
+        final BeanFactory factory = ((ConfigurableApplicationContext) builderContext.
+                getApplicationContext()).getBeanFactory();
+        
+        assertNotNull(factory);
+        assertTrue(factory instanceof DefaultListableBeanFactory);
+        
+        final XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader((DefaultListableBeanFactory) factory);
+        //we need to register the IdentifiableBeanPostProcessor as that is not registered by this point if
+        //used in the webflow, and this resource is not loading one.
+        ((DefaultListableBeanFactory)factory).addBeanPostProcessor(new IdentifiableBeanPostProcessor());
+        
+        ((DefaultListableBeanFactory)factory).setParameterNameDiscoverer(new AnnotationParameterNameDiscoverer());
+        
+        //create converter for Durations
+        final ConversionServiceFactoryBean conversationService = new ConversionServiceFactoryBean();
+        //unable to register the spring-ext converter even though it is the same!       
+        conversationService.setConverters(createConverters());
+        conversationService.afterPropertiesSet();
+        
+        ((DefaultListableBeanFactory)factory).setConversionService(conversationService.getObject());   
+        
+        final int beanNumbers = beanDefinitionReader.loadBeanDefinitions(new EncodedResource(xmlFile, "UTF-8"));
+        
+        //process any properties in the file bean expressions
+        final Properties mock = new Properties();        
+        //add the standard mocks to enable basic IdP function if needed.
+        mock.setProperty("idp.home", "classpath:");
+        mock.setProperty("idp.entityID", entityId);
+        mock.setProperty("idp.webflows", "classpath*:/flows");
+        if (additionalProperties != null) {
+            additionalProperties.forEach((k, v) -> {
+                log.debug("Adding property ['{}':'{}']", k, v);
+                mock.setProperty(k, v);
+            });
+        }
+        final PropertySourcesPlaceholderConfigurer ppc = new PropertySourcesPlaceholderConfigurer();
+        ppc.setProperties(mock);
+        ppc.setPlaceholderPrefix("%{");
+        ppc.setPlaceholderSuffix("}");
+        ppc.postProcessBeanFactory((DefaultListableBeanFactory)factory);
+        
+        log.info("Loaded {} beans from {}",beanNumbers, xmlFile);
+    }
+    
+    /**
+     * Create a set of converters similar to those in the IdP's global-system.xml file.
+     * For some reason, it will not register the pre-defined converters, so they are
+     * re-created here. 
+     * 
+     * @return a set of converters.
+     */
+    private Set<Converter<?, ?>> createConverters(){
+        final Converter<String,Duration> durationConverter = new Converter<String,Duration>(){
+
+            @Override
+            public Duration convert(final String source) {
+                if (source.startsWith("P") || source.startsWith("-P")) {
+                    return DOMTypeSupport.stringToDuration(source.trim());
+                }                
+                // Treat as milliseconds.
+                return Duration.ofMillis(Long.valueOf(source));
+            }
+            
+        };  
+
+        //some of these may not work comming from the spring-ext IdP project and may need specifying explicitly
+        //like the above converter. TODO check why.
+        return Set.of(durationConverter, new StringToResourceConverter(), new StringToIPRangeConverter(),
+                new BooleanToPredicateConverter(),new StringBooleanToPredicateConverter(),
+                new StringToResourceConverter(),new FunctionToFunctionConverter<>(),
+                new PredicateToPredicateConverter<>());
+    }
+    
+    /**
+     * Register a {@link MockPropertySource} with the {@link Environment} of the current flow builders
+     * {@link ApplicationContext}. Set properties required to run an authentication flow.
+     * 
+     * @param builderContext the builder context to attach the mock property source to.
+     */
+    protected void registerMockPropertySource(@Nonnull final MockFlowBuilderContext builderContext) {
+        
+        assertNotNull(builderContext);
+        assertNotNull(builderContext.getApplicationContext());
+        assertNotNull(builderContext.getApplicationContext().getEnvironment());
+        assertTrue(builderContext.getApplicationContext().getEnvironment() instanceof StandardEnvironment);
+        
+        final MockPropertySource mock = new MockPropertySource();
+        
+        //add the standard mocks to enable basic IdP function if needed.
+        mock.setProperty("idp.home", "classpath:");
+        mock.setProperty("idp.webflows", "classpath*:/flows");
+        
+        //add test specific mocks
+        mockProperties.forEach((key,value) -> mock.setProperty(key, value));
+        
+        //Tell the root context to use the Shib style property prefix over ${. The PropertySourcesPlaceholderConfigurer 
+        //defined in the beans file does not have access to the MockProperties set by the test, this root context resolver does
+        //so we change this here. This will not work for bean import definitions, they are run with a standard resolver ${. 
+        //hence why we use a custom flow builder (see super class).
+        ((StandardEnvironment)builderContext.getApplicationContext().getEnvironment()).
+        getPropertySources().addFirst(mock);
+        ((StandardEnvironment)builderContext.getApplicationContext().getEnvironment()).setPlaceholderPrefix("%{");
+        ((StandardEnvironment)builderContext.getApplicationContext().getEnvironment()).setPlaceholderSuffix("}");
+
+        
+    }
+    
+    /**
+     * Build a {@link ProfileRequestContext} by configuring a suitable context tree e.g. a 
+     * {@link AuthenticationContext}.
+     * 
+     * @param forceAuthn force authentication
+     * @param addC14Context add the c14 context, yes or no.
+     * @return a profile request context.
+     */
+    @Nonnull protected ProfileRequestContext buildProfileRequestContext(@Nonnull final String flowId,
+            @Nonnull final boolean forceAuthn,
+            @Nonnull final boolean addC14Context) {
+        
+        final ProfileRequestContext prc = new ProfileRequestContext();
+        final AuthenticationContext ac = new AuthenticationContext();
+        final AuthenticationFlowDescriptor afd = new AuthenticationFlowDescriptor();
+        afd.setId(flowId);
+        ac.setAttemptedFlow(afd);
+        ac.addSubcontext(new RelyingPartyUIContext());
+        ac.setForceAuthn(forceAuthn);
+        
+        prc.addSubcontext(ac);
+        if (addC14Context) {
+            final SubjectCanonicalizationContext scc = new SubjectCanonicalizationContext();
+                scc.setPrincipalName("jdoe");
+                prc.addSubcontext(scc);
+        }
+        final SessionContext sc = new SessionContext();
+        final IdPSession session = Mockito.mock(IdPSession.class);
+        Mockito.when(session.getPrincipalName()).thenReturn("jdoe");
+        sc.setIdPSession(session);
+        prc.addSubcontext(sc);
+        
+        prc.setBrowserProfile(true);
+        return prc;
+    }
+
+    
+
+}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/flow/mock/IdPPropertyConfigurer.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/flow/mock/IdPPropertyConfigurer.java
new file mode 100644
index 0000000..f593a39
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/flow/mock/IdPPropertyConfigurer.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.test.flow.mock;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
+
+
+/**
+ * Configure the property sources placeholder to use the correct placeholder prefix of {@literal %}.
+ */
+ at Configuration
+public class IdPPropertyConfigurer {
+    
+    @Bean
+    public static PropertySourcesPlaceholderConfigurer properties() throws Exception {
+        final PropertySourcesPlaceholderConfigurer pspc = new PropertySourcesPlaceholderConfigurer();
+        pspc.setPlaceholderPrefix("%{");
+        return pspc;
+    }
+
+}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/flow/mock/MockFlowBuilder.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/flow/mock/MockFlowBuilder.java
new file mode 100644
index 0000000..b92e1b0
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/flow/mock/MockFlowBuilder.java
@@ -0,0 +1,73 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.test.flow.mock;
+
+import javax.annotation.Nonnull;
+
+import org.springframework.util.Assert;
+import org.springframework.webflow.engine.ActionState;
+import org.springframework.webflow.engine.EndState;
+import org.springframework.webflow.engine.Flow;
+import org.springframework.webflow.engine.RequestControlContext;
+import org.springframework.webflow.engine.Transition;
+import org.springframework.webflow.engine.support.DefaultTargetStateResolver;
+import org.springframework.webflow.execution.Event;
+import org.springframework.webflow.execution.FlowExecutionException;
+
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+
+/**
+ * A builder for generating simple mock flows. Mostly used to generate stub subflows when testing
+ * a flow.
+ */
+public final class MockFlowBuilder {
+    
+    /** Constructor.*/
+    private MockFlowBuilder() {
+        
+    }
+    
+    /**
+     * Create a simple flow with one start-state which transitions to an
+     * end-state that ends the flow. The flow does nothing.
+     * 
+     * @param flowId the flow identifier.
+     * 
+     * @return the constructed mock flow.
+     */
+    public static Flow MockNoOpFlow(@Nonnull @NotEmpty final String flowId) {
+        Assert.notNull(flowId, "Flow to mock must have an ID");
+        
+        final Flow flow = new Flow(flowId);
+        final ActionState endingStartState = new ActionState(flow,"noop-start-start") {
+            
+            @Override
+            protected void doEnter(final RequestControlContext context) throws FlowExecutionException {
+                context.handleEvent(new Event(this, "proceed"));
+                
+            }
+        };
+        //add a transition on any event to the 'proceed' end-state
+        endingStartState.getTransitionSet().add(new Transition(new DefaultTargetStateResolver("proceed")));
+        //add a 'proceed' end state to finish the sublow.
+        new EndState(flow, "proceed");       
+        flow.setStartState(endingStartState);
+        return flow;
+    }
+
+}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomAbstractXmlFlowExecutionTests.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomAbstractXmlFlowExecutionTests.java
new file mode 100644
index 0000000..0af35c8
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomAbstractXmlFlowExecutionTests.java
@@ -0,0 +1,103 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.shibboleth.idp.plugin.authn.test.spring;
+
+import org.springframework.beans.factory.config.ConfigurableBeanFactory;
+import org.springframework.webflow.config.FlowDefinitionResource;
+import org.springframework.webflow.config.FlowDefinitionResourceFactory;
+import org.springframework.webflow.engine.builder.FlowBuilder;
+import org.springframework.webflow.engine.model.builder.DefaultFlowModelHolder;
+import org.springframework.webflow.engine.model.builder.FlowModelBuilder;
+import org.springframework.webflow.engine.model.builder.xml.XmlFlowModelBuilder;
+import org.springframework.webflow.engine.model.registry.FlowModelHolder;
+import org.springframework.webflow.engine.model.registry.FlowModelRegistry;
+import org.springframework.webflow.engine.model.registry.FlowModelRegistryImpl;
+import org.springframework.webflow.test.execution.AbstractExternalizedFlowExecutionTests;
+import org.springframework.webflow.test.execution.AbstractXmlFlowExecutionTests;
+
+/**
+ * Copy of the {@link AbstractXmlFlowExecutionTests} class, but using a custom {@link CustomFlowModelFlowBuilder} 
+ * to set the property placeholder prefix of <code>%{</code>.
+ */
+public abstract class CustomAbstractXmlFlowExecutionTests extends AbstractExternalizedFlowExecutionTests {
+
+    private FlowModelRegistry flowModelRegistry = new FlowModelRegistryImpl();
+
+    /**
+     * Constructs a default XML flow execution test.
+     * @see #setName(String)
+     */
+    public CustomAbstractXmlFlowExecutionTests() {
+        super();
+    }
+
+    /**
+     * Constructs an XML flow execution test with given name.
+     * @param name the name of the test
+     */
+    public CustomAbstractXmlFlowExecutionTests(String name) {
+        super(name);
+    }
+
+    protected final FlowBuilder createFlowBuilder(FlowDefinitionResource resource) {
+        registerDependentFlowModels();
+        FlowModelBuilder modelBuilder = new XmlFlowModelBuilder(resource.getPath(), flowModelRegistry);
+        FlowModelHolder modelHolder = new DefaultFlowModelHolder(modelBuilder);
+        flowModelRegistry.registerFlowModel(resource.getId(), modelHolder);
+        return new CustomFlowModelFlowBuilder(modelHolder) {            
+            
+            protected void registerFlowBeans(ConfigurableBeanFactory flowBeanFactory) {
+                registerMockFlowBeans(flowBeanFactory);            
+            }
+            
+        };
+    }
+
+    /**
+     * Template method subclasses may override to return pointers to "flow model resources" needed to build the
+     * definition of the flow being tested. Typically overridden when the flow being tested extends from another flow.
+     * Default returns null, assuming no inheritance.
+     * @param resourceFactory the resource factory
+     * @return the flow definition model resources
+     */
+    protected FlowDefinitionResource[] getModelResources(FlowDefinitionResourceFactory resourceFactory) {
+        return null;
+    }
+
+    /**
+     * Template method subclasses may override to register mock implementations of services used locally by the flow
+     * being tested. By default, this method does nothing.
+     * @param flowBeanFactory the local flow bean factory, you may register mock services with it using
+     * {@link ConfigurableBeanFactory#registerSingleton(String, Object)}
+     */
+    protected void registerMockFlowBeans(ConfigurableBeanFactory flowBeanFactory) {
+    }
+
+    // internal helpers
+
+    private void registerDependentFlowModels() {
+        FlowDefinitionResource[] modelResources = getModelResources(getResourceFactory());
+        if (modelResources != null) {
+            for (FlowDefinitionResource modelResource : modelResources) {
+                FlowModelBuilder modelBuilder = new XmlFlowModelBuilder(modelResource.getPath(), flowModelRegistry);
+                flowModelRegistry.registerFlowModel(modelResource.getId(), new DefaultFlowModelHolder(modelBuilder));
+            }
+        }
+    }
+
+}
+
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomFlowModelFlowBuilder.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomFlowModelFlowBuilder.java
new file mode 100644
index 0000000..e826874
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomFlowModelFlowBuilder.java
@@ -0,0 +1,1016 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package net.shibboleth.idp.plugin.authn.test.spring;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.springframework.beans.factory.config.ConfigurableBeanFactory;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.xml.XmlBeanDefinitionReader;
+import org.springframework.binding.convert.ConversionExecutionException;
+import org.springframework.binding.convert.ConversionExecutor;
+import org.springframework.binding.convert.service.RuntimeBindingConversionExecutor;
+import org.springframework.binding.expression.Expression;
+import org.springframework.binding.expression.ExpressionParser;
+import org.springframework.binding.expression.ParserContext;
+import org.springframework.binding.expression.support.FluentParserContext;
+import org.springframework.binding.mapping.Mapper;
+import org.springframework.binding.mapping.impl.DefaultMapper;
+import org.springframework.binding.mapping.impl.DefaultMapping;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigUtils;
+import org.springframework.context.support.AbstractApplicationContext;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.context.support.ReloadableResourceBundleMessageSource;
+import org.springframework.core.io.Resource;
+import org.springframework.core.style.ToStringCreator;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
+import org.springframework.web.context.WebApplicationContext;
+import org.springframework.web.context.request.RequestScope;
+import org.springframework.web.context.support.GenericWebApplicationContext;
+import org.springframework.webflow.action.EvaluateAction;
+import org.springframework.webflow.action.ExternalRedirectAction;
+import org.springframework.webflow.action.FlowDefinitionRedirectAction;
+import org.springframework.webflow.action.RenderAction;
+import org.springframework.webflow.action.SetAction;
+import org.springframework.webflow.action.ViewFactoryActionAdapter;
+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.Flow;
+import org.springframework.webflow.engine.FlowExecutionExceptionHandler;
+import org.springframework.webflow.engine.FlowVariable;
+import org.springframework.webflow.engine.History;
+import org.springframework.webflow.engine.SubflowAttributeMapper;
+import org.springframework.webflow.engine.TargetStateResolver;
+import org.springframework.webflow.engine.Transition;
+import org.springframework.webflow.engine.TransitionCriteria;
+import org.springframework.webflow.engine.VariableValueFactory;
+import org.springframework.webflow.engine.ViewVariable;
+import org.springframework.webflow.engine.builder.BinderConfiguration;
+import org.springframework.webflow.engine.builder.BinderConfiguration.Binding;
+import org.springframework.webflow.engine.builder.FlowBuilderContext;
+import org.springframework.webflow.engine.builder.FlowBuilderException;
+import org.springframework.webflow.engine.builder.model.FlowModelFlowBuilder;
+import org.springframework.webflow.engine.builder.support.AbstractFlowBuilder;
+import org.springframework.webflow.engine.model.AbstractActionModel;
+import org.springframework.webflow.engine.model.AbstractMappingModel;
+import org.springframework.webflow.engine.model.AbstractStateModel;
+import org.springframework.webflow.engine.model.ActionStateModel;
+import org.springframework.webflow.engine.model.AttributeModel;
+import org.springframework.webflow.engine.model.BeanImportModel;
+import org.springframework.webflow.engine.model.BinderModel;
+import org.springframework.webflow.engine.model.BindingModel;
+import org.springframework.webflow.engine.model.DecisionStateModel;
+import org.springframework.webflow.engine.model.EndStateModel;
+import org.springframework.webflow.engine.model.EvaluateModel;
+import org.springframework.webflow.engine.model.ExceptionHandlerModel;
+import org.springframework.webflow.engine.model.FlowModel;
+import org.springframework.webflow.engine.model.IfModel;
+import org.springframework.webflow.engine.model.InputModel;
+import org.springframework.webflow.engine.model.OutputModel;
+import org.springframework.webflow.engine.model.PersistenceContextModel;
+import org.springframework.webflow.engine.model.RenderModel;
+import org.springframework.webflow.engine.model.SecuredModel;
+import org.springframework.webflow.engine.model.SetModel;
+import org.springframework.webflow.engine.model.SubflowStateModel;
+import org.springframework.webflow.engine.model.TransitionModel;
+import org.springframework.webflow.engine.model.VarModel;
+import org.springframework.webflow.engine.model.ViewStateModel;
+import org.springframework.webflow.engine.model.builder.FlowModelBuilderException;
+import org.springframework.webflow.engine.model.registry.FlowModelHolder;
+import org.springframework.webflow.engine.support.ActionExecutingViewFactory;
+import org.springframework.webflow.engine.support.BeanFactoryVariableValueFactory;
+import org.springframework.webflow.engine.support.DefaultTransitionCriteria;
+import org.springframework.webflow.engine.support.GenericSubflowAttributeMapper;
+import org.springframework.webflow.engine.support.TransitionCriteriaChain;
+import org.springframework.webflow.engine.support.TransitionExecutingFlowExecutionExceptionHandler;
+import org.springframework.webflow.execution.Action;
+import org.springframework.webflow.execution.AnnotatedAction;
+import org.springframework.webflow.execution.RequestContext;
+import org.springframework.webflow.execution.ViewFactory;
+import org.springframework.webflow.scope.ConversationScope;
+import org.springframework.webflow.scope.FlashScope;
+import org.springframework.webflow.scope.FlowScope;
+import org.springframework.webflow.scope.ViewScope;
+import org.springframework.webflow.security.SecurityRule;
+
+import net.shibboleth.ext.spring.resource.ConditionalResourceResolver;
+import net.shibboleth.ext.spring.util.AnnotationParameterNameDiscoverer;
+
+/**
+ * Copy of {@link FlowModelFlowBuilder} for setting the property placeholder prefix of <code>%{</code>, and
+ * inclusion of a conditional resource resolver for resolving {@literal conditional:} resources.
+ */
+public class CustomFlowModelFlowBuilder extends AbstractFlowBuilder {
+
+    private static final boolean IS_SPRING_FACES_PRESENT = ClassUtils.isPresent(
+            "org.springframework.faces.webflow.FlowActionListener", FlowModelFlowBuilder.class.getClassLoader());
+
+    public static final String VALIDATOR_FLOW_ATTR = 
+            FlowModelFlowBuilder.class.getSimpleName() + ".validator";
+
+    public static final String VALIDATION_HINT_RESOLVER_FLOW_ATTR =
+            FlowModelFlowBuilder.class.getSimpleName() + ".validationHintResolver";
+
+    private FlowModelHolder flowModelHolder;
+
+    private FlowModel flowModel;
+    
+    private CustomLocalFlowBuilderContext localFlowBuilderContext;
+
+
+    /**
+     * Creates a flow builder that can build a {@link Flow} from a {@link FlowModel}.
+     * @param flowModelHolder the flow model holder
+     */
+    public CustomFlowModelFlowBuilder(FlowModelHolder flowModelHolder) {
+        Assert.notNull(flowModelHolder, "The FlowModelHolder is required");
+        this.flowModelHolder = flowModelHolder;
+    }
+
+    /**
+     * Initialize this builder. This could cause the builder to open a stream to an externalized resource representing
+     * the flow definition, for example.
+     * 
+     * @throws FlowBuilderException an exception occurred building the flow
+     */
+    protected void doInit() throws FlowBuilderException {
+        try {
+            flowModel = flowModelHolder.getFlowModel();
+            initLocalFlowContext();
+        } catch (FlowModelBuilderException e) {
+            throw new FlowBuilderException("Unable to get the model for this flow", e);
+        }
+        if ("true".equals(flowModel.getAbstract())) {
+            throw new FlowBuilderException("Abstract flow models cannot be instantiated.");
+        }
+    }
+
+    protected Flow createFlow() {
+        String flowId = getContext().getFlowId();
+        AttributeMap<Object> flowAttributes = parseFlowMetaAttributes(flowModel);
+        flowAttributes = getContext().getFlowAttributes().union(flowAttributes);
+        if (IS_SPRING_FACES_PRESENT) {
+            flowAttributes.asMap().put(VALIDATOR_FLOW_ATTR, getLocalContext().getValidator());
+            flowAttributes.asMap().put(VALIDATION_HINT_RESOLVER_FLOW_ATTR,
+                    getLocalContext().getValidationHintResolver());
+        }
+        Flow flow = getLocalContext().getFlowArtifactFactory().createFlow(flowId, flowAttributes);
+        flow.setApplicationContext(getLocalContext().getApplicationContext());
+        return flow;
+    }
+
+    /**
+     * Builds any variables initialized by the flow when it starts.
+     * 
+     * @throws FlowBuilderException an exception occurred building the flow
+     */
+    public void buildVariables() throws FlowBuilderException {
+        if (flowModel.getVars() != null) {
+            for (VarModel varModel : flowModel.getVars()) {
+                getFlow().addVariable(parseFlowVariable(varModel));
+            }
+        }
+    }
+
+    /**
+     * Builds the input mapper responsible for mapping flow input on start.
+     * 
+     * @throws FlowBuilderException an exception occurred building the flow
+     */
+    public void buildInputMapper() throws FlowBuilderException {
+        getFlow().setInputMapper(parseFlowInputMapper(flowModel.getInputs()));
+    }
+
+    /**
+     * Builds any start actions to execute when the flow starts.
+     * 
+     * @throws FlowBuilderException an exception occurred building the flow
+     */
+    public void buildStartActions() throws FlowBuilderException {
+        getFlow().getStartActionList().addAll(parseActions(flowModel.getOnStartActions()));
+    }
+
+    /**
+     * Builds the states of the flow.
+     * 
+     * @throws FlowBuilderException an exception occurred building the flow
+     */
+    public void buildStates() throws FlowBuilderException {
+        if (flowModel.getStates() == null) {
+            throw new FlowBuilderException("At least one state is required to build a Flow");
+        }
+        for (AbstractStateModel state : flowModel.getStates()) {
+            if (state instanceof ActionStateModel) {
+                parseAndAddActionState((ActionStateModel) state, getFlow());
+            } else if (state instanceof ViewStateModel) {
+                parseAndAddViewState((ViewStateModel) state, getFlow());
+            } else if (state instanceof DecisionStateModel) {
+                parseAndAddDecisionState((DecisionStateModel) state, getFlow());
+            } else if (state instanceof SubflowStateModel) {
+                parseAndAddSubflowState((SubflowStateModel) state, getFlow());
+            } else if (state instanceof EndStateModel) {
+                parseAndAddEndState((EndStateModel) state, getFlow());
+            }
+        }
+        if (flowModel.getStartStateId() != null) {
+            getFlow().setStartState(flowModel.getStartStateId());
+        }
+    }
+
+    /**
+     * Builds any transitions shared by all states of the flow.
+     * 
+     * @throws FlowBuilderException an exception occurred building the flow
+     */
+    public void buildGlobalTransitions() throws FlowBuilderException {
+        getFlow().getGlobalTransitionSet().addAll(parseTransitions(flowModel.getGlobalTransitions()));
+    }
+
+    /**
+     * Builds any end actions to execute when the flow ends.
+     * 
+     * @throws FlowBuilderException an exception occurred building the flow
+     */
+    public void buildEndActions() throws FlowBuilderException {
+        getFlow().getEndActionList().addAll(parseActions(flowModel.getOnEndActions()));
+    }
+
+    /**
+     * Builds the output mapper responsible for mapping flow output on end.
+     * 
+     * @throws FlowBuilderException an exception occurred building the flow
+     */
+    public void buildOutputMapper() throws FlowBuilderException {
+        if (flowModel.getOutputs() != null) {
+            getFlow().setOutputMapper(parseFlowOutputMapper(flowModel.getOutputs()));
+        }
+    }
+
+    /**
+     * Creates and adds all exception handlers to the flow built by this builder.
+     * 
+     * @throws FlowBuilderException an exception occurred building this flow
+     */
+    public void buildExceptionHandlers() throws FlowBuilderException {
+        getFlow().getExceptionHandlerSet()
+                .addAll(parseExceptionHandlers(flowModel.getExceptionHandlers(), flowModel.getGlobalTransitions()));
+    }
+
+    public boolean hasFlowChanged() {
+        return flowModelHolder.hasFlowModelChanged();
+    }
+
+    public String getFlowResourceString() {
+        return flowModelHolder.getFlowModelResource().getDescription();
+    }
+
+    /**
+     * Shutdown the builder, releasing any resources it holds. A new flow construction process should start with another
+     * call to the {@link #init(FlowBuilderContext)} method.
+     * 
+     * @throws FlowBuilderException an exception occurred building this flow
+     */
+    protected void doDispose() throws FlowBuilderException {
+        flowModel = null;
+        setLocalContext(null);
+    }
+
+    // subclassing hooks
+
+    protected FlowModel getFlowModel() {
+        return flowModel;
+    }
+
+    protected CustomLocalFlowBuilderContext getLocalContext() {
+        return localFlowBuilderContext;
+    }
+
+    protected void setLocalContext(CustomLocalFlowBuilderContext localFlowBuilderContext) {
+        this.localFlowBuilderContext = localFlowBuilderContext;
+    }
+
+    /**
+     * Register beans in the bean factory local to the flow definition being built.
+     * <p>
+     * Subclasses may override this method to customize the population of the bean factory local to the flow definition
+     * being built; for example, to register mock implementations of services in a test environment.
+     * 
+     * @param beanFactory the bean factory; register local beans with it using
+     *            {@link ConfigurableBeanFactory#registerSingleton(String, Object)}
+     */
+    protected void registerFlowBeans(ConfigurableBeanFactory beanFactory) {
+    }
+
+    // internal helpers
+
+    private void initLocalFlowContext() {
+        Resource[] contextResources = parseContextResources(getFlowModel().getBeanImports());
+        GenericApplicationContext flowContext = createFlowApplicationContext(contextResources);
+        setLocalContext(new CustomLocalFlowBuilderContext(getContext(), flowContext));
+    }
+
+    private Resource[] parseContextResources(List<BeanImportModel> beanImports) {
+        if (beanImports != null && !beanImports.isEmpty()) {
+            Resource flowResource = flowModelHolder.getFlowModelResource();
+            List<Resource> resources = new ArrayList<>(beanImports.size());
+            for (BeanImportModel beanImport : getFlowModel().getBeanImports()) {
+                try {
+                    resources.add(flowResource.createRelative(beanImport.getResource()));
+                } catch (IOException e) {
+                    throw new FlowBuilderException(
+                            "Could not access flow-relative artifact resource '" + beanImport.getResource() + "'", e);
+                }
+            }
+            return resources.toArray(new Resource[resources.size()]);
+        } else {
+            return new Resource[0];
+        }
+    }
+
+    private GenericApplicationContext createFlowApplicationContext(Resource[] resources) {
+        ApplicationContext parent = getContext().getApplicationContext();
+        GenericApplicationContext flowContext;
+        if (parent instanceof WebApplicationContext) {
+            GenericWebApplicationContext webContext = new GenericWebApplicationContext();
+            webContext.setServletContext(((WebApplicationContext) parent).getServletContext());
+            flowContext = webContext;
+        } else {
+            flowContext = new GenericApplicationContext();
+        }
+        //HERE is the setting of the property placeholder prefix.
+        flowContext.getEnvironment().setPlaceholderPrefix("%{");
+        flowContext.getEnvironment().setPlaceholderSuffix("}");
+        flowContext.setDisplayName("Flow ApplicationContext [" + getContext().getFlowId() + "]");
+        flowContext.setParent(parent);
+        flowContext.getBeanFactory().registerScope("request", new RequestScope());
+        flowContext.getBeanFactory().registerScope("flash", new FlashScope());
+        flowContext.getBeanFactory().registerScope("view", new ViewScope());
+        flowContext.getBeanFactory().registerScope("flow", new FlowScope());
+        flowContext.getBeanFactory().registerScope("conversation", new ConversationScope());
+        
+        flowContext.getDefaultListableBeanFactory().setParameterNameDiscoverer(new AnnotationParameterNameDiscoverer());
+
+        // Ensure the current ClassLoader is used, or otherwise setting the ResourceLoader would suppress it
+        ClassLoader classLoaderToUse = flowContext.getClassLoader();
+        flowContext.setClassLoader(classLoaderToUse);
+
+        Resource flowResource = flowModelHolder.getFlowModelResource();
+        CustomFlowRelativeResourceLoader loader = new CustomFlowRelativeResourceLoader(flowResource);
+        loader.addProtocolResolver(new ConditionalResourceResolver());
+        flowContext.setResourceLoader(loader);
+        
+
+        AnnotationConfigUtils.registerAnnotationConfigProcessors(flowContext);
+        new XmlBeanDefinitionReader(flowContext).loadBeanDefinitions(resources);
+        registerFlowBeans(flowContext.getBeanFactory());
+        registerMessageSource(flowContext, flowResource);
+
+        flowContext.refresh();
+
+        return flowContext;
+    }
+
+    private boolean isFlowInDevelopment() {
+        return getContext().getFlowAttributes().getBoolean("development", false);
+    }
+
+    private void registerMessageSource(GenericApplicationContext flowContext, Resource flowResource) {
+        boolean localMessageSourcePresent =
+                flowContext.containsLocalBean(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME);
+        if (!localMessageSourcePresent) {
+            Resource messageBundle;
+            try {
+                messageBundle = flowResource.createRelative("messages.properties");
+            } catch (IOException e) {
+                messageBundle = null;
+            }
+            if (messageBundle != null && messageBundle.exists()) {
+                BeanDefinitionBuilder builder =
+                        BeanDefinitionBuilder.rootBeanDefinition(ReloadableResourceBundleMessageSource.class);
+                builder.addPropertyValue("basename", "messages");
+                if (isFlowInDevelopment()) {
+                    builder.addPropertyValue("cacheSeconds", "0");
+                }
+                flowContext.registerBeanDefinition(AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME,
+                        builder.getBeanDefinition());
+            }
+        }
+    }
+
+    private AttributeMap<Object> parseFlowMetaAttributes(FlowModel flow) {
+        MutableAttributeMap<Object> flowAttributes = parseMetaAttributes(flow.getAttributes());
+        parseAndPutPersistenceContext(flow.getPersistenceContext(), flowAttributes);
+        parseAndPutSecured(flow.getSecured(), flowAttributes);
+        return flowAttributes;
+    }
+
+    private FlowVariable parseFlowVariable(VarModel var) {
+        Class<?> clazz = toClass(var.getClassName());
+        VariableValueFactory valueFactory = new BeanFactoryVariableValueFactory(clazz,
+                getFlow().getApplicationContext().getAutowireCapableBeanFactory());
+        return new FlowVariable(var.getName(), valueFactory);
+    }
+
+    private Mapper parseFlowInputMapper(List<InputModel> inputs) {
+        if (inputs != null && !inputs.isEmpty()) {
+            DefaultMapper inputMapper = new DefaultMapper();
+            for (InputModel inputModel : inputs) {
+                inputMapper.addMapping(parseFlowInputMapping(inputModel));
+            }
+            return inputMapper;
+        } else {
+            return null;
+        }
+    }
+
+    private DefaultMapping parseFlowInputMapping(InputModel input) {
+        ExpressionParser parser = getLocalContext().getExpressionParser();
+        String name = input.getName();
+        String value = null;
+        if (StringUtils.hasText(input.getValue())) {
+            value = input.getValue();
+        } else {
+            value = "flowScope." + name;
+        }
+        Expression source = parser.parseExpression(name, new FluentParserContext().evaluate(MutableAttributeMap.class));
+        Expression target = parser.parseExpression(value, new FluentParserContext().evaluate(RequestContext.class));
+        DefaultMapping mapping = new DefaultMapping(source, target);
+        parseAndSetMappingConversionExecutor(input, mapping);
+        parseAndSetMappingRequired(input, mapping);
+        return mapping;
+    }
+
+    private Mapper parseSubflowInputMapper(List<InputModel> inputs) {
+        if (inputs != null && !inputs.isEmpty()) {
+            DefaultMapper inputMapper = new DefaultMapper();
+            for (InputModel inputModel : inputs) {
+                inputMapper.addMapping(parseSubflowInputMapping(inputModel));
+            }
+            return inputMapper;
+        } else {
+            return null;
+        }
+    }
+
+    private DefaultMapping parseSubflowInputMapping(InputModel input) {
+        ExpressionParser parser = getLocalContext().getExpressionParser();
+        String name = input.getName();
+        String value = null;
+        if (StringUtils.hasText(input.getValue())) {
+            value = input.getValue();
+        } else {
+            value = name;
+        }
+        Expression source = parser.parseExpression(value, new FluentParserContext().evaluate(RequestContext.class));
+        Expression target = parser.parseExpression(name, new FluentParserContext().evaluate(MutableAttributeMap.class));
+        DefaultMapping mapping = new DefaultMapping(source, target);
+        parseAndSetMappingConversionExecutor(input, mapping);
+        parseAndSetMappingRequired(input, mapping);
+        return mapping;
+    }
+
+    private Mapper parseFlowOutputMapper(List<OutputModel> outputs) {
+        if (outputs != null && !outputs.isEmpty()) {
+            DefaultMapper outputMapper = new DefaultMapper();
+            for (OutputModel outputModel : outputs) {
+                outputMapper.addMapping(parseFlowOutputMapping(outputModel));
+            }
+            return outputMapper;
+        } else {
+            return null;
+        }
+    }
+
+    private DefaultMapping parseFlowOutputMapping(OutputModel output) {
+        ExpressionParser parser = getLocalContext().getExpressionParser();
+        String name = output.getName();
+        String value = null;
+        if (StringUtils.hasText(output.getValue())) {
+            value = output.getValue();
+        } else {
+            value = name;
+        }
+        Expression source = parser.parseExpression(value, new FluentParserContext().evaluate(RequestContext.class));
+        Expression target = parser.parseExpression(name, new FluentParserContext().evaluate(MutableAttributeMap.class));
+        DefaultMapping mapping = new DefaultMapping(source, target);
+        parseAndSetMappingConversionExecutor(output, mapping);
+        parseAndSetMappingRequired(output, mapping);
+        return mapping;
+    }
+
+    private Mapper parseSubflowOutputMapper(List<OutputModel> outputs) {
+        if (outputs != null && !outputs.isEmpty()) {
+            DefaultMapper outputMapper = new DefaultMapper();
+            for (OutputModel outputModel : outputs) {
+                outputMapper.addMapping(parseSubflowOutputMapping(outputModel));
+            }
+            return outputMapper;
+        } else {
+            return null;
+        }
+    }
+
+    private DefaultMapping parseSubflowOutputMapping(OutputModel output) {
+        ExpressionParser parser = getLocalContext().getExpressionParser();
+        String name = output.getName();
+        String value = null;
+        if (StringUtils.hasText(output.getValue())) {
+            value = output.getValue();
+        } else {
+            value = "flowScope." + name;
+        }
+        Expression source = parser.parseExpression(name, new FluentParserContext().evaluate(MutableAttributeMap.class));
+        Expression target = parser.parseExpression(value, new FluentParserContext().evaluate(RequestContext.class));
+        DefaultMapping mapping = new DefaultMapping(source, target);
+        parseAndSetMappingConversionExecutor(output, mapping);
+        parseAndSetMappingRequired(output, mapping);
+        return mapping;
+    }
+
+    private void parseAndSetMappingConversionExecutor(AbstractMappingModel mappingModel, DefaultMapping mapping) {
+        if (StringUtils.hasText(mappingModel.getType())) {
+            Class<?> type = toClass(mappingModel.getType());
+            ConversionExecutor typeConverter =
+                    new RuntimeBindingConversionExecutor(type, getLocalContext().getConversionService());
+            mapping.setTypeConverter(typeConverter);
+        }
+    }
+
+    private void parseAndSetMappingRequired(AbstractMappingModel mappingModel, DefaultMapping mapping) {
+        if (StringUtils.hasText(mappingModel.getRequired())) {
+            boolean required = ((Boolean) fromStringTo(Boolean.class).execute(mappingModel.getRequired()));
+            mapping.setRequired(required);
+        }
+    }
+
+    private void parseAndAddViewState(ViewStateModel state, Flow flow) {
+        ViewFactory viewFactory = parseViewFactory(state.getView(), state.getId(), false, state.getBinder());
+        Boolean redirect = null;
+        if (StringUtils.hasText(state.getRedirect())) {
+            redirect = (Boolean) fromStringTo(Boolean.class).execute(state.getRedirect());
+        }
+        boolean popup = false;
+        if (StringUtils.hasText(state.getPopup())) {
+            popup = ((Boolean) fromStringTo(Boolean.class).execute(state.getPopup()));
+        }
+        MutableAttributeMap<Object> attributes = parseMetaAttributes(state.getAttributes());
+        if (state.getModel() != null) {
+            attributes.put("model", getLocalContext().getExpressionParser().parseExpression(state.getModel(),
+                    new FluentParserContext().evaluate(RequestContext.class)));
+        }
+        if (state.getValidationHints() != null) {
+            attributes.put("validationHints", getLocalContext().getExpressionParser().parseExpression(
+                    state.getValidationHints(), new FluentParserContext().evaluate(RequestContext.class)));
+        }
+        parseAndPutSecured(state.getSecured(), attributes);
+        getLocalContext().getFlowArtifactFactory().createViewState(state.getId(), flow,
+                parseViewVariables(state.getVars()), parseActions(state.getOnEntryActions()), viewFactory, redirect,
+                popup, parseActions(state.getOnRenderActions()), parseTransitions(state.getTransitions()),
+                parseExceptionHandlers(state.getExceptionHandlers(), state.getTransitions()),
+                parseActions(state.getOnExitActions()), attributes);
+    }
+
+    private void parseAndAddActionState(ActionStateModel state, Flow flow) {
+        MutableAttributeMap<Object> attributes = parseMetaAttributes(state.getAttributes());
+        parseAndPutSecured(state.getSecured(), attributes);
+        getLocalContext().getFlowArtifactFactory().createActionState(state.getId(), flow,
+                parseActions(state.getOnEntryActions()), parseActions(state.getActions()),
+                parseTransitions(state.getTransitions()),
+                parseExceptionHandlers(state.getExceptionHandlers(), state.getTransitions()),
+                parseActions(state.getOnExitActions()), attributes);
+    }
+
+    private void parseAndAddDecisionState(DecisionStateModel state, Flow flow) {
+        MutableAttributeMap<Object> attributes = parseMetaAttributes(state.getAttributes());
+        parseAndPutSecured(state.getSecured(), attributes);
+        getLocalContext().getFlowArtifactFactory().createDecisionState(state.getId(), flow,
+                parseActions(state.getOnEntryActions()), parseIfs(state.getIfs()),
+                parseExceptionHandlers(state.getExceptionHandlers(), null), parseActions(state.getOnExitActions()),
+                attributes);
+    }
+
+    private void parseAndAddSubflowState(SubflowStateModel state, Flow flow) {
+        MutableAttributeMap<Object> attributes = parseMetaAttributes(state.getAttributes());
+        parseAndPutSecured(state.getSecured(), attributes);
+        getLocalContext().getFlowArtifactFactory().createSubflowState(state.getId(), flow,
+                parseActions(state.getOnEntryActions()), parseSubflowExpression(state.getSubflow()),
+                parseSubflowAttributeMapper(state), parseTransitions(state.getTransitions()),
+                parseExceptionHandlers(state.getExceptionHandlers(), state.getTransitions()),
+                parseActions(state.getOnExitActions()), attributes);
+    }
+
+    private void parseAndAddEndState(EndStateModel state, Flow flow) {
+        MutableAttributeMap<Object> attributes = parseMetaAttributes(state.getAttributes());
+        if (StringUtils.hasText(state.getCommit())) {
+            attributes.put("commit", fromStringTo(Boolean.class).execute(state.getCommit()));
+        }
+        parseAndPutSecured(state.getSecured(), attributes);
+        Action finalResponseAction;
+        ViewFactory viewFactory = parseViewFactory(state.getView(), state.getId(), true, null);
+        if (viewFactory != null) {
+            finalResponseAction = new ViewFactoryActionAdapter(viewFactory);
+        } else {
+            finalResponseAction = null;
+        }
+        getLocalContext().getFlowArtifactFactory().createEndState(state.getId(), flow,
+                parseActions(state.getOnEntryActions()), finalResponseAction, parseFlowOutputMapper(state.getOutputs()),
+                parseExceptionHandlers(state.getExceptionHandlers(), null), attributes);
+    }
+
+    private ViewFactory parseViewFactory(String view, String stateId, boolean endState, BinderModel binderModel) {
+        if (!StringUtils.hasText(view)) {
+            if (endState) {
+                return null;
+            } else {
+                view = getLocalContext().getViewFactoryCreator().getViewIdByConvention(stateId);
+                Expression viewId = getLocalContext().getExpressionParser().parseExpression(view,
+                        new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class));
+                return createViewFactory(viewId, binderModel);
+            }
+        } else if (view.startsWith("externalRedirect:")) {
+            String encodedUrl = view.substring("externalRedirect:".length());
+            Expression externalUrl = getLocalContext().getExpressionParser().parseExpression(encodedUrl,
+                    new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class));
+            return new ActionExecutingViewFactory(new ExternalRedirectAction(externalUrl));
+        } else if (view.startsWith("flowRedirect:")) {
+            String flowRedirect = view.substring("flowRedirect:".length());
+            Expression expression = getLocalContext().getExpressionParser().parseExpression(flowRedirect,
+                    new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class));
+            return new ActionExecutingViewFactory(new FlowDefinitionRedirectAction(expression));
+        } else {
+            Expression viewId = getLocalContext().getExpressionParser().parseExpression(view,
+                    new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class));
+            return createViewFactory(viewId, binderModel);
+        }
+    }
+
+    private ViewFactory createViewFactory(Expression viewId, BinderModel binderModel) {
+        BinderConfiguration binderConfiguration = createBinderConfiguration(binderModel);
+        return getLocalContext().getViewFactoryCreator().createViewFactory(viewId,
+                getLocalContext().getExpressionParser(), getLocalContext().getConversionService(), binderConfiguration,
+                getLocalContext().getValidator(), getLocalContext().getValidationHintResolver());
+    }
+
+    private BinderConfiguration createBinderConfiguration(BinderModel binderModel) {
+        if (binderModel != null && binderModel.getBindings() != null) {
+            BinderConfiguration binderConfiguration = new BinderConfiguration();
+            List<BindingModel> bindings = binderModel.getBindings();
+            for (BindingModel bindingModel : bindings) {
+                boolean required = false;
+                if (StringUtils.hasText(bindingModel.getRequired())) {
+                    required = ((Boolean) fromStringTo(Boolean.class).execute(bindingModel.getRequired()));
+                }
+                Binding binding = new Binding(bindingModel.getProperty(), bindingModel.getConverter(), required);
+                binderConfiguration.addBinding(binding);
+            }
+            return binderConfiguration;
+        } else {
+            return null;
+        }
+    }
+
+    private ViewVariable[] parseViewVariables(List<VarModel> vars) {
+        if (vars != null && !vars.isEmpty()) {
+            List<ViewVariable> variables = new ArrayList<>(vars.size());
+            for (VarModel varModel : vars) {
+                variables.add(parseViewVariable(varModel));
+            }
+            return variables.toArray(new ViewVariable[variables.size()]);
+        } else {
+            return new ViewVariable[0];
+        }
+    }
+
+    private ViewVariable parseViewVariable(VarModel var) {
+        Class<?> clazz = toClass(var.getClassName());
+        VariableValueFactory valueFactory = new BeanFactoryVariableValueFactory(clazz,
+                getFlow().getApplicationContext().getAutowireCapableBeanFactory());
+        return new ViewVariable(var.getName(), valueFactory);
+    }
+
+    private Transition[] parseIfs(List<IfModel> ifModels) {
+        if (ifModels != null && !ifModels.isEmpty()) {
+            List<Transition> transitions = new ArrayList<>(ifModels.size());
+            for (IfModel ifModel : ifModels) {
+                transitions.addAll(Arrays.asList(parseIf(ifModel)));
+            }
+            return transitions.toArray(new Transition[transitions.size()]);
+        } else {
+            return new Transition[0];
+        }
+    }
+
+    private Transition[] parseIf(IfModel ifModel) {
+        Transition thenTransition = parseThen(ifModel);
+        if (StringUtils.hasText(ifModel.getElse())) {
+            Transition elseTransition = parseElse(ifModel);
+            return new Transition[] {thenTransition, elseTransition};
+        } else {
+            return new Transition[] {thenTransition};
+        }
+    }
+
+    private Transition parseThen(IfModel ifModel) {
+        Expression test = getLocalContext().getExpressionParser().parseExpression(ifModel.getTest(),
+                new FluentParserContext().evaluate(RequestContext.class).expectResult(Boolean.class));
+        TransitionCriteria matchingCriteria = new DefaultTransitionCriteria(test);
+        TargetStateResolver targetStateResolver =
+                (TargetStateResolver) fromStringTo(TargetStateResolver.class).execute(ifModel.getThen());
+        return getLocalContext().getFlowArtifactFactory().createTransition(targetStateResolver, matchingCriteria, null,
+                null);
+    }
+
+    private Transition parseElse(IfModel ifModel) {
+        TargetStateResolver stateResolver =
+                (TargetStateResolver) fromStringTo(TargetStateResolver.class).execute(ifModel.getElse());
+        return getLocalContext().getFlowArtifactFactory().createTransition(stateResolver, null, null, null);
+    }
+
+    private Expression parseSubflowExpression(String subflow) {
+        Expression subflowId = getLocalContext().getExpressionParser().parseExpression(subflow,
+                new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class));
+        return new CustomSubflowExpression(subflowId, getLocalContext().getFlowDefinitionLocator());
+    }
+
+    private SubflowAttributeMapper parseSubflowAttributeMapper(SubflowStateModel state) {
+        if (StringUtils.hasText(state.getSubflowAttributeMapper())) {
+            String beanId = state.getSubflowAttributeMapper();
+            return getLocalContext().getApplicationContext().getBean(beanId, SubflowAttributeMapper.class);
+        } else {
+            Mapper inputMapper = parseSubflowInputMapper(state.getInputs());
+            Mapper outputMapper = parseSubflowOutputMapper(state.getOutputs());
+            return new GenericSubflowAttributeMapper(inputMapper, outputMapper);
+        }
+    }
+
+    private FlowExecutionExceptionHandler[] parseExceptionHandlers(List<ExceptionHandlerModel> modelExceptionHandlers,
+            List<TransitionModel> modelTransitions) {
+        FlowExecutionExceptionHandler[] transitionExecutingHandlers =
+                parseTransitionExecutingExceptionHandlers(modelTransitions);
+        FlowExecutionExceptionHandler[] customHandlers = parseCustomExceptionHandlers(modelExceptionHandlers);
+        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(List<TransitionModel> transitionModels) {
+        if (transitionModels != null && !transitionModels.isEmpty()) {
+            List<FlowExecutionExceptionHandler> exceptionHandlers = new ArrayList<>(transitionModels.size());
+            for (TransitionModel model : transitionModels) {
+                if (StringUtils.hasText(model.getOnException())) {
+                    if (model.getSecured() != null) {
+                        throw new FlowBuilderException("Exception based transitions cannot be secured");
+                    }
+                    exceptionHandlers.add(parseTransitionExecutingExceptionHandler(model));
+                }
+            }
+            return exceptionHandlers.toArray(new FlowExecutionExceptionHandler[exceptionHandlers.size()]);
+        } else {
+            return new FlowExecutionExceptionHandler[0];
+        }
+    }
+
+    private FlowExecutionExceptionHandler parseTransitionExecutingExceptionHandler(TransitionModel transition) {
+        TransitionExecutingFlowExecutionExceptionHandler handler =
+                new TransitionExecutingFlowExecutionExceptionHandler();
+        Class<Throwable> exceptionClass = toClass(transition.getOnException(), Throwable.class);
+        TargetStateResolver targetStateResolver =
+                (TargetStateResolver) fromStringTo(TargetStateResolver.class).execute(transition.getTo());
+        handler.add(exceptionClass, targetStateResolver);
+        handler.getActionList().addAll(parseActions(transition.getActions()));
+        return handler;
+    }
+
+    private FlowExecutionExceptionHandler[]
+            parseCustomExceptionHandlers(List<ExceptionHandlerModel> exceptionHandlerModels) {
+        if (exceptionHandlerModels != null && !exceptionHandlerModels.isEmpty()) {
+            List<FlowExecutionExceptionHandler> exceptionHandlers = new ArrayList<>(exceptionHandlerModels.size());
+            for (ExceptionHandlerModel exceptionHandlerModel : exceptionHandlerModels) {
+                exceptionHandlers.add(parseCustomExceptionHandler(exceptionHandlerModel));
+            }
+            return exceptionHandlers.toArray(new FlowExecutionExceptionHandler[exceptionHandlers.size()]);
+        } else {
+            return new FlowExecutionExceptionHandler[0];
+        }
+    }
+
+    private FlowExecutionExceptionHandler parseCustomExceptionHandler(ExceptionHandlerModel exceptionHandler) {
+        return getLocalContext().getApplicationContext().getBean(exceptionHandler.getBean(),
+                FlowExecutionExceptionHandler.class);
+    }
+
+    private Transition[] parseTransitions(List<TransitionModel> transitionModels) {
+        if (transitionModels != null && !transitionModels.isEmpty()) {
+            List<Transition> transitions = new ArrayList<>(transitionModels.size());
+            if (transitionModels != null) {
+                for (TransitionModel transition : transitionModels) {
+                    if (!StringUtils.hasText(transition.getOnException())) {
+                        transitions.add(parseTransition(transition));
+                    }
+                }
+            }
+            return transitions.toArray(new Transition[transitions.size()]);
+        } else {
+            return new Transition[0];
+        }
+    }
+
+    private Transition parseTransition(TransitionModel transition) {
+        TransitionCriteria matchingCriteria =
+                (TransitionCriteria) fromStringTo(TransitionCriteria.class).execute(transition.getOn());
+        TargetStateResolver stateResolver =
+                (TargetStateResolver) fromStringTo(TargetStateResolver.class).execute(transition.getTo());
+        TransitionCriteria executionCriteria =
+                TransitionCriteriaChain.criteriaChainFor(parseActions(transition.getActions()));
+        MutableAttributeMap<Object> attributes = parseMetaAttributes(transition.getAttributes());
+        if (StringUtils.hasText(transition.getBind())) {
+            attributes.put("bind", fromStringTo(Boolean.class).execute(transition.getBind()));
+        }
+        if (StringUtils.hasText(transition.getValidate())) {
+            attributes.put("validate", fromStringTo(Boolean.class).execute(transition.getValidate()));
+        }
+        if (StringUtils.hasText(transition.getValidationHints())) {
+            attributes.put("validationHints", getLocalContext().getExpressionParser().parseExpression(
+                    transition.getValidationHints(), new FluentParserContext().evaluate(RequestContext.class)));
+        }
+        if (StringUtils.hasText(transition.getHistory())) {
+            attributes.put("history", fromStringTo(History.class).execute(transition.getHistory().toUpperCase()));
+        }
+        parseAndPutSecured(transition.getSecured(), attributes);
+        return getLocalContext().getFlowArtifactFactory().createTransition(stateResolver, matchingCriteria,
+                executionCriteria, attributes);
+    }
+
+    private Action[] parseActions(List<AbstractActionModel> actionModels) {
+        if (actionModels != null && !actionModels.isEmpty()) {
+            List<AnnotatedAction> actions = new ArrayList<>(actionModels.size());
+            for (AbstractActionModel actionModel : actionModels) {
+                Action action;
+                if (actionModel instanceof EvaluateModel) {
+                    action = parseEvaluateAction((EvaluateModel) actionModel);
+                } else if (actionModel instanceof RenderModel) {
+                    action = parseRenderAction((RenderModel) actionModel);
+                } else if (actionModel instanceof SetModel) {
+                    action = parseSetAction((SetModel) actionModel);
+                } else {
+                    action = null;
+                }
+                if (action != null) {
+                    AnnotatedAction annotatedAction = new AnnotatedAction(action);
+                    annotatedAction.getAttributes().putAll(parseMetaAttributes(actionModel.getAttributes()));
+                    actions.add(annotatedAction);
+                }
+            }
+            return actions.toArray(new Action[actions.size()]);
+        } else {
+            return new Action[0];
+        }
+    }
+
+    private Action parseEvaluateAction(EvaluateModel evaluate) {
+        FluentParserContext evaluateExpressionParserContext = new FluentParserContext().evaluate(RequestContext.class);
+        if (StringUtils.hasText(evaluate.getResultType())) {
+            evaluateExpressionParserContext.expectResult(toClass(evaluate.getResultType()));
+        }
+        Expression evaluateExpression = getLocalContext().getExpressionParser()
+                .parseExpression(evaluate.getExpression(), evaluateExpressionParserContext);
+        Expression resultExpression = null;
+        if (StringUtils.hasText(evaluate.getResult())) {
+            resultExpression = getLocalContext().getExpressionParser().parseExpression(evaluate.getResult(),
+                    new FluentParserContext().evaluate(RequestContext.class));
+        }
+        return new EvaluateAction(evaluateExpression, resultExpression);
+    }
+
+    private Action parseRenderAction(RenderModel render) {
+        String[] fragmentExpressionStrings = StringUtils.commaDelimitedListToStringArray(render.getFragments());
+        fragmentExpressionStrings = StringUtils.trimArrayElements(fragmentExpressionStrings);
+        ParserContext context =
+                new FluentParserContext().template().evaluate(RequestContext.class).expectResult(String.class);
+        Expression[] fragments = new Expression[fragmentExpressionStrings.length];
+        for (int i = 0; i < fragmentExpressionStrings.length; i++) {
+            String fragment = fragmentExpressionStrings[i];
+            fragments[i] = getLocalContext().getExpressionParser().parseExpression(fragment, context);
+        }
+        return new RenderAction(fragments);
+    }
+
+    private Action parseSetAction(SetModel set) {
+        Expression nameExpression = getLocalContext().getExpressionParser().parseExpression(set.getName(),
+                new FluentParserContext().evaluate(RequestContext.class));
+        FluentParserContext valueParserContext = new FluentParserContext().evaluate(RequestContext.class);
+        if (StringUtils.hasText(set.getType())) {
+            valueParserContext.expectResult(toClass(set.getType()));
+        }
+        Expression valueExpression =
+                getLocalContext().getExpressionParser().parseExpression(set.getValue(), valueParserContext);
+        return new SetAction(nameExpression, valueExpression);
+    }
+
+    private MutableAttributeMap<Object> parseMetaAttributes(List<AttributeModel> attributeModels) {
+        if (attributeModels != null && !attributeModels.isEmpty()) {
+            LocalAttributeMap<Object> attributes = new LocalAttributeMap<>();
+            for (AttributeModel attributeModel : attributeModels) {
+                parseAndPutMetaAttribute(attributeModel, attributes);
+            }
+            return attributes;
+        } else {
+            return new LocalAttributeMap<>();
+        }
+    }
+
+    private void parseAndPutMetaAttribute(AttributeModel attribute, MutableAttributeMap<Object> attributes) {
+        String name = attribute.getName();
+        String value = attribute.getValue();
+        attributes.put(name, parseAttributeValueIfNecessary(attribute, value));
+    }
+
+    private Object parseAttributeValueIfNecessary(AttributeModel attribute, String stringValue) {
+        if (StringUtils.hasText(attribute.getType())) {
+            Class<?> targetClass = toClass(attribute.getType());
+            return fromStringTo(targetClass).execute(stringValue);
+        } else {
+            return stringValue;
+        }
+    }
+
+    private void parseAndPutPersistenceContext(PersistenceContextModel persistenceContext,
+            MutableAttributeMap<Object> attributes) {
+        if (persistenceContext != null) {
+            attributes.put("persistenceContext", true);
+        }
+    }
+
+    private void parseAndPutSecured(SecuredModel secured, MutableAttributeMap<Object> attributes) {
+        if (secured != null) {
+            SecurityRule rule = new SecurityRule();
+            rule.setAttributes(SecurityRule.commaDelimitedListToSecurityAttributes(secured.getAttributes()));
+            String comparisonType = secured.getMatch();
+            if ("any".equals(comparisonType)) {
+                rule.setComparisonType(SecurityRule.COMPARISON_ANY);
+            } else if ("all".equals(comparisonType)) {
+                rule.setComparisonType(SecurityRule.COMPARISON_ALL);
+            } else {
+                // default to any
+                rule.setComparisonType(SecurityRule.COMPARISON_ANY);
+            }
+            attributes.put(SecurityRule.SECURITY_ATTRIBUTE_NAME, rule);
+        }
+    }
+
+    private ConversionExecutor fromStringTo(Class<?> targetType) throws ConversionExecutionException {
+        return getLocalContext().getConversionService().getConversionExecutor(String.class, targetType);
+    }
+
+    private Class<?> toClass(String name) {
+        Class<?> clazz = getLocalContext().getConversionService().getClassForAlias(name);
+        if (clazz != null) {
+            return clazz;
+        } else {
+            try {
+                ClassLoader classLoader = getLocalContext().getApplicationContext().getClassLoader();
+                return ClassUtils.forName(name, classLoader);
+            } catch (ClassNotFoundException e) {
+                throw new IllegalArgumentException("Unable to load class '" + name + "'");
+            }
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private <T> Class<T> toClass(String name, Class<T> superType) {
+        Class<?> clazz = toClass(name);
+        Assert.isAssignable(superType, clazz);
+        return (Class<T>) clazz;
+    }
+
+    public String toString() {
+        return new ToStringCreator(this).append("flowModelResource", flowModelHolder.getFlowModelResource()).toString();
+    }
+
+}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomFlowRelativeResourceLoader.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomFlowRelativeResourceLoader.java
new file mode 100644
index 0000000..4ef42d4
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomFlowRelativeResourceLoader.java
@@ -0,0 +1,81 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.shibboleth.idp.plugin.authn.test.spring;
+
+import java.io.IOException;
+import java.util.LinkedHashSet;
+import java.util.Set;
+
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.ProtocolResolver;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.util.Assert;
+
+/** 
+ * Custom FlowRelativeResourceLoader which allows the addition of protocol resolvers. 
+ * Of particular importance allowing {@literal conditional:} resources.
+ */
+public class CustomFlowRelativeResourceLoader implements ResourceLoader {
+    
+    private final Set<ProtocolResolver> protocolResolvers = new LinkedHashSet<>(4);
+
+    private Resource flowResource;
+    
+    public void addProtocolResolver(ProtocolResolver resolver) {
+        Assert.notNull(resolver, "ProtocolResolver must not be null");
+        this.protocolResolvers.add(resolver);
+    }
+
+    public CustomFlowRelativeResourceLoader(Resource resource) {
+        this.flowResource = resource;
+    }
+
+    public ClassLoader getClassLoader() {
+        return flowResource.getClass().getClassLoader();
+    }
+
+    public Resource getResource(String location) {
+        Assert.notNull(location, "Location must not be null");
+
+        for (ProtocolResolver protocolResolver : protocolResolvers) {
+            Resource resource = protocolResolver.resolve(location, this);
+            if (resource != null) {
+                return resource;
+            }
+        }
+
+        //maintain old behaviour for classpath resources
+        if (location.startsWith(CLASSPATH_URL_PREFIX)) {
+            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
+        } else {
+            return createFlowRelativeResource(location);
+        }
+    }
+
+    private Resource createFlowRelativeResource(String location) {
+        try {
+            return flowResource.createRelative(location);
+        } catch (IOException e) {
+            IllegalArgumentException iae = new IllegalArgumentException(
+                    "Unable to access a flow relative resource at location '" + location + "'");
+            iae.initCause(e);
+            throw iae;
+        }
+    }
+
+}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomLocalFlowBuilderContext.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomLocalFlowBuilderContext.java
new file mode 100644
index 0000000..083d611
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomLocalFlowBuilderContext.java
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.shibboleth.idp.plugin.authn.test.spring;
+
+import org.springframework.binding.convert.ConversionService;
+import org.springframework.binding.expression.ExpressionParser;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.validation.Validator;
+import org.springframework.webflow.core.collection.AttributeMap;
+import org.springframework.webflow.definition.registry.FlowDefinitionLocator;
+import org.springframework.webflow.engine.builder.FlowArtifactFactory;
+import org.springframework.webflow.engine.builder.FlowBuilderContext;
+import org.springframework.webflow.engine.builder.ViewFactoryCreator;
+import org.springframework.webflow.validation.ValidationHintResolver;
+
+public class CustomLocalFlowBuilderContext {
+    
+    private FlowBuilderContext parent;
+
+    private ApplicationContext localFlowContext;
+
+    public CustomLocalFlowBuilderContext(FlowBuilderContext parent, GenericApplicationContext localFlowContext) {
+        this.parent = parent;
+        this.localFlowContext = localFlowContext;
+    }
+
+    public ApplicationContext getApplicationContext() {
+        return localFlowContext;
+    }
+
+    public String getFlowId() {
+        return parent.getFlowId();
+    }
+
+    public AttributeMap<Object> getFlowAttributes() {
+        return parent.getFlowAttributes();
+    }
+
+    public FlowDefinitionLocator getFlowDefinitionLocator() {
+        if (localFlowContext.containsLocalBean("flowRegistry")) {
+            return localFlowContext.getBean("flowRegistry", FlowDefinitionLocator.class);
+        } else {
+            return parent.getFlowDefinitionLocator();
+        }
+    }
+
+    public FlowArtifactFactory getFlowArtifactFactory() {
+        if (localFlowContext.containsLocalBean("flowArtifactFactory")) {
+            return localFlowContext.getBean("flowArtifactFactory", FlowArtifactFactory.class);
+        } else {
+            return parent.getFlowArtifactFactory();
+        }
+    }
+
+    public ConversionService getConversionService() {
+        if (localFlowContext.containsLocalBean("conversionService")) {
+            return localFlowContext.getBean("conversionService", ConversionService.class);
+        } else {
+            return parent.getConversionService();
+        }
+    }
+
+    public ViewFactoryCreator getViewFactoryCreator() {
+        if (localFlowContext.containsLocalBean("viewFactoryCreator")) {
+            return localFlowContext.getBean("viewFactoryCreator", ViewFactoryCreator.class);
+        } else {
+            return parent.getViewFactoryCreator();
+        }
+    }
+
+    public ExpressionParser getExpressionParser() {
+        if (localFlowContext.containsLocalBean("expressionParser")) {
+            return localFlowContext.getBean("expressionParser", ExpressionParser.class);
+        } else {
+            return parent.getExpressionParser();
+        }
+    }
+
+    public Validator getValidator() {
+        if (localFlowContext.containsLocalBean("validator")) {
+            return localFlowContext.getBean("validator", Validator.class);
+        } else {
+            return parent.getValidator();
+        }
+    }
+
+    public ValidationHintResolver getValidationHintResolver() {
+        if (localFlowContext.containsLocalBean("validationHintResolver")) {
+            return localFlowContext.getBean("validationHintResolver", ValidationHintResolver.class);
+        } else {
+            return parent.getValidationHintResolver();
+        }
+    }
+
+}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomSubflowExpression.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomSubflowExpression.java
new file mode 100644
index 0000000..1dcdbd6
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/CustomSubflowExpression.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package net.shibboleth.idp.plugin.authn.test.spring;
+
+import org.springframework.binding.expression.EvaluationException;
+import org.springframework.binding.expression.Expression;
+import org.springframework.webflow.definition.registry.FlowDefinitionLocator;
+
+public class CustomSubflowExpression implements Expression {
+
+    private Expression subflowId;
+
+    private FlowDefinitionLocator flowDefinitionLocator;
+
+    public CustomSubflowExpression(Expression subflowId, FlowDefinitionLocator flowDefinitionLocator) {
+        this.subflowId = subflowId;
+        this.flowDefinitionLocator = flowDefinitionLocator;
+    }
+
+    public Object getValue(Object context) throws EvaluationException {
+        String subflowId = (String) this.subflowId.getValue(context);
+        return flowDefinitionLocator.getFlowDefinition(subflowId);
+    }
+
+    public void setValue(Object context, Object value) throws EvaluationException {
+        throw new UnsupportedOperationException("Cannot set a subflow expression");
+    }
+
+    public Class<?> getValueType(Object context) {
+        return null;
+    }
+
+    public String getExpressionString() {
+        return null;
+    }
+
+}
diff --git a/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/package-info.java b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/package-info.java
new file mode 100644
index 0000000..dc50344
--- /dev/null
+++ b/idp-oidc-rp-impl/src/test/java/net/shibboleth/idp/plugin/authn/test/spring/package-info.java
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/** Duo OIDC 2FA login flow custom spring test classes. */
+package net.shibboleth.idp.plugin.authn.test.spring;

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list