From 1ae46de7e19513ade02d9666305478acbdeb1295 Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Sun, 9 Mar 2008 05:43:40 +0000 Subject: [PATCH] reduced flow execution key length extracted ajax handler strategy and spring javascript impl --- .../conversation/ConversationException.java | 2 +- .../impl/ConversationContainer.java | 14 +++- .../SessionBindingConversationManager.java | 48 ++++--------- .../impl/SimpleConversationId.java | 5 +- .../impl/DefaultFlowExecutionRepository.java | 51 +++++++------ .../impl/FlowExecutionContinuationGroup.java | 2 +- .../AbstractFlowExecutionRepository.java | 72 ++++++++++--------- .../support/CompositeFlowExecutionKey.java | 15 ++-- .../webflow/mvc/AjaxHandler.java | 27 +++++++ .../webflow/mvc/FlowController.java | 41 ++--------- .../webflow/mvc/FlowHandlerAdapter.java | 58 ++++++--------- .../mvc/SpringJavascriptAjaxHandler.java | 53 ++++++++++++++ .../DefaultFlowExecutionRepositoryTests.java | 25 +++++-- .../CompositeFlowExecutionKeyTests.java | 4 +- 14 files changed, 238 insertions(+), 179 deletions(-) create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/mvc/AjaxHandler.java create mode 100644 spring-webflow/src/main/java/org/springframework/webflow/mvc/SpringJavascriptAjaxHandler.java diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationException.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationException.java index f3a2ce2f..7aebc7f6 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationException.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/ConversationException.java @@ -22,7 +22,7 @@ import org.springframework.webflow.core.FlowException; * * @author Keith Donald */ -public abstract class ConversationException extends FlowException { +public class ConversationException extends FlowException { /** * Creates a conversation service exception. diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ConversationContainer.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ConversationContainer.java index f69ccbea..8f25f81f 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ConversationContainer.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/ConversationContainer.java @@ -50,6 +50,11 @@ class ConversationContainer implements Serializable { */ private List conversations; + /** + * The sequence for unique conversation identifiers within this container. + */ + private int conversationIdSequence; + /** * Create a new conversation container. * @param maxConversations the maximum number of allowed concurrent conversations, -1 for unlimited @@ -77,12 +82,11 @@ class ConversationContainer implements Serializable { /** * Create a new conversation based on given parameters and add it to the container. - * @param id the unique id of the conversation * @param parameters descriptive parameters * @return the created conversation */ - public synchronized Conversation createAndAddConversation(ConversationId id, ConversationParameters parameters) { - ContainedConversation conversation = new ContainedConversation(this, id); + public synchronized Conversation createConversation(ConversationParameters parameters) { + ContainedConversation conversation = new ContainedConversation(this, nextId()); conversations.add(conversation); if (maxExceeded()) { // end oldest conversation @@ -91,6 +95,10 @@ class ConversationContainer implements Serializable { return conversation; } + private ConversationId nextId() { + return new SimpleConversationId(Integer.valueOf(++conversationIdSequence)); + } + /** * Return the identified conversation. * @param id the id to lookup diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/SessionBindingConversationManager.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/SessionBindingConversationManager.java index 40300201..76a5ce04 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/SessionBindingConversationManager.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/SessionBindingConversationManager.java @@ -15,8 +15,6 @@ */ package org.springframework.webflow.conversation.impl; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; import org.springframework.webflow.context.ExternalContextHolder; import org.springframework.webflow.conversation.Conversation; import org.springframework.webflow.conversation.ConversationException; @@ -24,8 +22,6 @@ import org.springframework.webflow.conversation.ConversationId; import org.springframework.webflow.conversation.ConversationManager; import org.springframework.webflow.conversation.ConversationParameters; import org.springframework.webflow.core.collection.SharedAttributeMap; -import org.springframework.webflow.util.RandomGuidUidGenerator; -import org.springframework.webflow.util.UidGenerator; /** * Simple implementation of a conversation manager that stores conversations in the session attribute map. @@ -40,8 +36,6 @@ import org.springframework.webflow.util.UidGenerator; */ public class SessionBindingConversationManager implements ConversationManager { - private static final Log logger = LogFactory.getLog(SessionBindingConversationManager.class); - /** * The name of the session attribute that will hold the conversation container used by this conversation manager. * @@ -50,12 +44,7 @@ public class SessionBindingConversationManager implements ConversationManager { * value to something unique. * @see #setSessionKey(String) */ - private String sessionKey = "webflow.conversationContainer"; - - /** - * The conversation uid generation strategy to use. - */ - private UidGenerator conversationIdGenerator = new RandomGuidUidGenerator(); + private String sessionKey = "webflowConversationContainer"; /** * The maximum number of active conversations allowed in a session. The default is 5. This is high enough for most @@ -63,20 +52,6 @@ public class SessionBindingConversationManager implements ConversationManager { */ private int maxConversations = 5; - /** - * Returns the used generator for conversation ids. Defaults to {@link RandomGuidUidGenerator}. - */ - public UidGenerator getConversationIdGenerator() { - return conversationIdGenerator; - } - - /** - * Sets the configured generator for conversation ids. - */ - public void setConversationIdGenerator(UidGenerator uidGenerator) { - this.conversationIdGenerator = uidGenerator; - } - /** * Returns the maximum number of allowed concurrent conversations. The default is 5. */ @@ -110,12 +85,7 @@ public class SessionBindingConversationManager implements ConversationManager { } public Conversation beginConversation(ConversationParameters conversationParameters) throws ConversationException { - ConversationId conversationId = new SimpleConversationId(conversationIdGenerator.generateUid()); - if (logger.isDebugEnabled()) { - logger.debug("Beginning conversation " + conversationParameters + "; unique conversation id = " - + conversationId); - } - return getConversationContainer().createAndAddConversation(conversationId, conversationParameters); + return getConversationContainer().createConversation(conversationParameters); } public Conversation getConversation(ConversationId id) throws ConversationException { @@ -123,7 +93,17 @@ public class SessionBindingConversationManager implements ConversationManager { } public ConversationId parseConversationId(String encodedId) throws ConversationException { - return new SimpleConversationId(conversationIdGenerator.parseUid(encodedId)); + try { + return new SimpleConversationId(Integer.valueOf(encodedId)); + } catch (NumberFormatException e) { + throw new ConversationException("Unable to parse string-encoded conversationId + '" + encodedId + "'", e); + } + } + + // hooks for subclassing + + protected ConversationContainer createConversationContainer() { + return new ConversationContainer(maxConversations, sessionKey); } // internal helpers @@ -137,7 +117,7 @@ public class SessionBindingConversationManager implements ConversationManager { synchronized (sessionMap.getMutex()) { ConversationContainer container = (ConversationContainer) sessionMap.get(sessionKey); if (container == null) { - container = new ConversationContainer(maxConversations, sessionKey); + container = createConversationContainer(); sessionMap.put(sessionKey, container); } return container; diff --git a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/SimpleConversationId.java b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/SimpleConversationId.java index 14be4030..9ab466c4 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/SimpleConversationId.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/conversation/impl/SimpleConversationId.java @@ -21,9 +21,8 @@ import org.springframework.webflow.conversation.ConversationId; import org.springframework.webflow.conversation.ConversationManager; /** - * An id that uniquely identifies a conversation managed by a {@link ConversationManager}. - *

- * This key consists of a unique string that is typically a GUID. + * An id that uniquely identifies a conversation managed by a {@link ConversationManager}. This key consists of a + * unique string that is typically a GUID. * * @author Ben Hale */ diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/impl/DefaultFlowExecutionRepository.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/impl/DefaultFlowExecutionRepository.java index 1ef9da4b..ceed0ab8 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/impl/DefaultFlowExecutionRepository.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/impl/DefaultFlowExecutionRepository.java @@ -82,13 +82,22 @@ public class DefaultFlowExecutionRepository extends AbstractFlowExecutionContinu } /** - * Sets the maximum number of continuations allowed per conversation in this repository. Use -1 for unlimited. The + * Returns the max number of continuations allowed per conversation by this repository. + */ + public int getMaxContinuations() { + return maxContinuations; + } + + /** + * Sets the maximum number of continuations allowed per conversation by this repository. Use -1 for unlimited. The * default is 30. */ public void setMaxContinuations(int maxContinuations) { this.maxContinuations = maxContinuations; } + // implementing flow execution repository + public FlowExecution getFlowExecution(FlowExecutionKey key) { if (logger.isDebugEnabled()) { logger.debug("Getting flow execution with key '" + key + "'"); @@ -117,28 +126,14 @@ public class DefaultFlowExecutionRepository extends AbstractFlowExecutionContinu putConversationScope(flowExecution); } - // internal helpers + // hooks for subclassing - /** - * Returns the continuation group associated with the governing conversation. TODO: memento persistence? - * @param key the flow execution key - * @return the continuation group - */ - private FlowExecutionContinuationGroup getContinuationGroup(FlowExecutionKey key) { - Conversation conversation = getConversation(key); - FlowExecutionContinuationGroup group = (FlowExecutionContinuationGroup) conversation - .getAttribute(CONTINUATION_GROUP_ATTRIBUTE); - if (group == null) { - // setup a new continuation group for the conversation - // no need to synchronize here since this code will only be executed - // during the launch of a new flow execution, at which time the - // key has not yet been communicated to any other threads - group = new FlowExecutionContinuationGroup(maxContinuations); - conversation.putAttribute(CONTINUATION_GROUP_ATTRIBUTE, group); - } - return group; + protected FlowExecutionContinuationGroup createFlowExecutionContinuationGroup() { + return new FlowExecutionContinuationGroup(maxContinuations); } + // internal helpers + /** * Returns the continuation in the group with the specified key. * @param key the flow execution key @@ -152,4 +147,20 @@ public class DefaultFlowExecutionRepository extends AbstractFlowExecutionContinu throw new FlowExecutionRestorationFailureException(key, e); } } + + /** + * Returns the continuation group associated with the governing conversation. + * @param key the flow execution key + * @return the continuation group + */ + private FlowExecutionContinuationGroup getContinuationGroup(FlowExecutionKey key) { + Conversation conversation = getConversation(key); + FlowExecutionContinuationGroup group = (FlowExecutionContinuationGroup) conversation + .getAttribute(CONTINUATION_GROUP_ATTRIBUTE); + if (group == null) { + group = createFlowExecutionContinuationGroup(); + conversation.putAttribute(CONTINUATION_GROUP_ATTRIBUTE, group); + } + return group; + } } \ No newline at end of file diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/impl/FlowExecutionContinuationGroup.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/impl/FlowExecutionContinuationGroup.java index fdee8d8d..3a29c1c5 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/impl/FlowExecutionContinuationGroup.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/impl/FlowExecutionContinuationGroup.java @@ -92,7 +92,7 @@ class FlowExecutionContinuationGroup implements Serializable { continuationIds.remove(continuationId); } continuationIds.add(continuationId); - // remove the oldest continuation if them maximium number of + // remove the oldest continuation if the maximium number of // continuations has been exceeded if (maxExceeded()) { removeOldestContinuation(); diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/AbstractFlowExecutionRepository.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/AbstractFlowExecutionRepository.java index f56597e0..5199ba22 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/AbstractFlowExecutionRepository.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/AbstractFlowExecutionRepository.java @@ -38,8 +38,6 @@ import org.springframework.webflow.execution.repository.FlowExecutionLock; import org.springframework.webflow.execution.repository.FlowExecutionRepository; import org.springframework.webflow.execution.repository.FlowExecutionRepositoryException; import org.springframework.webflow.execution.repository.NoSuchFlowExecutionException; -import org.springframework.webflow.util.RandomGuidUidGenerator; -import org.springframework.webflow.util.UidGenerator; /** * Abstract base class for flow execution repository implementations. Does not make any assumptions about the storage @@ -68,11 +66,6 @@ public abstract class AbstractFlowExecutionRepository implements FlowExecutionRe */ private FlowExecutionStateRestorer executionStateRestorer; - /** - * The uid generation strategy to use. - */ - private UidGenerator continuationIdGenerator = new RandomGuidUidGenerator(); - /** * Flag to indicate whether or not a new flow execution key should always be generated before each put call. Default * is true. @@ -91,15 +84,6 @@ public abstract class AbstractFlowExecutionRepository implements FlowExecutionRe this.executionStateRestorer = executionStateRestorer; } - /** - * Sets the uid generation strategy used to generate unique continuation identifiers for - * {@link FlowExecutionKey flow execution keys}. - */ - public void setContinuationIdGenerator(UidGenerator continuationIdGenerator) { - Assert.notNull(continuationIdGenerator, "The continuation id generator is required"); - this.continuationIdGenerator = continuationIdGenerator; - } - /** * Sets a flag indicating if a new {@link FlowExecutionKey} should always be generated before each put call. By * setting this to false a FlowExecution can remain identified by the same key throughout its life. @@ -111,7 +95,7 @@ public abstract class AbstractFlowExecutionRepository implements FlowExecutionRe public FlowExecutionKey getKey(FlowExecution execution) { if (execution.getKey() == null) { Conversation conversation = beginConversation(execution); - return new CompositeFlowExecutionKey(conversation.getId(), continuationIdGenerator.generateUid()); + return new CompositeFlowExecutionKey(conversation.getId(), Integer.valueOf(1)); } else { return getNextKey(execution); } @@ -120,25 +104,16 @@ public abstract class AbstractFlowExecutionRepository implements FlowExecutionRe public FlowExecutionKey parseFlowExecutionKey(String encodedKey) throws FlowExecutionRepositoryException { if (!StringUtils.hasText(encodedKey)) { throw new BadlyFormattedFlowExecutionKeyException(encodedKey, - "The string encoded flow execution key is required"); + "The string-encoded flow execution key is required"); } String[] keyParts = CompositeFlowExecutionKey.keyParts(encodedKey); - // parse out the conversation id ConversationId conversationId; try { conversationId = conversationManager.parseConversationId(keyParts[0]); } catch (ConversationException e) { - throw new BadlyFormattedFlowExecutionKeyException(encodedKey, "The conversation id '" + keyParts[0] - + "' contained in the composite flow execution key '" + encodedKey + "' is invalid", e); - } - // parse out the continuation id - Serializable continuationId; - try { - continuationId = continuationIdGenerator.parseUid(keyParts[1]); - } catch (FlowExecutionRepositoryException e) { - throw new BadlyFormattedFlowExecutionKeyException(encodedKey, "The continuation id '" + keyParts[1] - + "' contained in the composite flow execution key '" + encodedKey + "' is invalid", e); + throw new BadlyFormattedFlowExecutionKeyException(encodedKey, CompositeFlowExecutionKey.getFormat(), e); } + Serializable continuationId = parseContinuationId(keyParts[1], encodedKey); return new CompositeFlowExecutionKey(conversationId, continuationId); } @@ -160,7 +135,7 @@ public abstract class AbstractFlowExecutionRepository implements FlowExecutionRe endConversation(flowExecution); } - // overridable hooks for use in subclasses + // hooks for use in subclasses /** * Factory method that maps a new flow execution to a descriptive @@ -181,7 +156,9 @@ public abstract class AbstractFlowExecutionRepository implements FlowExecutionRe protected FlowExecutionKey getNextKey(FlowExecution execution) { if (alwaysGenerateNewNextKey) { CompositeFlowExecutionKey key = (CompositeFlowExecutionKey) execution.getKey(); - return new CompositeFlowExecutionKey(key.getConversationId(), continuationIdGenerator.generateUid()); + Integer continuationId = (Integer) key.getContinuationId(); + Integer nextId = Integer.valueOf(continuationId.intValue() + 1); + return new CompositeFlowExecutionKey(key.getConversationId(), nextId); } else { return execution.getKey(); } @@ -201,26 +178,48 @@ public abstract class AbstractFlowExecutionRepository implements FlowExecutionRe } } + /** + * Returns the conversationId portion of the flow execution key. + * @param key the execution key + */ protected ConversationId getConversationId(FlowExecutionKey key) { return ((CompositeFlowExecutionKey) key).getConversationId(); } + /** + * Returns the continuationId portion of the flow execution key. + * @param key the execution key + */ protected Serializable getContinuationId(FlowExecutionKey key) { return ((CompositeFlowExecutionKey) key).getContinuationId(); } + /** + * Returns the transient state of the flow execution after potential deserialization. + * @param execution the flow execution + * @param key the flow execution key + */ protected FlowExecution restoreTransientState(FlowExecution execution, FlowExecutionKey key) { return executionStateRestorer.restoreState(execution, key, getConversationScope(key), this); } + /** + * Puts the value of conversation scope in the conversation object. + * @param flowExecution the flow execution holding a reference to conversation scope + */ protected void putConversationScope(FlowExecution flowExecution) { getConversation(flowExecution.getKey()).putAttribute("scope", flowExecution.getConversationScope()); } + /** + * Assert that a flow execution key has been assigned to the execution. + * @param execution the flow execution + * @throws IllegalStateException if a key has not yet been assigned as expected + */ protected void assertKeySet(FlowExecution execution) throws IllegalStateException { if (execution.getKey() == null) { throw new IllegalStateException( - "Key for the flow execution is null; make sure the key is assigned first. Execution = " + "The key for the flow execution is null; make sure the key is assigned first. Execution Details = " + execution); } } @@ -233,8 +232,15 @@ public abstract class AbstractFlowExecutionRepository implements FlowExecutionRe return conversation; } + private Serializable parseContinuationId(String encodedId, String encodedKey) { + try { + return Integer.valueOf(encodedId); + } catch (NumberFormatException e) { + throw new BadlyFormattedFlowExecutionKeyException(encodedKey, CompositeFlowExecutionKey.getFormat(), e); + } + } + private Conversation endConversation(FlowExecution flowExecution) { - // end the governing conversation Conversation conversation = getConversation(flowExecution.getKey()); conversation.end(); return conversation; diff --git a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/CompositeFlowExecutionKey.java b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/CompositeFlowExecutionKey.java index 3b39ca0a..6aeac4c8 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/CompositeFlowExecutionKey.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/execution/repository/support/CompositeFlowExecutionKey.java @@ -42,14 +42,14 @@ import org.springframework.webflow.execution.repository.continuation.FlowExecuti class CompositeFlowExecutionKey extends FlowExecutionKey { /** - * The default conversation id prefix delimiter ("_c"). + * The default conversation id prefix delimiter. */ - private static final String CONVERSATION_ID_PREFIX = "_c"; + private static final String CONVERSATION_ID_PREFIX = "c"; /** - * The default continuation id prefix delimiter ("_k"). + * The default continuation id prefix delimiter. */ - private static final String CONTINUATION_ID_PREFIX = "_k"; + private static final String CONTINUATION_ID_PREFIX = "v"; /** * The format of the default string-encoded form, as returned by toString(). @@ -112,6 +112,13 @@ class CompositeFlowExecutionKey extends FlowExecutionKey { // static helpers + /** + * Returns a string description of the format of this key. + */ + public static String getFormat() { + return FORMAT; + } + /** * Helper that splits the string-form of an instance of this class into its "parts" so the parts can be easily * parsed. diff --git a/spring-webflow/src/main/java/org/springframework/webflow/mvc/AjaxHandler.java b/spring-webflow/src/main/java/org/springframework/webflow/mvc/AjaxHandler.java new file mode 100644 index 00000000..678a2dc6 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/mvc/AjaxHandler.java @@ -0,0 +1,27 @@ +package org.springframework.webflow.mvc; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +/** + * Strategy interface that encapsulates knowledge about a client-side ajax system and how to communicate with that + * system. + * @author Keith Donald + */ +public interface AjaxHandler { + + /** + * Is the request from the client an Ajax request? + * @param request the current request + */ + public boolean isAjaxRequest(HttpServletRequest request); + + /** + * Send a redirect request to the Ajax client. This should cause the client to send a new request to the specified + * target url. + * @param response the response object + * @param targetUrl the target url to redirect to + * @param popup wheter the redirect should be sent from a new popup dialog window + */ + public void sendAjaxRedirect(HttpServletResponse response, String targetUrl, boolean popup); +} diff --git a/spring-webflow/src/main/java/org/springframework/webflow/mvc/FlowController.java b/spring-webflow/src/main/java/org/springframework/webflow/mvc/FlowController.java index 6e77bcc6..b90ba2ad 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/mvc/FlowController.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/mvc/FlowController.java @@ -9,7 +9,6 @@ import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.util.StringUtils; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.mvc.AbstractController; import org.springframework.webflow.context.servlet.DefaultFlowUrlHandler; @@ -29,32 +28,14 @@ import org.springframework.webflow.executor.FlowExecutor; */ public class FlowController extends AbstractController { - /** - * The response header to be set on an Ajax redirect - */ - private static final String FLOW_REDIRECT_URL_HEADER = "Flow-Redirect-URL"; - - /** - * The response header to be set on an redirect that should be issued from a popup window. - */ - private static final String POPUP_VIEW_HEADER = "Flow-Modal-View"; - - /** - * The accept header value that signifies an Ajax request. - */ - private static final String AJAX_ACCEPT_CONTENT_TYPE = "text/html;type=ajax"; - - /** - * Alternate request parameter to indicate an Ajax request for cases when control of the header is not available. - */ - private static final String AJAX_SOURCE_PARAM = "ajaxSource"; - private static final Log logger = LogFactory.getLog(FlowController.class); private FlowExecutor flowExecutor; private FlowUrlHandler urlHandler; + private AjaxHandler ajaxHandler; + private Map flowHandlers = new HashMap(); /** @@ -63,6 +44,7 @@ public class FlowController extends AbstractController { public FlowController(FlowExecutor flowExecutor) { this.flowExecutor = flowExecutor; this.urlHandler = new DefaultFlowUrlHandler(); + this.ajaxHandler = new SpringJavascriptAjaxHandler(); // set the cache seconds property to 0 so no pages are cached by default for flows setCacheSeconds(0); } @@ -108,20 +90,10 @@ public class FlowController extends AbstractController { protected ServletExternalContext createServletExternalContext(HttpServletRequest request, HttpServletResponse response) { ServletExternalContext context = new ServletExternalContext(getServletContext(), request, response, urlHandler); - context.setAjaxRequest(isAjaxRequest(request)); + context.setAjaxRequest(ajaxHandler.isAjaxRequest(request)); return context; } - protected boolean isAjaxRequest(HttpServletRequest request) { - String acceptHeader = request.getHeader("Accept"); - String ajaxParam = request.getParameter(AJAX_SOURCE_PARAM); - if (AJAX_ACCEPT_CONTENT_TYPE.equals(acceptHeader) || StringUtils.hasText(ajaxParam)) { - return true; - } else { - return false; - } - } - protected MutableAttributeMap defaultFlowExecutionInputMap(HttpServletRequest request) { return new LocalAttributeMap(request.getParameterMap()); } @@ -200,10 +172,7 @@ public class FlowController extends AbstractController { private void sendRedirect(ServletExternalContext context, HttpServletResponse response, String targetUrl) throws IOException { if (context.isAjaxRequest()) { - if (context.redirectInPopup()) { - response.setHeader(POPUP_VIEW_HEADER, "true"); - } - response.setHeader(FLOW_REDIRECT_URL_HEADER, response.encodeRedirectURL(targetUrl)); + ajaxHandler.sendAjaxRedirect(response, targetUrl, context.redirectInPopup()); } else { response.sendRedirect(response.encodeRedirectURL(targetUrl)); } diff --git a/spring-webflow/src/main/java/org/springframework/webflow/mvc/FlowHandlerAdapter.java b/spring-webflow/src/main/java/org/springframework/webflow/mvc/FlowHandlerAdapter.java index 34f3117e..e76f03dc 100644 --- a/spring-webflow/src/main/java/org/springframework/webflow/mvc/FlowHandlerAdapter.java +++ b/spring-webflow/src/main/java/org/springframework/webflow/mvc/FlowHandlerAdapter.java @@ -7,7 +7,6 @@ import javax.servlet.http.HttpServletResponse; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.util.StringUtils; import org.springframework.web.context.support.WebApplicationObjectSupport; import org.springframework.web.servlet.HandlerAdapter; import org.springframework.web.servlet.ModelAndView; @@ -22,43 +21,43 @@ import org.springframework.webflow.execution.repository.NoSuchFlowExecutionExcep import org.springframework.webflow.executor.FlowExecutionResult; import org.springframework.webflow.executor.FlowExecutor; +/** + * A Spring MVC handler adapter that encapsulates the generic workflow associated with executing flows. Delegates to + * mapped flow handlers to manage the specific execution semantics required for particular registered flow definition. + * + * @author Keith Donald + */ public class FlowHandlerAdapter extends WebApplicationObjectSupport implements HandlerAdapter { - /** - * The response header to be set on an Ajax redirect - */ - private static final String FLOW_REDIRECT_URL_HEADER = "Flow-Redirect-URL"; - - /** - * The response header to be set on an redirect that should be issued from a popup window. - */ - private static final String POPUP_VIEW_HEADER = "Flow-Modal-View"; - - /** - * The accept header value that signifies an Ajax request. - */ - private static final String AJAX_ACCEPT_CONTENT_TYPE = "text/html;type=ajax"; - - /** - * Alternate request parameter to indicate an Ajax request for cases when control of the header is not available. - */ - private static final String AJAX_SOURCE_PARAM = "ajaxSource"; - private static final Log logger = LogFactory.getLog(FlowHandlerAdapter.class); private FlowExecutor flowExecutor; private FlowUrlHandler urlHandler; + private AjaxHandler ajaxHandler; + + /** + * Creates a new flow handler adapter + * @param flowExecutor the flow executor + */ public FlowHandlerAdapter(FlowExecutor flowExecutor) { this.flowExecutor = flowExecutor; this.urlHandler = new DefaultFlowUrlHandler(); + this.ajaxHandler = new SpringJavascriptAjaxHandler(); } + /** + * Returns the flow url handler. + */ public FlowUrlHandler getFlowUrlHandler() { return urlHandler; } + /** + * Sets the flow url handler + * @param urlHandler the flow url handler + */ public void setFlowUrlHandler(FlowUrlHandler urlHandler) { this.urlHandler = urlHandler; } @@ -96,20 +95,10 @@ public class FlowHandlerAdapter extends WebApplicationObjectSupport implements H protected ServletExternalContext createServletExternalContext(HttpServletRequest request, HttpServletResponse response) { ServletExternalContext context = new ServletExternalContext(getServletContext(), request, response, urlHandler); - context.setAjaxRequest(isAjaxRequest(request)); + context.setAjaxRequest(ajaxHandler.isAjaxRequest(request)); return context; } - protected boolean isAjaxRequest(HttpServletRequest request) { - String acceptHeader = request.getHeader("Accept"); - String ajaxParam = request.getParameter(AJAX_SOURCE_PARAM); - if (AJAX_ACCEPT_CONTENT_TYPE.equals(acceptHeader) || StringUtils.hasText(ajaxParam)) { - return true; - } else { - return false; - } - } - protected MutableAttributeMap defaultFlowExecutionInputMap(HttpServletRequest request) { return new LocalAttributeMap(request.getParameterMap()); } @@ -189,10 +178,7 @@ public class FlowHandlerAdapter extends WebApplicationObjectSupport implements H private void sendRedirect(ServletExternalContext context, HttpServletResponse response, String targetUrl) throws IOException { if (context.isAjaxRequest()) { - if (context.redirectInPopup()) { - response.setHeader(POPUP_VIEW_HEADER, "true"); - } - response.setHeader(FLOW_REDIRECT_URL_HEADER, response.encodeRedirectURL(targetUrl)); + ajaxHandler.sendAjaxRedirect(response, targetUrl, context.redirectInPopup()); } else { response.sendRedirect(response.encodeRedirectURL(targetUrl)); } diff --git a/spring-webflow/src/main/java/org/springframework/webflow/mvc/SpringJavascriptAjaxHandler.java b/spring-webflow/src/main/java/org/springframework/webflow/mvc/SpringJavascriptAjaxHandler.java new file mode 100644 index 00000000..a3ccde51 --- /dev/null +++ b/spring-webflow/src/main/java/org/springframework/webflow/mvc/SpringJavascriptAjaxHandler.java @@ -0,0 +1,53 @@ +package org.springframework.webflow.mvc; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.util.StringUtils; + +/** + * Ajax handler that works with Spring Javascript (Spring.js). + * + * @author Jeremy Grelle + * @author Keith Donald + */ +public class SpringJavascriptAjaxHandler implements AjaxHandler { + + /** + * The response header to be set on an Ajax redirect + */ + private static final String FLOW_REDIRECT_URL_HEADER = "Flow-Redirect-URL"; + + /** + * The response header to be set on an redirect that should be issued from a popup window. + */ + private static final String POPUP_VIEW_HEADER = "Flow-Modal-View"; + + /** + * The accept header value that signifies an Ajax request. + */ + private static final String AJAX_ACCEPT_CONTENT_TYPE = "text/html;type=ajax"; + + /** + * Alternate request parameter to indicate an Ajax request for cases when control of the header is not available. + */ + private static final String AJAX_SOURCE_PARAM = "ajaxSource"; + + public boolean isAjaxRequest(HttpServletRequest request) { + String acceptHeader = request.getHeader("Accept"); + String ajaxParam = request.getParameter(AJAX_SOURCE_PARAM); + if (AJAX_ACCEPT_CONTENT_TYPE.equals(acceptHeader) || StringUtils.hasText(ajaxParam)) { + return true; + } else { + return false; + } + } + + public void sendAjaxRedirect(HttpServletResponse response, String targetUrl, boolean popup) { + if (popup) { + response.setHeader(POPUP_VIEW_HEADER, "true"); + } + response.setHeader(FLOW_REDIRECT_URL_HEADER, response.encodeRedirectURL(targetUrl)); + } + +} diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/impl/DefaultFlowExecutionRepositoryTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/impl/DefaultFlowExecutionRepositoryTests.java index b54d0d0d..ad2d26b0 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/impl/DefaultFlowExecutionRepositoryTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/impl/DefaultFlowExecutionRepositoryTests.java @@ -53,31 +53,42 @@ public class DefaultFlowExecutionRepositoryTests extends TestCase { } public void testParseFlowExecutionKey() { - String key = "_c12345_k54321"; + String key = "c12345v54321"; FlowExecutionKey k = repository.parseFlowExecutionKey(key); assertEquals(key, k.toString()); } public void testParseBadlyFormattedFlowExecutionKey() { - String key = "_c12345"; + String key = "c12345"; try { repository.parseFlowExecutionKey(key); fail("Should have failed"); } catch (BadlyFormattedFlowExecutionKeyException e) { - assertEquals("_c12345", e.getInvalidKey()); + assertEquals("c12345", e.getInvalidKey()); + assertNotNull(e.getFormat()); + } + } + + public void testParseBadlyFormattedFlowExecutionKeyBadContinuationId() { + String key = "c12345vaaaa"; + try { + repository.parseFlowExecutionKey(key); + fail("Should have failed"); + } catch (BadlyFormattedFlowExecutionKeyException e) { + assertEquals("c12345vaaaa", e.getInvalidKey()); assertNotNull(e.getFormat()); } } public void testGetLock() { - FlowExecutionKey key = repository.parseFlowExecutionKey("_c12345_k54321"); + FlowExecutionKey key = repository.parseFlowExecutionKey("c12345v54321"); FlowExecutionLock lock = repository.getLock(key); assertNotNull(lock); lock.unlock(); } public void testGetLockNoSuchFlowExecution() { - FlowExecutionKey key = repository.parseFlowExecutionKey("_cbogus_k54321"); + FlowExecutionKey key = repository.parseFlowExecutionKey("cbogusv54321"); try { repository.getLock(key); fail("should have failed"); @@ -93,7 +104,9 @@ public class DefaultFlowExecutionRepositoryTests extends TestCase { execution.start(null, new MockExternalContext()); assertNotNull(execution.getKey()); repository.putFlowExecution(execution); - FlowExecution execution2 = repository.getFlowExecution(execution.getKey()); + String key = execution.getKey().toString(); + FlowExecutionKey parsedKey = repository.parseFlowExecutionKey(key); + FlowExecution execution2 = repository.getFlowExecution(parsedKey); assertSame(execution.getDefinition(), execution2.getDefinition()); assertEquals(execution.getActiveSession().getState().getId(), execution2.getActiveSession().getState().getId()); } diff --git a/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/support/CompositeFlowExecutionKeyTests.java b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/support/CompositeFlowExecutionKeyTests.java index 07ab3aad..9d461c81 100644 --- a/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/support/CompositeFlowExecutionKeyTests.java +++ b/spring-webflow/src/test/java/org/springframework/webflow/execution/repository/support/CompositeFlowExecutionKeyTests.java @@ -22,8 +22,8 @@ import org.springframework.webflow.conversation.impl.SimpleConversationId; public class CompositeFlowExecutionKeyTests extends TestCase { public void testToString() { - CompositeFlowExecutionKey key = new CompositeFlowExecutionKey(new SimpleConversationId("foo"), "bar"); - assertEquals("_cfoo_kbar", key.toString()); + CompositeFlowExecutionKey key = new CompositeFlowExecutionKey(new SimpleConversationId("1"), "1"); + assertEquals("c1v1", key.toString()); } public void testEquals() {