diff --git a/spring-webflow/src/main/java/org/springframework/webflow/mvc/MvcViewFactoryCreator.java b/spring-webflow/src/main/java/org/springframework/webflow/mvc/MvcViewFactoryCreator.java index 68911130..103a8b4a 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/mvc/MvcViewFactoryCreator.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/mvc/MvcViewFactoryCreator.java @@ -1,5 +1,7 @@ package org.springframework.webflow.mvc; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; @@ -8,54 +10,137 @@ import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.binding.expression.Expression; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.core.OrderComparator; import org.springframework.core.io.ContextResource; import org.springframework.core.io.ResourceLoader; +import org.springframework.util.ClassUtils; import org.springframework.web.servlet.ViewResolver; +import org.springframework.web.servlet.view.InternalResourceView; +import org.springframework.web.servlet.view.JstlView; import org.springframework.webflow.engine.builder.ViewFactoryCreator; -import org.springframework.webflow.engine.builder.support.ActionInvokingViewFactory; import org.springframework.webflow.execution.Action; import org.springframework.webflow.execution.Event; import org.springframework.webflow.execution.RequestContext; import org.springframework.webflow.execution.View; import org.springframework.webflow.execution.ViewFactory; -public class MvcViewFactoryCreator implements ViewFactoryCreator { +/** + * View factory creator implementation that produces View Factories that create Spring MVC-based views. + * + * This class is used by a flow builder in a Spring MVC environment to configure view factories on flows that render + * Spring MVC-based views. + * + * This class supports rendering views resolved by existing Spring MVC-based resolver infrastructure, or, if no such + * infrastructure is configured, JSP resources relative to the flow definition being built. + * + * @author Keith Donald + */ +public class MvcViewFactoryCreator implements ViewFactoryCreator, ApplicationContextAware { + + private static final boolean jstlPresent = ClassUtils.isPresent("javax.servlet.jsp.jstl.fmt.LocalizationContext"); + + private List viewResolvers; + + private ApplicationContext applicationContext; public Action createFinalResponseAction(Expression viewId, ResourceLoader viewResourceLoader) { - return new TestAction(); + return new ViewFactoryActionAdapter(createViewFactory(viewId, viewResourceLoader)); } public ViewFactory createViewFactory(Expression viewId, ResourceLoader viewResourceLoader) { - return new ActionInvokingViewFactory(new TestAction()); - } - - public class TestAction implements Action { - public Event execute(RequestContext context) throws Exception { - System.out.println("Render me..."); - return new Event(this, "success"); + if (viewResolvers != null) { + return new ViewResolvingMvcViewFactory(viewId, viewResolvers); + } else { + return new InternalFlowResourceMvcViewFactory(viewId, applicationContext, viewResourceLoader); } } - private static class MvcViewFactory implements ViewFactory { - private ResourceLoader viewResourceLoader; + public void setApplicationContext(ApplicationContext context) { + initViewResolvers(context); + this.applicationContext = context; + } + private void initViewResolvers(ApplicationContext context) { + Map matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(context, ViewResolver.class, true, false); + if (!matchingBeans.isEmpty()) { + viewResolvers = new ArrayList(matchingBeans.values()); + Collections.sort(viewResolvers, new OrderComparator()); + } + } + + /** + * View factory implementation that creates a Spring-MVC Internal Resource view to render a flow-relative view + * resource such as a JSP or Velocity template. + * @author Keith Donald + */ + static class InternalFlowResourceMvcViewFactory implements ViewFactory { private Expression viewExpression; + private ApplicationContext applicationContext; + + private ResourceLoader resourceLoader; + + public InternalFlowResourceMvcViewFactory(Expression viewExpression, ApplicationContext context, + ResourceLoader resourceLoader) { + this.viewExpression = viewExpression; + this.applicationContext = context; + this.resourceLoader = resourceLoader; + } + + public View getView(RequestContext context) { + String viewId = (String) viewExpression.getValue(context); + if (viewId.startsWith("/")) { + return getViewInternal(viewId, context); + } else { + ContextResource viewResource = (ContextResource) resourceLoader.getResource(viewId); + return getViewInternal(viewResource.getPathWithinContext(), context); + } + } + + private View getViewInternal(String viewPath, RequestContext context) { + if (viewPath.endsWith(".jsp")) { + if (jstlPresent) { + JstlView view = new JstlView(viewPath); + view.setApplicationContext(applicationContext); + return new MvcView(view, context); + } else { + InternalResourceView view = new InternalResourceView(viewPath); + view.setApplicationContext(applicationContext); + return new MvcView(view, context); + } + } else { + throw new IllegalArgumentException("Unsupported view type " + viewPath + + " only types supported are [.jsp]"); + } + } + } + + /** + * View factory implementation that delegates to the Spring-configured view resolver chain to resolve the Spring MVC + * view implementation to render. + * @author Keith Donald + */ + private static class ViewResolvingMvcViewFactory implements ViewFactory { + private Expression viewId; + private List viewResolvers; - private boolean internalResourceResolverOnly; + public ViewResolvingMvcViewFactory(Expression viewId, List viewResolvers) { + this.viewId = viewId; + this.viewResolvers = viewResolvers; + } public View getView(RequestContext context) { - String view = (String) viewExpression.getValue(context); + String view = (String) viewId.getValue(context); return new MvcView(resolveView(view), context); } protected org.springframework.web.servlet.View resolveView(String viewName) { - if (internalResourceResolverOnly && !viewName.startsWith("/")) { - viewName = ((ContextResource) viewResourceLoader.getResource(viewName)).getPathWithinContext(); - } for (Iterator it = viewResolvers.iterator(); it.hasNext();) { ViewResolver viewResolver = (ViewResolver) it.next(); try { @@ -65,43 +150,61 @@ public class MvcViewFactoryCreator implements ViewFactoryCreator { return view; } } catch (Exception e) { - e.printStackTrace(); + throw new IllegalStateException("Exception resolving view with name '" + viewName + "'", e); } } return null; } + } - private static class MvcView implements View { + private static class MvcView implements View { - private RequestContext context; + private RequestContext context; - private org.springframework.web.servlet.View view; + private org.springframework.web.servlet.View view; - public MvcView(org.springframework.web.servlet.View view, RequestContext context) { - this.view = view; - this.context = context; - } + public MvcView(org.springframework.web.servlet.View view, RequestContext context) { + this.view = view; + this.context = context; + } - public boolean eventSignaled() { - return context.getRequestParameters().contains("_eventId"); - } + public boolean eventSignaled() { + return context.getRequestParameters().contains("_eventId"); + } - public Event getEvent() { - return new Event(view, context.getRequestParameters().get("_eventId"), context.getRequestParameters() - .asAttributeMap()); - } + public Event getEvent() { + return new Event(view, context.getRequestParameters().get("_eventId"), context.getRequestParameters() + .asAttributeMap()); + } - public void render() { - Map model = new HashMap(); - model.putAll(context.getConversationScope().union(context.getFlowScope()) - .union(context.getFlashScope()).union(context.getRequestScope()).asMap()); - try { - view.render(model, (HttpServletRequest) context.getExternalContext().getRequest(), - (HttpServletResponse) context.getExternalContext().getResponse()); - } catch (Exception e) { - e.printStackTrace(); - } + public void render() { + Map model = new HashMap(); + model.putAll(context.getConversationScope().union(context.getFlowScope()).union(context.getFlashScope()) + .union(context.getRequestScope()).asMap()); + try { + view.render(model, (HttpServletRequest) context.getExternalContext().getRequest(), + (HttpServletResponse) context.getExternalContext().getResponse()); + } catch (Exception e) { + throw new IllegalStateException("Exception rendering view", e); } } } -} + + /** + * Simple adapter that adapts a view factory render cycle to the action interface. + * @author Keith Donald + */ + private static class ViewFactoryActionAdapter implements Action { + private ViewFactory viewFactory; + + public ViewFactoryActionAdapter(ViewFactory viewFactory) { + this.viewFactory = viewFactory; + } + + public Event execute(RequestContext context) throws Exception { + viewFactory.getView(context).render(); + return new Event(this, "success"); + } + } + +} \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/test/MockExternalContext.java b/spring-webflow/src/main/java/org/springframework/webflow/test/MockExternalContext.java index 6f13a480..838900ae 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/test/MockExternalContext.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/test/MockExternalContext.java @@ -128,6 +128,62 @@ public class MockExternalContext implements ExternalContext { return applicationMap; } + public Object getContext() { + return context; + } + + public Object getRequest() { + return request; + } + + public Object getResponse() { + return response; + } + + public PrintWriter getResponseWriter() { + throw new UnsupportedOperationException("Not yet implemented"); + } + + public String encode(String string) { + return string; + } + + public String buildFlowDefinitionUrl(FlowDefinitionRequestInfo requestInfo) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + public String buildFlowExecutionUrl(FlowExecutionRequestInfo requestInfo, boolean contextRelative) { + throw new UnsupportedOperationException("Not yet implemented"); + } + + public void sendFlowDefinitionRedirect(FlowDefinitionRequestInfo requestInfo) { + this.flowDefinitionRedirectResult = requestInfo; + } + + public void sendFlowExecutionRedirect(FlowExecutionRequestInfo requestInfo) { + this.flowExecutionRedirectResult = requestInfo; + } + + public void sendExternalRedirect(String resourceUri) { + externalRedirectResult = resourceUri; + } + + public void setPausedResult(String flowExecutionKey) { + this.pausedFlowExecutionKeyResult = flowExecutionKey; + } + + public void setEndedResult(String flowExecutionKey) { + + } + + public void setExceptionResult(FlowException e) { + exceptionResult = e; + } + + public boolean isResponseCommitted() { + return false; + } + // helper setters public void setFlowId(String flowId) { @@ -146,6 +202,18 @@ public class MockExternalContext implements ExternalContext { this.requestPath = requestPath; } + public void setContext(Object context) { + this.context = context; + } + + public void setRequest(Object request) { + this.request = request; + } + + public void setResponse(Object response) { + this.response = response; + } + /** * Set the request parameter map. * @see ExternalContext#getRequestParameterMap() @@ -215,60 +283,6 @@ public class MockExternalContext implements ExternalContext { getMockRequestParameterMap().put(parameterName, parameterValues); } - public Object getContext() { - return context; - } - - public Object getRequest() { - return request; - } - - public Object getResponse() { - return response; - } - - public PrintWriter getResponseWriter() { - throw new UnsupportedOperationException("Auto-generated method stub"); - } - - public String encode(String string) { - return string; - } - - public String buildFlowDefinitionUrl(FlowDefinitionRequestInfo requestInfo) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Auto-generated method stub"); - } - - public String buildFlowExecutionUrl(FlowExecutionRequestInfo requestInfo, boolean contextRelative) { - // TODO Auto-generated method stub - throw new UnsupportedOperationException("Auto-generated method stub"); - } - - public void sendFlowDefinitionRedirect(FlowDefinitionRequestInfo requestInfo) { - this.flowDefinitionRedirectResult = requestInfo; - } - - public void sendFlowExecutionRedirect(FlowExecutionRequestInfo requestInfo) { - this.flowExecutionRedirectResult = requestInfo; - } - - public void sendExternalRedirect(String resourceUri) { - externalRedirectResult = resourceUri; - } - - public void setPausedResult(String flowExecutionKey) { - this.pausedFlowExecutionKeyResult = flowExecutionKey; - } - - public void setEndedResult(String flowExecutionKey) { - - } - - public void setExceptionResult(FlowException e) { - exceptionResult = e; - } - public FlowDefinitionRequestInfo getFlowDefinitionRedirectResult() { return flowDefinitionRedirectResult; } @@ -288,12 +302,4 @@ public class MockExternalContext implements ExternalContext { public FlowException getExceptionResult() { return exceptionResult; } - - public boolean isResponseCommitted() { - return false; - /* - * return flowExecutionRedirectResult == true || flowDefinitionRedirectResult != null || externalRedirectResult != - * null; - */ - } } \ No newline at end of file diff --git a/spring-webflow/src/test/java/org/springframework/webflow/mvc/MvcViewFactoryTests.java b/spring-webflow/src/test/java/org/springframework/webflow/mvc/MvcViewFactoryTests.java new file mode 100644 index 00000000..1b26bf96 --- /dev/null +++ b/spring-webflow/src/test/java/org/springframework/webflow/mvc/MvcViewFactoryTests.java @@ -0,0 +1,128 @@ +package org.springframework.webflow.mvc; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Locale; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import junit.framework.TestCase; + +import org.springframework.binding.expression.Expression; +import org.springframework.binding.expression.support.StaticExpression; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.core.io.AbstractResource; +import org.springframework.core.io.ContextResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.util.ClassUtils; +import org.springframework.web.servlet.ViewResolver; +import org.springframework.webflow.execution.View; +import org.springframework.webflow.execution.ViewFactory; +import org.springframework.webflow.test.MockExternalContext; +import org.springframework.webflow.test.MockRequestContext; + +public class MvcViewFactoryTests extends TestCase { + private MvcViewFactoryCreator creator; + private StaticApplicationContext context; + + protected void setUp() { + creator = new MvcViewFactoryCreator(); + context = new StaticApplicationContext(); + } + + public void testNoResolversGetResource() { + creator.setApplicationContext(context); + ResourceLoader viewResourceLoader = new ResourceLoader() { + public ClassLoader getClassLoader() { + return ClassUtils.getDefaultClassLoader(); + } + + public Resource getResource(String name) { + return new TestContextResource("/parent/" + name); + } + }; + Expression viewId = new StaticExpression("myview.jsp"); + ViewFactory viewFactory = creator.createViewFactory(viewId, viewResourceLoader); + MockRequestContext context = new MockRequestContext(); + MockExternalContext externalContext = new MockExternalContext(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + externalContext.setRequest(request); + externalContext.setResponse(response); + context.setExternalContext(externalContext); + View view = viewFactory.getView(context); + view.render(); + assertEquals("/parent/myview.jsp", response.getForwardedUrl()); + } + + public void testViewResolversGetResource() { + MockViewResolver viewResolver = new MockViewResolver("myview"); + context.getBeanFactory().registerSingleton("viewResolver", viewResolver); + creator.setApplicationContext(context); + Expression viewId = new StaticExpression("myview"); + ViewFactory viewFactory = creator.createViewFactory(viewId, null); + MockRequestContext context = new MockRequestContext(); + MockExternalContext externalContext = new MockExternalContext(); + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + externalContext.setRequest(request); + externalContext.setResponse(response); + context.setExternalContext(externalContext); + View view = viewFactory.getView(context); + view.render(); + assertEquals("myview", response.getForwardedUrl()); + } + + private static class MockViewResolver implements ViewResolver { + + private String expectedViewName; + + public MockViewResolver(String expectedViewName) { + this.expectedViewName = expectedViewName; + } + + public org.springframework.web.servlet.View resolveViewName(String viewName, Locale arg1) throws Exception { + assertEquals(expectedViewName, viewName); + return new MockView(); + } + + class MockView implements org.springframework.web.servlet.View { + boolean renderCalled; + + public String getContentType() { + return "text/html"; + } + + public void render(Map model, HttpServletRequest request, HttpServletResponse response) throws Exception { + request.getRequestDispatcher(expectedViewName).forward(request, response); + renderCalled = true; + } + } + + } + + private static class TestContextResource extends AbstractResource implements ContextResource { + private String path; + + public TestContextResource(String path) { + this.path = path; + } + + public String getDescription() { + return "test context resource"; + } + + public InputStream getInputStream() throws IOException { + throw new UnsupportedOperationException("Auto-generated method stub"); + } + + public String getPathWithinContext() { + return path; + } + } +} \ No newline at end of file