blob: 46d1d9feea29b7e3336dd7427fbaae17ced606eb [file] [log] [blame]
/*
* Copyright 2004-2005 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 grails.pageflow;
import groovy.lang.Closure;
import java.lang.String;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang.StringUtils;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.webflow.Action;
import org.springframework.webflow.FlowAttributeMapper;
import org.springframework.webflow.RequestContext;
/**
* <p>Simple container object for a state in a page flow.
*
* <p>This class can map a view state, an action state, a sub flow state
* or a decision state.
*
* <p>One and only one of the following fields must be set, otherwise
* the state will be regarded as an end state:
*
* <ul>
* <li>{@link #action}
* <li>{@link #actionClass}
* <li>{@link #actionClosure}
* <li>{@link #actionFormDetails}
* <li>{@link #viewName}
* <li>{@link #viewClosure}
* <li>{@link #decisionExpression}
* <li>{@link #decisionClosure}
* <li>{@link #subFlowId}
* </ul>
*
* <p>In case one of the {@link #isActionState()}, {@link #isViewState()}, {@link #isDecisionState()} or
* {@link #isSubflowState()} methods is called before calling the {@link #validate()} method the result
* of those methods may be incorrect.
*
* @author Steven Devijver
* @since Jul 9, 2005
*/
public class State {
private static final String ACTION = "action";
private static final String VIEW = "view";
private static final String DECISION = "decision";
private static final String SUBFLOW = "subflow";
private static final String PROPERTIES = "properties";
private static final String MODEL = "model";
private static final String INPUT = "input";
private static final String OUTPUT = "output";
private static final String MAPPER = "mapper";
private static final String TRUE_STATE = "trueState";
private static final String FALSE_STATE = "falseState";
private static final String METHOD = "method";
private static final String END = "end";
/**
* <p>Class that implements org.springframework.webflow.Action
* or has an non-abstract execute method that takes a
* RequestContext parameter and returns either String or
* org.springframework.webflow.Event. The class should have
* a public no-arg constructor. A new instance of this class
* will be created as used as action handler.
*
* <p>{@link #actionProperties} will be used to set the properties
* of the action instance. If {@link #actionProperties} contains
* properties names that are not available on the action class
* an exception will be thrown.
*/
private Class actionClass = null;
/**
* <p>A org.springframework.webflow.Action instance that will
* be used as action handler.
*
* <p>{@link #actionProperties} will not be used to set the
* properties of this instance.
*
*/
private Action action = null;
/**
* <p>A closure that will handle the action logic.
*
* <p>The closure can have one parameter, the
* {@link org.springframework.webflow.RequestContext}
* instance.
*/
private Closure actionClosure = null;
/**
* <p>Delegates action handling to {@link org.springframework.webflow.action.FormAction}.
*
* <p>This is a convenience notation for the combination of {@link #actionClass} and
* {@link #actionProperties}.
*
* <p>All properties available on {@link org.springframework.webflow.action.FormAction} can
* be specified in the map.
*/
private Map actionFormDetails = null;
/**
* <p>Holds properties names and values for instances of {@link #actionProperties}.
*/
private Map actionProperties = null;
/**
* <p>Method that has to be executed on action.
*/
private String actionMethod = null;
/**
* <p>Holds the view name of this state.
*/
private String viewName = null;
/**
* <p>A closure that should return the view name.
*
* <p>The return value should be not null and of
* type {@link String} or {@link org.springframework.webflow.Event}
* otherwise an exception is thrown.
*/
private Closure viewClosure = null;
/**
* <p>Model names and values that can be set on the request context.
*
* <p>If a value is a {@link Closure} it will be executed everytime the
* model needs to be populated. If no value is returned the name will
* be added to the model with a null value.
*/
private Map viewModel = null;
/**
* <p>The name or id of a subflow.
*/
private String subFlowId = null;
/**
* <p>A closure that maps values in the flow scope to map. This map
* is added to the flow scope of the subflow that is started.
*
* <p>The closure can have one parameter, a {@link org.springframework.webflow.RequestContext}
* instance which is the request context of the parent flow.
*
* <p>The return value should be null or an instance of {@link Map}.
*
* <p>{@link #attributeMapperClass} and {@link #attributeMapper} should
* be null if this field is set.
*/
private Closure subFlowInput = null;
/**
* <p>A closure that puts values in the flow scope of an ending subflow
* in the flow scope of the resuming parent flow scope.
*
* <p>The closure can have two parameters: the subflow scope and the
* parent flow scope.
*
* <p>{@link #attributeMapperClass} and {@link #attributeMapper} should
* be null if this field is set.
*/
private Closure subFlowOutput = null;
/**
* <p>Class that implements {@link FlowAttributeMapper}. The class
* should have a public no-arg constructor. A new instance of this
* class will be created and used as flow attribute mapper between
* the parent flow and the subflow scopes.
*
* <p>{@link #attributeMapperProperties} will be used to set the
* properties on this flow attribute mapper attribute. If {@link #attributeMapperProperties}
* contains property names that are not available on the flow attribute
* mapper class an exception will be thrown.
*
* <p>{@link #subFlowInput}, {@link #subFlowOutput} and {@link #attributeMapper}
* should be null if this field is set.
*/
private Class attributeMapperClass = null;
/**
* <p>{@link FlowAttributeMapper} instance used as flow attribute mapper
* between the parent flwo and subflow scopes.
*
* <p>{@link #attributeMapperProperties} will not be used to set
* the properties of this instance.
*
* <p>{@link #subFlowInput}, {@link #subFlowOutput} and {@link #attributeMapperClass}
* should be null if this field is set.
*/
private FlowAttributeMapper attributeMapper = null;
/**
* <p>Hold property names and values for instances for {@link #attributeMapperClass}.
*/
private Map attributeMapperProperties = null;
/**
* <p>The state to transition to in case the decision state returns true.
*/
private String decisionTrueStateId = null;
/**
* <p>The state to transition to in case the decision state returns false.
*/
private String decisionFalseStateId = null;
/**
* <p>OGNL expression to test the outcome of the decision state based
* on the request context.
*/
private String decisionExpression = null;
/**
* <p>A closure that implements a test to determine the outcome of
* the decision state.
*
* <p>The closure can have one parameter, an instance of {@link org.springframework.webflow.RequestContext}.
*
* <p>Valid return values are:
*
* <ul>
* <li>true
* <li>false
* <li>{@link Boolean#TRUE}
* <li>{@link Boolean#FALSE}
* </ul>
*/
private Closure decisionClosure = null;
/**
* <p>The id or name of this state.
*/
private String id = null;
/**
* <p>Transitions for this state.
*/
private List transitions = null;
private boolean endState = false;
public State(String id) {
super();
setId(id);
this.transitions = new ArrayList();
}
public State(String id, Map attributes) {
this(id);
Assert.notNull(attributes);
for (Iterator iter = attributes.entrySet().iterator(); iter.hasNext();) {
Map.Entry entry = (Map.Entry)iter.next();
if (ACTION.equals(entry.getKey())) {
if (entry.getValue() == null) {
throw new IllegalArgumentException("Value for property [" + ACTION + "] on state [" + id + "] must not be null!");
} else if (entry.getValue() instanceof Class) {
actionClass = (Class)entry.getValue();
} else if (entry.getValue() instanceof Action) {
action = (Action)entry.getValue();
} else if (entry.getValue() instanceof Map) {
actionFormDetails = (Map)entry.getValue();
} else if (entry.getValue() instanceof Closure) {
actionClosure = (Closure)entry.getValue();
} else {
throw new IllegalArgumentException("Unsupported type [" + entry.getValue().getClass().getName() + "] for attribute [" + ACTION + "] on state [" + id + "]!");
}
} else if (VIEW.equals(entry.getKey())) {
if (entry.getValue() == null) {
throw new IllegalArgumentException("Value for property [" + VIEW + "] on state [" + id + "] must not be null!");
} else if (entry.getValue() instanceof String) {
viewName = (String)entry.getValue();
} else if (entry.getValue() instanceof Closure) {
viewClosure = (Closure)entry.getValue();
} else {
throw new IllegalArgumentException("Unsupported type [" + entry.getValue().getClass().getName() + "] for attribute [" + VIEW + "] on state [" + id + "]!");
}
} else if (DECISION.equals(entry.getKey())) {
if (entry.getValue() == null) {
throw new IllegalArgumentException("Value for property [" + DECISION + "] on state [" + id + "] must not be null!");
} else if (entry.getValue() instanceof String) {
decisionExpression = (String)entry.getValue();
} else if (entry.getValue() instanceof Closure) {
decisionClosure = (Closure)entry.getValue();
} else {
throw new IllegalArgumentException("Unsupported type [" + entry.getValue().getClass().getName() + "] for attribute [" + DECISION + "] on state [" + id + "]!");
}
} else if (SUBFLOW.equals(entry.getKey())) {
if (entry.getValue() == null) {
throw new IllegalArgumentException("Value for property [" + SUBFLOW + "] on state [" + id + "] must not be null!");
} else if (entry.getValue() instanceof String) {
subFlowId = (String)entry.getValue();
} else {
throw new IllegalArgumentException("Unsupported type [" + entry.getValue().getClass().getName() + "] for attribute [" + SUBFLOW + "] on state [" + id + "]!");
}
} else if (PROPERTIES.equals(entry.getKey())) {
if (entry.getValue() == null) {
// accept this
} else if (entry.getValue() instanceof Map) {
// if (isActionState()) {
// actionProperties = (Map)entry.getValue();
// } else if (isSubflowState()) {
// attributeMapperProperties = (Map)entry.getValue();
// } else if (isViewState()) {
// throw new IllegalArgumentException("The [" + PROPERTIES + "] property is not supported for view state [" + id + "]!");
// } else if (isDecisionState()) {
// throw new IllegalArgumentException("The [" + PROPERTIES + "] property is not supported for decision state [" + id + "]!");
// } else {
// throw new IllegalStateException("Could not determine state type when handling [" + PROPERTIES + "] property for state [" + id +"]. Try moving the state discriminator attribute ([" + ACTION + "] or [" + SUBFLOW +"]) in front of the [" + PROPERTIES + "] property if have specified it!");
// }
/*
* This is a hack. The code above does not work as order of attribute entries is undertermined.
* We set both actionProperties and attributeMapperProperties.
* As a side effect attribute with discriminator property and with properties property will
* not fail in this construtor but will fail in validate method.
*/
actionProperties = (Map)entry.getValue();
attributeMapperProperties = (Map)entry.getValue();
} else {
throw new IllegalArgumentException("Unsuppored type [" + entry.getValue().getClass().getName() + "] for attribute [" + PROPERTIES + "] on state [" + id + "]!");
}
} else if (MODEL.equals(entry.getKey())) {
if (entry.getValue() == null) {
// accept this
} else if (entry.getValue() instanceof Map) {
viewModel = (Map)entry.getValue();
} else {
throw new IllegalArgumentException("Unsupported type [" + entry.getValue().getClass().getName() + "] for attribute [" + MODEL + "] on state [" + id + "]!");
}
} else if (INPUT.equals(entry.getKey())) {
if (entry.getValue() == null) {
// accept this
} else if (entry.getValue() instanceof Closure) {
subFlowInput = (Closure)entry.getValue();
} else {
throw new IllegalArgumentException("Unsupported type [" + entry.getValue().getClass().getName() + "] for attribute [" + INPUT + "] on state [" + id + "]!");
}
} else if (OUTPUT.equals(entry.getKey())) {
if (entry.getValue() == null) {
// accept this
} else if (entry.getValue() instanceof Closure) {
subFlowOutput = (Closure)entry.getValue();
} else {
throw new IllegalArgumentException("Unsupported type [" + entry.getValue().getClass().getName() + "] for attribute [" + OUTPUT + "] on state [" + id + "]!");
}
} else if (MAPPER.equals(entry.getKey())) {
if (entry.getValue() == null) {
// accept this
} else if (entry.getValue() instanceof Class) {
attributeMapperClass = (Class)entry.getValue();
} else if (entry.getValue() instanceof FlowAttributeMapper) {
attributeMapper = (FlowAttributeMapper)entry.getValue();
} else {
throw new IllegalArgumentException("Unsupported type [" + entry.getValue().getClass().getName() + "] attribute [" + MAPPER + "] on state [" + id + "]!");
}
} else if (TRUE_STATE.equals(entry.getKey())) {
if (entry.getValue() == null) {
throw new IllegalArgumentException("Value for property [" + TRUE_STATE + "] on state [" + id + "] must not be null!");
} else if (entry.getValue() instanceof String) {
decisionTrueStateId = (String)entry.getValue();
} else {
throw new IllegalArgumentException("Unsupported type [" + entry.getValue().getClass().getName() + "] for attribute [" + TRUE_STATE + "] on state [" + id + "]!");
}
} else if (FALSE_STATE.equals(entry.getKey())) {
if (entry.getValue() == null) {
throw new IllegalArgumentException("Value for property [" + FALSE_STATE + "] on state [" + id + "] must not be null!");
} else if (entry.getValue() instanceof String) {
decisionFalseStateId = (String)entry.getValue();
} else {
throw new IllegalArgumentException("Unsupported type [" + entry.getValue().getClass().getName() + "] for attribute [" + FALSE_STATE + "] on state [" + id + "]!");
}
} else if (METHOD.equals(entry.getKey())) {
if (entry.getValue() == null) {
throw new IllegalArgumentException("Value for property [" + METHOD + "] on state [" + id + "] must not be null!");
} else if (entry.getValue() instanceof String) {
actionMethod = (String)entry.getValue();
} else {
throw new IllegalArgumentException("Unsupported type [" + entry.getValue().getClass().getName() + "] for attribute [" + METHOD + "] on state [" + id + "]!");
}
} else if (END.equals(entry.getKey())) {
if (entry.getValue() == null) {
throw new IllegalArgumentException("Value for property [" + END + "] on state [" + id + "] must not be null!");
} else if (entry.getValue() instanceof Boolean) {
endState = ((Boolean)entry.getValue()).booleanValue();
} else {
throw new IllegalArgumentException("Unsupported type [" + entry.getValue().getClass().getName() + "] for attribute [" + END + "] on state [" + id + "]!");
}
} else {
throw new IllegalArgumentException("Unknow property [" + entry.getValue() + "] on state [" + id + "]!");
}
}
}
/**
* <p>Adds a transition to this state.
*/
public void addTransition(Transition transition) {
Assert.notNull(transition);
for (Iterator iter = this.transitions.iterator(); iter.hasNext();) {
Transition tmpTransition = (Transition)iter.next();
if (tmpTransition.getTargetStateId().equals(transition.getTargetStateId())) {
throw new IllegalArgumentException("This state [" + getId() + "] already has a transition to state [" + tmpTransition.getTargetStateId() + "]!");
}
}
this.transitions.add(transition);
}
/**
* <p>Returns the list of transitions for this state.
*
* @return transitions for this state.
*/
public List getTransitions() {
return this.transitions;
}
public Action getAction() {
return action;
}
public void setAction(Action action) {
this.action = action;
}
public Class getActionClass() {
return actionClass;
}
public void setActionClass(Class actionClass) {
this.actionClass = actionClass;
}
public Closure getActionClosure() {
return actionClosure;
}
public void setActionClosure(Closure actionClosure) {
this.actionClosure = actionClosure;
}
public Map getActionFormDetails() {
return actionFormDetails;
}
public void setActionFormDetails(Map actionFormDetails) {
this.actionFormDetails = actionFormDetails;
}
public Map getActionProperties() {
return actionProperties;
}
public void setActionProperties(Map actionProperties) {
this.actionProperties = actionProperties;
}
public FlowAttributeMapper getAttributeMapper() {
return attributeMapper;
}
public void setAttributeMapper(FlowAttributeMapper attributeMapper) {
this.attributeMapper = attributeMapper;
}
public Class getAttributeMapperClass() {
return attributeMapperClass;
}
public void setAttributeMapperClass(Class attributeMapperClass) {
this.attributeMapperClass = attributeMapperClass;
}
public Map getAttributeMapperProperties() {
return attributeMapperProperties;
}
public void setAttributeMapperProperties(Map attributeMapperProperties) {
this.attributeMapperProperties = attributeMapperProperties;
}
public Closure getDecisionClosure() {
return decisionClosure;
}
public void setDecisionClosure(Closure decisionClosure) {
this.decisionClosure = decisionClosure;
}
public String getDecisionExpression() {
return decisionExpression;
}
public void setDecisionExpression(String decisionExpression) {
this.decisionExpression = decisionExpression;
}
public String getDecisionFalseStateId() {
return decisionFalseStateId;
}
public void setDecisionFalseStateId(String decisionFalseStateId) {
this.decisionFalseStateId = decisionFalseStateId;
}
public String getDecisionTrueStateId() {
return decisionTrueStateId;
}
public void setDecisionTrueStateId(String decisionTrueStateId) {
this.decisionTrueStateId = decisionTrueStateId;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getSubFlowId() {
return subFlowId;
}
public void setSubFlowId(String subFlowId) {
this.subFlowId = subFlowId;
}
public Closure getSubFlowInput() {
return subFlowInput;
}
public void setSubFlowInput(Closure subFlowInput) {
this.subFlowInput = subFlowInput;
}
public Closure getSubFlowOutput() {
return subFlowOutput;
}
public void setSubFlowOutput(Closure subFlowOutput) {
this.subFlowOutput = subFlowOutput;
}
public Closure getViewClosure() {
return viewClosure;
}
public void setViewClosure(Closure viewClosure) {
this.viewClosure = viewClosure;
}
public Map getViewModel() {
return viewModel;
}
public void setViewModel(Map viewModel) {
this.viewModel = viewModel;
}
public String getViewName() {
return viewName;
}
public void setViewName(String viewName) {
this.viewName = viewName;
}
public String getActionMethod() {
return actionMethod;
}
public void setActionMethod(String actionMethod) {
this.actionMethod = actionMethod;
}
/**
* <p>Validate the various fields of this class.
*/
public void validate() throws IllegalStateException {
Object[] fields = new Object[] { action, actionClass, actionClosure, actionFormDetails, decisionClosure, decisionExpression, subFlowId, viewClosure, viewName };
boolean oneNotNull = false;
if (StringUtils.isBlank(id)) {
throw new IllegalStateException("State id must be specified!");
}
for (int i = 0; i < fields.length; i++) {
if (oneNotNull) {
if (fields[i] != null) {
throw new IllegalStateException("Could not determine state type, more than one discriminator field was specified!");
}
} else {
oneNotNull = fields[i] != null;
}
}
// if (!oneNotNull) {
// throw new IllegalStateException("Could not determine state type, no descriminator field was specified!");
// }
if (isSubflowState()) {
if ((subFlowInput != null || subFlowOutput != null) && (attributeMapper != null || attributeMapperClass != null)) {
throw new IllegalStateException("attribute mapper and attribute mapper class must be null if either subflow input or subflow output are set!");
} else if (attributeMapper != null && (subFlowInput != null || subFlowOutput != null || attributeMapperClass != null)) {
throw new IllegalStateException("subflow input and subflow output and attribute mapper class must be null if attribute mapper is set!");
} else if (attributeMapperClass != null && (subFlowInput != null || subFlowOutput != null || attributeMapper != null)) {
throw new IllegalStateException("subflow input and subflow output and attribute mapper class must be null if attribute mapper class is set!");
}
}
if (isDecisionState()) {
if (StringUtils.isBlank(decisionTrueStateId)) {
throw new IllegalStateException("Decision true state id must be set!");
} else if (StringUtils.isBlank(decisionFalseStateId)) {
throw new IllegalStateException("Decision false state id must be set!");
}
}
if (actionClass != null) {
if (!Action.class.isAssignableFrom(actionClass) && !ClassUtils.hasMethod(actionClass, "execute", new Class[] { RequestContext.class })) {
throw new IllegalStateException("Action class [" + actionClass.getName() + "] does not implement [" + Action.class.getName() + "] and has no public method [execute(" + RequestContext.class.getName() + ")]!");
} else if (Modifier.isAbstract(actionClass.getModifiers())) {
throw new IllegalStateException("Action class [" + actionClass.getName() + "] is abstract!");
}
}
if (attributeMapperClass != null) {
if (!FlowAttributeMapper.class.isAssignableFrom(attributeMapperClass)) {
throw new IllegalStateException("Flow attribute mapper class [" + attributeMapperClass.getName() + "] does not implement [" + FlowAttributeMapper.class.getName() + "]!");
} else if (Modifier.isAbstract(attributeMapperClass.getModifiers())) {
throw new IllegalStateException("Flow attribute mapper class [" + attributeMapperClass.getName() + "] is abstract!");
}
}
}
/**
* <p>Is this state an action state.
*
* @return true is this state is an action state.
*/
public boolean isActionState() {
return (action != null || actionClass != null || actionClosure != null || actionFormDetails != null);
}
/**
* <p>Is this state a view state.
*
* @return true if this state in a view state.
*/
public boolean isViewState() {
return ((viewName != null || viewClosure != null) && !endState);
}
/**
* <p>Is this state a decision state.
*
* @return true if this state is a decision state.
*/
public boolean isDecisionState() {
return (decisionExpression != null || decisionClosure != null);
}
/**
* <p>Is this state a subflow state.
*
* @return true if this state is a subflow state.
*/
public boolean isSubflowState() {
return (subFlowId != null);
}
/**
* <p>Is this state an end state.
*
* @return true if this state is an end state.
*/
public boolean isEndState() {
return ((endState && isViewState()) || (!isActionState() && !isViewState() && !isDecisionState() && !isSubflowState()));
}
public String toString() {
return new ToStringCreator(this)
.append("action=" + action)
.append("actionClass=" + actionClass)
.append("actionClosure=" + actionClosure)
.append("actionFormDetail=" + actionFormDetails)
.append("actionProperties=" + actionProperties)
.append("attributeMapper=" + attributeMapper)
.append("attributeMapperClass=" + attributeMapperClass)
.append("attributeMapperProperties=" + attributeMapperProperties)
.append("decisionClosure=" + decisionClosure)
.append("decisionExpression=" + decisionExpression)
.append("decisionTrueStateId=" + decisionTrueStateId)
.append("decisionFalseStateId=" + decisionFalseStateId)
.append("subFlowId=" + subFlowId)
.append("subFlowInput=" + subFlowInput)
.append("subFlowOutput" + subFlowOutput)
.append("transitions=" + transitions)
.append("viewClosure=" + viewClosure)
.append("viewName=" + viewName)
.append("viewModel=" + viewModel)
.append("\n").toString();
}
}