ReplicationOptimizedSessionBindingConversationManager.java

package com.github.gfernandez598.swf.conversation.optforrepl;

/*
 * #%L
 * Spring Web Flow OptForRepl
 * %%
 * Copyright (C) 2015 gfernandez598
 * %%
 * 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.
 * #L%
 */

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;
import org.springframework.webflow.conversation.ConversationId;
import org.springframework.webflow.conversation.ConversationManager;
import org.springframework.webflow.conversation.ConversationParameters;
import org.springframework.webflow.conversation.impl.BadlyFormattedConversationIdException;
import org.springframework.webflow.conversation.impl.SimpleConversationId;
import org.springframework.webflow.core.collection.SharedAttributeMap;

import com.github.gfernandez598.swf.util.RandomUUIDUidGenerator;
import com.github.gfernandez598.swf.util.UidGenerator;

/**
 * <p>
 * Base on workaround attached to issue SWF-1030.
 * </p>
 * 
 * @see <a
 *      href="https://jira.spring.io/browse/SWF-1030">https://jira.spring.io/browse/SWF-1030</a>
 * 
 * @author gfernandez598
 * 
 */
public class ReplicationOptimizedSessionBindingConversationManager implements
		ConversationManager {
	private static final Log logger = LogFactory
			.getLog(ReplicationOptimizedSessionBindingConversationManager.class);

	/**
	 * The name of the session attribute that will hold the conversation
	 * container used by this conversation manager.
	 * <p/>
	 * 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 = "webflow.conversationContainer";

	/**
	 * The conversation uid generation strategy to use.
	 */
	private UidGenerator conversationIdGenerator = new RandomUUIDUidGenerator();

	/**
	 * 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 used generator for conversation ids. Defaults to
	 * {@link RandomUUIDUidGenerator}.
	 */
	public UidGenerator getConversationIdGenerator() {
		return conversationIdGenerator;
	}

	/**
	 * Sets the configured generator for conversation ids.
	 */
	public void setConversationIdGenerator(UidGenerator uidGenerator) {
		this.conversationIdGenerator = uidGenerator;
	}

	/**
	 * 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;
	}

	/**
	 * 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 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;
	}

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

	public Conversation getConversation(ConversationId id)
			throws ConversationException {
		if (logger.isDebugEnabled()) {
			logger.debug("Getting conversation " + id);
		}
		return getConversationContainer().getConversation(id);
	}

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

	// internal helpers

	/**
	 * 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.
	 */
	private SessionMapConversationContainer getConversationContainer() {
		SharedAttributeMap sessionMap = ExternalContextHolder
				.getExternalContext().getSessionMap();
		int lala;
		
		synchronized (sessionMap.getMutex()) {
			SessionMapConversationContainer container = (SessionMapConversationContainer) sessionMap
					.get(sessionKey);
			if (container == null) {
				container = new SessionMapConversationContainer(
						maxConversations, lockTimeoutSeconds, sessionKey);
				sessionMap.put(sessionKey, container);
			}
			return container;
		}
		
	}
}