DefaultFlowExecutionRepository.java

/*
 * Copyright 2004-2008 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.execution.repository.impl;

import java.io.Serializable;

import org.springframework.webflow.conversation.Conversation;
import org.springframework.webflow.conversation.ConversationManager;
import org.springframework.webflow.execution.FlowExecution;
import org.springframework.webflow.execution.FlowExecutionKey;
import org.springframework.webflow.execution.repository.FlowExecutionRestorationFailureException;
import org.springframework.webflow.execution.repository.snapshot.AbstractSnapshottingFlowExecutionRepository;
import org.springframework.webflow.execution.repository.snapshot.FlowExecutionSnapshot;
import org.springframework.webflow.execution.repository.snapshot.FlowExecutionSnapshotFactory;
import org.springframework.webflow.execution.repository.snapshot.SnapshotNotFoundException;

/**
 * The default flow execution repository implementation. Takes <i>one to {@link #getMaxSnapshots() max}</i> flow
 * execution snapshots, where each snapshot represents a copy of a {@link FlowExecution} taken at a point in time.
 * Snapshots are created via a {@link FlowExecutionSnapshotFactory} and that may or may not involve creating a copy of a
 * flow execution through Java serialization. In particular when the flow-execution-repository element is configured
 * with max-execution-snapshots="0", creating snapshot copies is effectively turned off.
 * <p>
 * The set of active flow executions are managed by a {@link ConversationManager} implementation, which this repository
 * delegates to.
 * <p>
 * This repository is responsible for:
 * <ul>
 * <li>Beginning a new {@link Conversation} when a {@link FlowExecution} is assigned a persistent key. Each conversation
 * is assigned a unique conversation id which forms one part of the flow execution key.
 * <li>Taking {@link FlowExecutionSnapshot execution snapshots} to persist flow execution state. A snapshot is a copy of
 * the execution created at a point in time <i>that can be restored and continued</i>. Snapshotting supports users going
 * back in their browser to continue their flow execution from a previoius point.
 * <li>Ending conversations when flow executions end.
 * </ul>
 * <p>
 * This repository implementation also provides support for <i>execution invalidation after completion</i>, where once a
 * logical flow execution completes, it and all of its snapshots are removed. This cleans up memory and prevents the
 * possibility of duplicate submission after completion.
 * 
 * @author Keith Donald
 */
public class DefaultFlowExecutionRepository extends AbstractSnapshottingFlowExecutionRepository {

	/**
	 * The conversation attribute that stores the group of flow execution snapshots.
	 */
	private static final String SNAPSHOT_GROUP_ATTRIBUTE = "flowExecutionSnapshotGroup";

	/**
	 * The maximum number of snapshots that can be taken per execution. The default is 30, which is generally high
	 * enough not to interfere with the user experience of normal users using the back button, but low enough to avoid
	 * excessive resource usage or denial of service attacks.
	 */
	private int maxSnapshots = 30;

	/**
	 * Create a new default flow execution repository using the given state restorer, conversation manager, and snapshot
	 * factory.
	 * @param conversationManager the conversation manager to use
	 * @param snapshotFactory the flow execution snapshot factory to use
	 */
	public DefaultFlowExecutionRepository(ConversationManager conversationManager,
			FlowExecutionSnapshotFactory snapshotFactory) {
		super(conversationManager, snapshotFactory);
	}

	/**
	 * Returns the max number of snapshots allowed per flow execution by this repository.
	 */
	public int getMaxSnapshots() {
		return maxSnapshots;
	}

	/**
	 * Sets the maximum number of snapshots allowed per flow execution by this repository. Use -1 for unlimited. The
	 * default is 30.
	 */
	public void setMaxSnapshots(int maxSnapshots) {
		this.maxSnapshots = maxSnapshots;
	}

	// supporting flow execution key factory impl

	protected Serializable nextSnapshotId(Serializable executionId) {
		return getSnapshotGroup(getConversation(executionId)).nextSnapshotId();
	}

	// implementing flow execution repository

	public FlowExecution getFlowExecution(FlowExecutionKey key) {
		if (logger.isDebugEnabled()) {
			logger.debug("Getting flow execution with key '" + key + "'");
		}
		Conversation conversation = getConversation(key);
		FlowExecutionSnapshot snapshot;
		try {
			snapshot = getSnapshotGroup(conversation).getSnapshot(getSnapshotId(key));
		} catch (SnapshotNotFoundException e) {
			throw new FlowExecutionRestorationFailureException(key, e);
		}
		return restoreFlowExecution(snapshot, key, conversation);
	}

	public void putFlowExecution(FlowExecution flowExecution) {
		assertKeySet(flowExecution);
		if (logger.isDebugEnabled()) {
			logger.debug("Putting flow execution '" + flowExecution + "' into repository");
		}
		FlowExecutionKey key = flowExecution.getKey();
		Conversation conversation = getConversation(key);
		FlowExecutionSnapshotGroup snapshotGroup = getSnapshotGroup(conversation);
		FlowExecutionSnapshot snapshot = snapshot(flowExecution);
		if (logger.isDebugEnabled()) {
			logger.debug("Adding snapshot to group with id " + getSnapshotId(key));
		}
		snapshotGroup.addSnapshot(getSnapshotId(key), snapshot);
		putConversationScope(flowExecution, conversation);
	}

	// implementing flow execution key factory

	public void updateFlowExecutionSnapshot(FlowExecution execution) {
		FlowExecutionKey key = execution.getKey();
		if (key == null) {
			return;
		}
		Conversation conversation = getConversation(key);
		getSnapshotGroup(conversation).updateSnapshot(getSnapshotId(key), snapshot(execution));
	}

	public void removeFlowExecutionSnapshot(FlowExecution execution) {
		FlowExecutionKey key = execution.getKey();
		if (key == null) {
			return;
		}
		Conversation conversation = getConversation(key);
		getSnapshotGroup(conversation).removeSnapshot(getSnapshotId(key));
	}

	public void removeAllFlowExecutionSnapshots(FlowExecution execution) {
		FlowExecutionKey key = execution.getKey();
		if (key == null) {
			return;
		}
		Conversation conversation = getConversation(execution.getKey());
		getSnapshotGroup(conversation).removeAllSnapshots();
	}

	// hooks for subclassing

	protected FlowExecutionSnapshotGroup createFlowExecutionSnapshotGroup() {
		SimpleFlowExecutionSnapshotGroup group = new SimpleFlowExecutionSnapshotGroup();
		group.setMaxSnapshots(maxSnapshots);
		return group;
	}

	/**
	 * Returns the snapshot group associated with the governing conversation.
	 * @param conversation the conversation where the snapshot group is stored
	 * @return the snapshot group
	 */
	protected FlowExecutionSnapshotGroup getSnapshotGroup(Conversation conversation) {
		FlowExecutionSnapshotGroup group = (FlowExecutionSnapshotGroup) conversation
				.getAttribute(SNAPSHOT_GROUP_ATTRIBUTE);
		if (group == null) {
			group = createFlowExecutionSnapshotGroup();
			conversation.putAttribute(SNAPSHOT_GROUP_ATTRIBUTE, group);
		}
		return group;
	}
}