/* * Copyright 2004-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.webflow.mvc.servlet; import java.io.IOException; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.InitializingBean; import org.springframework.http.HttpStatus; import org.springframework.js.ajax.AjaxHandler; import org.springframework.js.ajax.SpringJavascriptAjaxHandler; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import org.springframework.web.servlet.FlashMap; import org.springframework.web.servlet.FlashMapManager; import org.springframework.web.servlet.HandlerAdapter; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.support.RequestContextUtils; import org.springframework.web.servlet.support.WebContentGenerator; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.webflow.context.servlet.DefaultFlowUrlHandler; import org.springframework.webflow.context.servlet.FlowUrlHandler; import org.springframework.webflow.context.servlet.ServletExternalContext; import org.springframework.webflow.core.FlowException; import org.springframework.webflow.core.collection.AttributeMap; import org.springframework.webflow.core.collection.LocalAttributeMap; import org.springframework.webflow.core.collection.MutableAttributeMap; import org.springframework.webflow.execution.FlowExecutionOutcome; import org.springframework.webflow.execution.repository.NoSuchFlowExecutionException; import org.springframework.webflow.executor.FlowExecutionResult; import org.springframework.webflow.executor.FlowExecutor; /** * A custom MVC HandlerAdapter that encapsulates the generic workflow associated with executing flows in a Servlet * environment. Delegates to mapped {@link FlowHandler flow handlers} to manage the interaction with executions of * specific flow definitions. * * @author Keith Donald * @author Phillip Webb */ public class FlowHandlerAdapter extends WebContentGenerator implements HandlerAdapter, InitializingBean { private static final Log logger = LogFactory.getLog(FlowHandlerAdapter.class); private static final String REFERER_FLOW_EXECUTION_ATTRIBUTE = "refererExecution"; private static final String SERVLET_RELATIVE_LOCATION_PREFIX = "servletRelative:"; private static final String CONTEXT_RELATIVE_LOCATION_PREFIX = "contextRelative:"; private static final String SERVER_RELATIVE_LOCATION_PREFIX = "serverRelative:"; /** * The entry point into Spring Web Flow. */ private FlowExecutor flowExecutor; /** * A strategy for extracting flow arguments and generating flow urls. */ private FlowUrlHandler flowUrlHandler; /** * The representation of an Ajax client service capable of interacting with web flow. */ private AjaxHandler ajaxHandler; private boolean redirectHttp10Compatible = true; private HttpStatus statusCode; private String[] hosts; private boolean saveOutputToFlashScopeOnRedirect; /** * Creates a new flow handler adapter. * @see #setFlowExecutor(FlowExecutor) * @see #setFlowUrlHandler(FlowUrlHandler) * @see #setAjaxHandler(AjaxHandler) * @see #afterPropertiesSet() */ public FlowHandlerAdapter() { // prevent caching of flow pages by default setCacheSeconds(0); } /** * Returns the central service for executing flows. Required. */ public FlowExecutor getFlowExecutor() { return flowExecutor; } /** * Sets the central service for executing flows. Required. * @param flowExecutor */ public void setFlowExecutor(FlowExecutor flowExecutor) { this.flowExecutor = flowExecutor; } /** * Returns the flow url handler. */ public FlowUrlHandler getFlowUrlHandler() { return flowUrlHandler; } /** * Sets the flow url handler * @param flowUrlHandler the flow url handler */ public void setFlowUrlHandler(FlowUrlHandler flowUrlHandler) { this.flowUrlHandler = flowUrlHandler; } /** * Returns the configured Ajax handler. */ public AjaxHandler getAjaxHandler() { return ajaxHandler; } /** * Sets the configured Ajax handler. * @param ajaxHandler the ajax handler */ public void setAjaxHandler(AjaxHandler ajaxHandler) { this.ajaxHandler = ajaxHandler; } /** * Whether redirect sent by this handler adapter should be compatible with HTTP 1.0 clients. * @return true if so, false otherwise */ public boolean getRedirectHttp10Compatible() { return redirectHttp10Compatible; } /** * Set whether redirects sent by this handler adapter should be compatible with HTTP 1.0 clients. * <p> * By default, this will enforce a redirect HTTP status code of 302 by delegating to * <code>HttpServletResponse.sendRedirect</code>. Setting this to false will send HTTP status code 303, which is the * correct code for HTTP 1.1 clients, but not understood by HTTP 1.0 clients. * <p> * Many HTTP 1.1 clients treat 302 just like 303, not making any difference. However, some clients depend on 303 * when redirecting after a POST request; turn this flag off in such a scenario. * @see javax.servlet.http.HttpServletResponse#sendRedirect */ public void setRedirectHttp10Compatible(boolean redirectHttp10Compatible) { this.redirectHttp10Compatible = redirectHttp10Compatible; } /** * Set the status code for this view. * <p>Default is to send 302/303, depending on the value of the * {@link #setRedirectHttp10Compatible(boolean) http10Compatible} flag. */ public void setStatusCode(HttpStatus statusCode) { this.statusCode = statusCode; } /** * Configure one or more hosts associated with the application. All other * hosts will be considered external hosts. In effect this property * provides a way turn off encoding via * {@link HttpServletResponse#encodeRedirectURL} for URLs that have a host * and that host is not listed as a known host. * <p>If not set (the default) all URLs are encoded through the response. * @param hosts one or more application hosts * @since 2.4.3 */ public void setHosts(String[] hosts) { this.hosts = hosts; } /** * Return the configured hosts associated with the application. */ public String[] getHosts() { return this.hosts; } /** * Set whether servlet relative redirects sent by this handler adapter * should pass {@link FlowExecutionOutcome#getOutput() flow output} to the * Spring MVC {@link FlashMap flash scope}. * * <p>By default, to remain compatible with previous releases, flow output is * not mapped to flash scope. * * @param saveOutputToFlashScopeOnRedirect */ public void setSaveOutputToFlashScopeOnRedirect(boolean saveOutputToFlashScopeOnRedirect) { this.saveOutputToFlashScopeOnRedirect = saveOutputToFlashScopeOnRedirect; } /** * Whether servlet relative redirects should pass * {@link FlowExecutionOutcome#getOutput() flow output} to the Spring MVC * {@link FlashMap flash scope}. * * @return {@code true} if so, {@code false} otherwise */ public boolean getSaveOutputToFlashScopeOnRedirect() { return this.saveOutputToFlashScopeOnRedirect; } public void afterPropertiesSet() throws Exception { Assert.notNull(flowExecutor, "The FlowExecutor to execute flows is required"); if (flowUrlHandler == null) { flowUrlHandler = new DefaultFlowUrlHandler(); } if (ajaxHandler == null) { ajaxHandler = new SpringJavascriptAjaxHandler(); } } public boolean supports(Object handler) { return handler instanceof FlowHandler; } public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { FlowHandler flowHandler = (FlowHandler) handler; checkAndPrepare(request, response, false); String flowExecutionKey = flowUrlHandler.getFlowExecutionKey(request); if (flowExecutionKey != null) { try { ServletExternalContext context = createServletExternalContext(request, response); FlowExecutionResult result = flowExecutor.resumeExecution(flowExecutionKey, context); handleFlowExecutionResult(result, context, request, response, flowHandler); } catch (FlowException e) { handleFlowException(e, request, response, flowHandler); } } else { try { String flowId = getFlowId(flowHandler, request); MutableAttributeMap<Object> input = getInputMap(flowHandler, request); ServletExternalContext context = createServletExternalContext(request, response); FlowExecutionResult result = flowExecutor.launchExecution(flowId, input, context); handleFlowExecutionResult(result, context, request, response, flowHandler); } catch (FlowException e) { handleFlowException(e, request, response, flowHandler); } } return null; } public long getLastModified(HttpServletRequest request, Object handler) { return -1; } // subclassing hooks /** * Creates the servlet external context for the current HTTP servlet request. * @param request the current request * @param response the current response */ protected ServletExternalContext createServletExternalContext(HttpServletRequest request, HttpServletResponse response) { ServletExternalContext context = new MvcExternalContext(getServletContext(), request, response, flowUrlHandler); context.setAjaxRequest(ajaxHandler.isAjaxRequest(request, response)); return context; } /** * The default algorithm to determine the id of the flow to launch from the current request. Only called if * {@link FlowHandler#getFlowId()} returns null. This implementation delegates to the configured * {@link FlowUrlHandler#getFlowId(HttpServletRequest)}. Subclasses may override. * @param request the current request */ protected String defaultGetFlowId(HttpServletRequest request) { return flowUrlHandler.getFlowId(request); } /** * The default algorithm to create the flow execution input map. Only called if * {@link FlowHandler#createExecutionInputMap(HttpServletRequest)} returns null. This implementation exposes all * current request parameters as flow execution input attributes. Subclasses may override. * @param request the current request */ protected MutableAttributeMap<Object> defaultCreateFlowExecutionInputMap(HttpServletRequest request) { Map<String, String[]> parameterMap = request.getParameterMap(); if (parameterMap.size() == 0) { return null; } LocalAttributeMap<Object> inputMap = new LocalAttributeMap<Object>(parameterMap.size(), 1); for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) { String[] values = entry.getValue(); inputMap.put(entry.getKey(), values.length == 1 ? values[0] : values); } return inputMap; } /** * The default algorithm for handling a flow execution outcome. Only called if * {@link FlowHandler#handleExecutionOutcome(FlowExecutionOutcome, HttpServletRequest, HttpServletResponse)} returns * null. This implementation attempts to start a new execution of the ended flow. Any flow execution output is * passed as input to the new execution. Subclasses may override. * @param flowId the id of the ended flow * @param outcome the flow execution outcome * @param context ServletExternalContext the completed ServletExternalContext * @param request the current request * @param response the current response */ protected void defaultHandleExecutionOutcome(String flowId, FlowExecutionOutcome outcome, ServletExternalContext context, HttpServletRequest request, HttpServletResponse response) throws IOException { if (!context.isResponseComplete()) { // by default, just start the flow over passing the output as input if (logger.isDebugEnabled()) { logger.debug("Ended flow '" + flowId + "' did not commit a response; " + "attempting to start a new flow execution as a default outcome handler"); } String flowUrl = flowUrlHandler.createFlowDefinitionUrl(flowId, outcome.getOutput(), request); sendRedirect(flowUrl, request, response); } } /** * The default algorithm for handling a {@link FlowException} now handled by the Web Flow system. Only called if * {@link FlowHandler#handleException(FlowException, HttpServletRequest, HttpServletResponse)} returns null. This * implementation rethrows the exception unless it is a {@link NoSuchFlowExecutionException}. If the exception is a * NoSuchFlowExecutionException, this implementation attempts to start a new execution of the ended or expired flow. * Subclasses may override. * @param flowId the id of the ended flow * @param e the flow exception * @param request the current request * @param response the current response */ protected void defaultHandleException(String flowId, FlowException e, HttpServletRequest request, HttpServletResponse response) throws IOException { if (e instanceof NoSuchFlowExecutionException && flowId != null) { if (!response.isCommitted()) { if (logger.isDebugEnabled()) { logger.debug("Restarting a new execution of previously ended flow '" + flowId + "'"); } // by default, attempt to restart the flow String flowUrl = flowUrlHandler.createFlowDefinitionUrl(flowId, null, request); sendRedirect(flowUrl, request, response); } } else { throw e; } } /** * Sends a redirect to the requested url using {@link HttpServletResponse#sendRedirect(String)}.Called to actually * perform flow execution redirects, flow definition redirects, and external redirects. Subclasses may override to * customize general Web Flow system redirect behavior. * @param url the url to redirect to * @param request the current request * @param response the current response * @throws IOException an exception occurred */ protected void sendRedirect(String url, HttpServletRequest request, HttpServletResponse response) throws IOException { if (ajaxHandler.isAjaxRequest(request, response)) { ajaxHandler.sendAjaxRedirect(url, request, response, false); } else { String encodedRedirectURL = (isRemoteHost(url) ? url : response.encodeRedirectURL(url)); if (redirectHttp10Compatible) { if (statusCode != null) { response.setStatus(statusCode.value()); response.setHeader("Location", encodedRedirectURL); } else { // Send status code 302 by default. response.sendRedirect(encodedRedirectURL); } } else { int code = (statusCode != null) ? statusCode.value() : 303; response.setStatus(code); response.setHeader("Location", response.encodeRedirectURL(url)); } } } /** * Whether the given targetUrl has a host that is a "foreign" system in which * case {@link HttpServletResponse#encodeRedirectURL} will not be applied. * This method returns {@code true} if the {@link #setHosts(String[])} * property is configured and the target URL has a host that does not match. * @param targetUrl the target redirect URL * @return {@code true} the target URL has a remote host, {@code false} if it * the URL does not have a host or the "host" property is not configured. * @since 2.4.3 */ protected boolean isRemoteHost(String targetUrl) { if (ObjectUtils.isEmpty(getHosts())) { return false; } String targetHost = UriComponentsBuilder.fromUriString(targetUrl).build().getHost(); if (StringUtils.isEmpty(targetHost)) { return false; } for (String host : getHosts()) { if (targetHost.equals(host)) { return false; } } return true; } // internal helpers private void handleFlowExecutionResult(FlowExecutionResult result, ServletExternalContext context, HttpServletRequest request, HttpServletResponse response, FlowHandler handler) throws IOException { if (result.isPaused()) { if (context.getFlowExecutionRedirectRequested()) { sendFlowExecutionRedirect(result, context, request, response); } else if (context.getFlowDefinitionRedirectRequested()) { sendFlowDefinitionRedirect(result, context, request, response); } else if (context.getExternalRedirectRequested()) { sendExternalRedirect(context.getExternalRedirectUrl(), request, response); } } else if (result.isEnded()) { if (context.getFlowDefinitionRedirectRequested()) { sendFlowDefinitionRedirect(result, context, request, response); } else if (context.getExternalRedirectRequested()) { sendExternalRedirect(context.getExternalRedirectUrl(), request, response, result); } else { String location = handler.handleExecutionOutcome(result.getOutcome(), request, response); if (location != null) { sendExternalRedirect(location, request, response, result); } else { defaultHandleExecutionOutcome(result.getFlowId(), result.getOutcome(), context, request, response); } } } else { throw new IllegalStateException("Execution result should have been one of [paused] or [ended]"); } } private void sendFlowExecutionRedirect(FlowExecutionResult result, ServletExternalContext context, HttpServletRequest request, HttpServletResponse response) throws IOException { String url = flowUrlHandler.createFlowExecutionUrl(result.getFlowId(), result.getPausedKey(), request); if (logger.isDebugEnabled()) { logger.debug("Sending flow execution redirect to '" + url + "'"); } if (context.isAjaxRequest()) { ajaxHandler.sendAjaxRedirect(url, request, response, context.getRedirectInPopup()); } else { sendRedirect(url, request, response); } } private void sendFlowDefinitionRedirect(FlowExecutionResult result, ServletExternalContext context, HttpServletRequest request, HttpServletResponse response) throws IOException { String flowId = context.getFlowRedirectFlowId(); MutableAttributeMap<Object> input = context.getFlowRedirectFlowInput(); if (result.isPaused()) { input.put(REFERER_FLOW_EXECUTION_ATTRIBUTE, result.getPausedKey()); } String url = flowUrlHandler.createFlowDefinitionUrl(flowId, input, request); if (logger.isDebugEnabled()) { logger.debug("Sending flow definition redirect to '" + url + "'"); } sendRedirect(url, request, response); } private void sendExternalRedirect(String location, HttpServletRequest request, HttpServletResponse response) throws IOException { sendExternalRedirect(location, request, response, null); } private void sendExternalRedirect(String location, HttpServletRequest request, HttpServletResponse response, FlowExecutionResult result) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Sending external redirect to '" + location + "'"); } if (location.startsWith(SERVLET_RELATIVE_LOCATION_PREFIX)) { sendServletRelativeRedirect(location.substring(SERVLET_RELATIVE_LOCATION_PREFIX.length()), request, response, result); } else if (location.startsWith(CONTEXT_RELATIVE_LOCATION_PREFIX)) { sendContextRelativeRedirect(location.substring(CONTEXT_RELATIVE_LOCATION_PREFIX.length()), request, response, result); } else if (location.startsWith(SERVER_RELATIVE_LOCATION_PREFIX)) { String url = location.substring(SERVER_RELATIVE_LOCATION_PREFIX.length()); if (!url.startsWith("/")) { url = "/" + url; } sendRedirect(url, request, response); } else if (location.startsWith("http://") || location.startsWith("https://")) { sendRedirect(location, request, response); } else { if (isRedirectServletRelative(request)) { sendServletRelativeRedirect(location, request, response, result); } else { sendContextRelativeRedirect(location, request, response, result); } } } /** * Returns true if the servlet path should automatically be prepended to an external redirect URL for which a prefix * such as "contextRelative: was not specified. This answer depends on how the MVC Dispatcher Servlet is mapped: (1) * default servlet, (2) prefix, (3) extension, (4) exact match. In (1), (3), and (4) it doesn't make sense to * prepend the servlet path, which contains the entire URL after the context path. * * Because there is no simple way to get the servlet mapping, this method is implemented to return True if path info * is not null. Also see SWF-1385. */ private boolean isRedirectServletRelative(HttpServletRequest request) { return (request.getPathInfo() != null); } private void sendContextRelativeRedirect(String location, HttpServletRequest request, HttpServletResponse response, FlowExecutionResult result) throws IOException { StringBuilder url = new StringBuilder(request.getContextPath()); if (!location.startsWith("/")) { url.append('/'); } url.append(location); sendRedirect(url.toString(), request, response, result); } private void sendServletRelativeRedirect(String location, HttpServletRequest request, HttpServletResponse response, FlowExecutionResult result) throws IOException { StringBuilder url = new StringBuilder(request.getContextPath()); url.append(request.getServletPath()); if (!location.startsWith("/")) { url.append('/'); } url.append(location); sendRedirect(url.toString(), request, response, result); } private void sendRedirect(String url, HttpServletRequest request, HttpServletResponse response, FlowExecutionResult result) throws IOException { if (this.saveOutputToFlashScopeOnRedirect) { saveFlashOutput(url.toString(), request, response, result); } sendRedirect(url, request, response); } private void saveFlashOutput(String location, HttpServletRequest request, HttpServletResponse response, FlowExecutionResult result) { if ((result == null) || (result.getOutcome() == null) || (result.getOutcome().getOutput().isEmpty())) { return; } AttributeMap<Object> output = result.getOutcome().getOutput(); FlashMapManager flashMapManager = RequestContextUtils.getFlashMapManager(request); if (flashMapManager == null) { return; } UriComponents uriComponents = UriComponentsBuilder.fromUriString(location).build(); FlashMap flashMap = new FlashMap(); flashMap.setTargetRequestPath(uriComponents.getPath()); flashMap.addTargetRequestParams(uriComponents.getQueryParams()); flashMap.putAll(output.asMap()); flashMapManager.saveOutputFlashMap(flashMap, request, response); } private void handleFlowException(FlowException e, HttpServletRequest request, HttpServletResponse response, FlowHandler handler) throws IOException { String location = handler.handleException(e, request, response); if (location != null) { sendExternalRedirect(location, request, response); } else { defaultHandleException(getFlowId(handler, request), e, request, response); } } private String getFlowId(FlowHandler handler, HttpServletRequest request) { String flowId = handler.getFlowId(); if (flowId != null) { return flowId; } else { return defaultGetFlowId(request); } } private MutableAttributeMap<Object> getInputMap(FlowHandler handler, HttpServletRequest request) { MutableAttributeMap<Object> input = handler.createExecutionInputMap(request); if (input != null) { return input; } else { return defaultCreateFlowExecutionInputMap(request); } } }