/*
* 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
*
* 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.
*/
package org.springframework.webflow.execution.repository.support;
import java.io.Serializable;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
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.NoSuchConversationException;
import org.springframework.webflow.definition.FlowDefinition;
import org.springframework.webflow.execution.FlowExecution;
import org.springframework.webflow.execution.FlowExecutionFactory;
import org.springframework.webflow.execution.FlowExecutionKey;
import org.springframework.webflow.execution.FlowExecutionKeyFactory;
import org.springframework.webflow.execution.repository.BadlyFormattedFlowExecutionKeyException;
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;
/**
* Abstract base class for flow execution repository implementations. Does not make any assumptions about the storage
* medium used to store active flow executions. Mandates the use of a {@link FlowExecutionStateRestorer}, used to
* rehydrate a flow execution after it has been obtained from storage from resume.
* <p>
* The configured {@link FlowExecutionStateRestorer} should be compatible with the chosen {@link FlowExecution}
* implementation and its {@link FlowExecutionFactory}.
*
* @author Keith Donald
* @author Erwin Vervaet
*/
public abstract class AbstractFlowExecutionRepository implements FlowExecutionRepository, FlowExecutionKeyFactory {
/**
* Logger, usable in subclasses
*/
protected final Log logger = LogFactory.getLog(getClass());
private ConversationManager conversationManager;
private boolean alwaysGenerateNewNextKey = true;
/**
* Constructor for use in subclasses.
* @param conversationManager the conversation manager to use
*/
protected AbstractFlowExecutionRepository(ConversationManager conversationManager) {
Assert.notNull(conversationManager, "The conversation manager is required");
this.conversationManager = conversationManager;
}
/**
* The conversation service to delegate to for managing conversations initiated by this repository.
*/
public ConversationManager getConversationManager() {
return conversationManager;
}
/**
* The flag indicating if a new {@link FlowExecutionKey} should always be generated before each put call.
*/
public boolean getAlwaysGenerateNewNextKey() {
return alwaysGenerateNewNextKey;
}
/**
* Sets the 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.
*/
public void setAlwaysGenerateNewNextKey(boolean alwaysGenerateNewNextKey) {
this.alwaysGenerateNewNextKey = alwaysGenerateNewNextKey;
}
// implementing flow execution key factory
public FlowExecutionKey getKey(FlowExecution execution) {
CompositeFlowExecutionKey key = (CompositeFlowExecutionKey) execution.getKey();
if (key == null) {
Conversation conversation = beginConversation(execution);
ConversationId executionId = conversation.getId();
return new CompositeFlowExecutionKey(executionId, nextSnapshotId(executionId));
} else {
if (alwaysGenerateNewNextKey) {
return new CompositeFlowExecutionKey(key.getExecutionId(), nextSnapshotId(key.getExecutionId()));
} else {
return execution.getKey();
}
}
}
// implementing flow execution repository
public FlowExecutionKey parseFlowExecutionKey(String encodedKey) throws FlowExecutionRepositoryException {
if (!StringUtils.hasText(encodedKey)) {
throw new BadlyFormattedFlowExecutionKeyException(encodedKey,
"The string-encoded flow execution key is required");
}
String[] keyParts = CompositeFlowExecutionKey.keyParts(encodedKey);
Serializable executionId = parseExecutionId(keyParts[0], encodedKey);
Serializable snapshotId = parseSnapshotId(keyParts[1], encodedKey);
return new CompositeFlowExecutionKey(executionId, snapshotId);
}
public FlowExecutionLock getLock(FlowExecutionKey key) throws FlowExecutionRepositoryException {
return new ConversationBackedFlowExecutionLock(getConversation(key));
}
public void removeFlowExecution(FlowExecution flowExecution) throws FlowExecutionRepositoryException {
assertKeySet(flowExecution);
if (logger.isDebugEnabled()) {
logger.debug("Removing flow execution '" + flowExecution + "' from repository");
}
endConversation(flowExecution);
}
// abstract repository methods to be overridden by subclasses
/**
* The next snapshot id to use for a {@link FlowExecution} instance. Called when {@link #getKey(FlowExecution)
* getting a flow execution key}.
* @return the id of the flow execution
*/
protected abstract Serializable nextSnapshotId(Serializable executionId);
public abstract FlowExecution getFlowExecution(FlowExecutionKey key) throws FlowExecutionRepositoryException;
public abstract void putFlowExecution(FlowExecution flowExecution) throws FlowExecutionRepositoryException;
// hooks for use in subclasses
/**
* Factory method that maps a new flow execution to a descriptive {@link ConversationParameters conversation
* parameters} object.
* @param flowExecution the new flow execution
* @return the conversation parameters object to pass to the conversation manager when the conversation is started
*/
protected ConversationParameters createConversationParameters(FlowExecution flowExecution) {
FlowDefinition flow = flowExecution.getDefinition();
return new ConversationParameters(flow.getId(), flow.getCaption(), flow.getDescription());
}
/**
* Returns the conversation governing the {@link FlowExecution} with the provided key.
* @param key the flow execution key
* @return the governing conversation
* @throws NoSuchFlowExecutionException when the conversation for identified flow execution cannot be found
*/
protected Conversation getConversation(FlowExecutionKey key) throws NoSuchFlowExecutionException {
try {
return getConversation(((CompositeFlowExecutionKey) key).getExecutionId());
} catch (NoSuchConversationException e) {
throw new NoSuchFlowExecutionException(key, e);
}
}
/**
* Returns the conversation governing the logical flow execution with the given execution id.
* @param executionId the flow execution id
* @return the governing conversation
* @throws NoSuchConversationException when the conversation for identified flow execution cannot be found
*/
protected Conversation getConversation(Serializable executionId) throws NoSuchConversationException {
return conversationManager.getConversation((ConversationId) executionId);
}
/**
* 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(
"The key for the flow execution is null; make sure the key is assigned first. Execution Details = "
+ execution);
}
}
// internal helpers
private Conversation beginConversation(FlowExecution execution) {
ConversationParameters parameters = createConversationParameters(execution);
Conversation conversation = conversationManager.beginConversation(parameters);
return conversation;
}
private ConversationId parseExecutionId(String encodedId, String encodedKey)
throws BadlyFormattedFlowExecutionKeyException {
try {
return conversationManager.parseConversationId(encodedId);
} catch (ConversationException e) {
throw new BadlyFormattedFlowExecutionKeyException(encodedKey, CompositeFlowExecutionKey.getFormat(), e);
}
}
private Serializable parseSnapshotId(String encodedId, String encodedKey)
throws BadlyFormattedFlowExecutionKeyException {
try {
return Integer.valueOf(encodedId);
} catch (NumberFormatException e) {
throw new BadlyFormattedFlowExecutionKeyException(encodedKey, CompositeFlowExecutionKey.getFormat(), e);
}
}
private Conversation endConversation(FlowExecution flowExecution) {
Conversation conversation = getConversation(flowExecution.getKey());
conversation.end();
return conversation;
}
}