SessionBindingConversationManager.java

/*
 * Copyright 2004-2012 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
 *
 *      https://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.conversation.impl;

import org.springframework.webflow.context.ExternalContextHolder;
import org.springframework.webflow.conversation.Conversation;
import org.springframework.webflow.conversation.ConversationException;
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;

/**
 * Simple implementation of a conversation manager that stores conversations in the session attribute map.
 * <p>
 * Using the {@link #setMaxConversations(int) maxConversations} property, you can limit the number of concurrently
 * active conversations allowed in a single session. If the maximum is exceeded, the conversation manager will
 * automatically end the oldest conversation. The default is 5, which should be fine for most situations. Set it to -1
 * for no limit. Setting maxConversations to 1 allows easy resource cleanup in situations where there should only be one
 * active conversation per session.
 * 
 * @author Erwin Vervaet
 */
public class SessionBindingConversationManager implements ConversationManager {

	/**
	 * The name of the session attribute that will hold the conversation container used by this conversation manager.
	 * 
	 * To support multiple independent conversation containers in the same web application, for example, for use with
	 * multiple flow executors each configured with their own session-binding conversation manager, set this field's
	 * value to something unique.
	 * @see #setSessionKey(String)
	 */
	private String sessionKey = "webflowConversationContainer";

	/**
	 * The maximum number of active conversations allowed in a session. The default is 5. This is high enough for most
	 * practical situations and low enough to avoid excessive resource usage or easy denial of service attacks.
	 */
	private int maxConversations = 5;

	/**
	 * The lock timeout in seconds.
	 */
	private int lockTimeoutSeconds = 30;

	/**
	 * Returns the key this conversation manager uses to store conversation data in the session.
	 * @return the session key
	 */
	public String getSessionKey() {
		return sessionKey;
	}

	/**
	 * Sets the key this conversation manager uses to store conversation data in the session. If multiple session
	 * binding conversation managers are used in the same web application to back independent flow executors, this value
	 * should be unique among them.
	 * @param sessionKey the session key
	 */
	public void setSessionKey(String sessionKey) {
		this.sessionKey = sessionKey;
	}

	/**
	 * Returns the maximum number of allowed concurrent conversations. The default is 5.
	 */
	public int getMaxConversations() {
		return maxConversations;
	}

	/**
	 * Set the maximum number of allowed concurrent conversations. Set to -1 for no limit. The default is 5.
	 */
	public void setMaxConversations(int maxConversations) {
		this.maxConversations = maxConversations;
	}

	/**
	 * Returns the time period that can elapse before a timeout occurs on an attempt to acquire a conversation lock. The
	 * default is 30 seconds.
	 */
	public int getLockTimeoutSeconds() {
		return lockTimeoutSeconds;
	}

	/**
	 * Sets the time period that can elapse before a timeout occurs on an attempt to acquire a conversation lock. The
	 * default is 30 seconds.
	 * @param lockTimeoutSeconds the timeout period in seconds
	 */
	public void setLockTimeoutSeconds(int lockTimeoutSeconds) {
		this.lockTimeoutSeconds = lockTimeoutSeconds;
	}

	// implementing conversation manager

	public Conversation beginConversation(ConversationParameters conversationParameters) throws ConversationException {
		ConversationLock lock = new JdkConcurrentConversationLock(lockTimeoutSeconds);
		return getConversationContainer().createConversation(conversationParameters, lock);
	}

	public Conversation getConversation(ConversationId id) throws ConversationException {
		return getConversationContainer().getConversation(id);
	}

	public ConversationId parseConversationId(String encodedId) throws ConversationException {
		try {
			return new SimpleConversationId(Integer.valueOf(encodedId));
		} catch (NumberFormatException e) {
			throw new BadlyFormattedConversationIdException(encodedId, e);
		}
	}

	// hooks for subclassing

	protected ConversationContainer createConversationContainer() {
		return new ConversationContainer(maxConversations, sessionKey);
	}

	/**
	 * Obtain the conversation container from the session. Create a new empty container and add it to the session if no
	 * existing container can be found.
	 */
	protected final ConversationContainer getConversationContainer() {
		SharedAttributeMap<Object> sessionMap = ExternalContextHolder.getExternalContext().getSessionMap();
		synchronized (sessionMap.getMutex()) {
			ConversationContainer container = (ConversationContainer) sessionMap.get(sessionKey);
			if (container == null) {
				container = createConversationContainer();
				sessionMap.put(sessionKey, container);
			}
			return container;
		}
	}
}