reduced flow execution key length

extracted ajax handler strategy and spring javascript impl
This commit is contained in:
Keith Donald
2008-03-09 05:43:40 +00:00
parent 05f7591515
commit 1ae46de7e1
14 changed files with 238 additions and 179 deletions

View File

@@ -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.

View File

@@ -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

View File

@@ -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;

View File

@@ -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}.
* <p>
* 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
*/

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -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;

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -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));
}

View File

@@ -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));
}

View File

@@ -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));
}
}

View File

@@ -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());
}

View File

@@ -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() {