All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.statefulj.fsm.FSM Maven / Gradle / Ivy

/***
 *
 * Copyright 2014 Andrew Hall
 *
 * 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.statefulj.fsm;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.statefulj.fsm.model.Action;
import org.statefulj.fsm.model.State;
import org.statefulj.fsm.model.StateActionPair;
import org.statefulj.fsm.model.Transition;
import org.statefulj.fsm.model.impl.DeterministicTransitionImpl;
import org.statefulj.fsm.model.impl.StateImpl;
import org.statefulj.persistence.memory.MemoryPersisterImpl;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * The FSM is responsible for the processing the event with the current State and persisting
 * the State with the composite Persister
 *
 * @author Andrew Hall
 *
 */
public class FSM {

	private static final Logger logger = LoggerFactory.getLogger(FSM.class);

	private static final int DEFAULT_RETRIES = 20;
	private static final int DEFAULT_RETRY_INTERVAL = 250;  // 250 ms

	private int retryAttempts = DEFAULT_RETRIES;
	private int retryInterval = DEFAULT_RETRY_INTERVAL;

	private Persister persister;
	private String name = "FSM";

	/**
	 * FSM Constructor with the name of the FSM
	 *
	 * @param name Name associated with the FSM
	 */
	public FSM(String name) {
		this.name = name;
	}

	/**
	 * FSM Constructor with the Persister responsible for setting the State on the Entity
	 *
	 * @param persister Persister responsible for setting the State on the Entity
	 */
	public FSM(Persister persister) {
		this.persister = persister;
	}

	/**
	 * FSM Constructor with the name of the FSM and Persister responsible for setting the State on the Entity
	 *
	 * @param name Name associated with the FSM
	 * @param persister Persister responsible for setting the State on the Entity
	 */
	public FSM(String name, Persister persister) {
		this.name = name;
		this.persister = persister;
	}

	/**
	 * FSM Constructor
	 *
	 * @param name Name associated with the FSM
	 * @param persister Persister responsible for setting the State on the Entity
	 * @param retryAttempts Number of Retry Attempts.  A value of -1 indicates unlimited Attempts
	 * @param retryInterval Time between Retry Attempts in milliseconds
	 */
	public FSM(String name, Persister persister, int retryAttempts, int retryInterval) {
		this.name = name;
		this.persister = persister;
		this.retryAttempts = retryAttempts;
		this.retryInterval = retryInterval;
	}

	/**
	 * Process event.  Will handle all retry attempts.  If attempts exceed maximum retries,
	 * it will throw a TooBusyException.
	 *
	 * @param stateful The Stateful Entity
	 * @param event The Event
	 * @param args Optional parameters to pass into the Action
	 * @return The current State
	 * @throws TooBusyException Exception indicating that we've exceeded the number of RetryAttempts
	 */
	public State onEvent(T stateful, String event, Object ... args) throws TooBusyException {

		int attempts = 0;

		while(this.retryAttempts == -1 || attempts < this.retryAttempts) {
			try {
				State current = this.getCurrentState(stateful);

				// Fetch the transition for this event from the current state
				//
				Transition transition = this.getTransition(event, current);

				// Is there one?
				//
				if (transition != null) {
					current = this.transition(stateful, current, event, transition, args);
				} else {

                    if (logger.isDebugEnabled())
                        logger.debug("{}({})::{}({})->{}/noop",
                                this.name,
                                stateful.getClass().getSimpleName(),
                                current.getName(),
                                event,
                                current.getName());

					// If blocking, force a transition to the current state as
					// it's possible that another thread has moved out of the blocking state.
					// Either way, we'll retry this event
					//
					if (current.isBlocking()) {
						this.setCurrent(stateful, current, current);
						throw new WaitAndRetryException(this.retryInterval);
					}
				}

				return current;

			} catch(RetryException re) {

				logger.warn("{}({})::Retrying event", this.name, stateful);

				// Wait?
				//
				if (WaitAndRetryException.class.isInstance(re)) {
					try {
						Thread.sleep(((WaitAndRetryException)re).getWait());
					} catch(InterruptedException ie) {
						throw new RuntimeException(ie);
					}
				}
				attempts++;
			}
		}
		logger.error("{}({})::Unable to process event", this.name, stateful);
		throw new TooBusyException();
	}

	public int getRetryAttempts() {
		return retryAttempts;
	}

	public void setRetryAttempts(int retries) {
		this.retryAttempts = retries;
	}

	public int getRetryInterval() {
		return retryInterval;
	}

	public void setRetryInterval(int retryInterval) {
		this.retryInterval = retryInterval;
	}

	public Persister getPersister() {
		return persister;
	}

	public void setPersister(Persister persister) {
		this.persister = persister;
	}

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public State getCurrentState(T obj) {
		return this.persister.getCurrent(obj);
	}


	/**
	 * Fluid FSM builder
	 *
	 * @author Andrew Hall
	 *
	 * @param  Type of the Stateful Entity
	 */
	public static class FSMBuilder {

		/**
		 * Fluid State Builder
		 * 
		 * @author Andrew Hall
		 *
		 * @param  Type of the Stateful Entity
		 */
		public static class StateBuilder {

			/**
			 * Fluid Transition Builder
			 * 
			 * @author Andrew Hall
			 *
			 * @param  Type of the Stateful Entity
			 */
			public static class TransitionBuilder {

				private StateBuilder stateBuilder;
				private String event;
				private String toStateName;
				private State toState;
				private Action action;

				private TransitionBuilder(String event, StateBuilder stateBuilder) {
					this.event = event;
					this.stateBuilder = stateBuilder;
					this.toStateName = null;
					this.action = null;
				}

				private TransitionBuilder(String event, String toStateKey) {
					this(event, toStateKey, null);
				}

				private TransitionBuilder(String event, String toStateKey, Action action) {
					if (event == null || event.trim().equals("")) {
						throw new RuntimeException("You must provide an Event");
					}
					this.event = event;
					this.toStateName = toStateKey;
					this.toState = null;
					this.action = action;
				}

				private TransitionBuilder(String event, State toState, Action action) {
					if (event == null || event.trim().equals("")) {
						throw new RuntimeException("You must provide an Event");
					}
					this.event = event;
					this.toState = toState;
					this.action = action;
				}

				/**
				 * Set the "To" State using the specified State Name
				 * @param toStateName Name of the State
				 * @return TransitionBuilder
				 */
				public TransitionBuilder setToState(String toStateName) {
					this.toStateName = toStateName;
					return this;
				}

				/**
				 * Set the "To" State to the specified State.  If both the "To" State is
				 * set using the name of the State and the State object, the State object 
				 * will take precedence
				 * @param toState To State object
				 * @return TransitionBuilder
				 */
				public TransitionBuilder setToState(State toState) {
					this.toState = toState;
					return this;
				}

				/**
				 * Set the Action for this Transition
				 *
				 * @param action
				 * @return TransitionBuilder
				 */
				public TransitionBuilder setAction(Action action) {
					this.action = action;
					return this;
				}

				/**
				 * Completes the setup for this Transition Builder
				 *
				 * @return StateBuilder
				 */
				public StateBuilder done() {
					return this.stateBuilder;
				}

				private Transition build(State from, Map> states) {
					State to = (this.toState != null)
							? this.toState
							: (this.toStateName != null)
								? states.get(this.toStateName)
								: from;
					return new DeterministicTransitionImpl(from, to, this.event, this.action);
				}

			}

			private String stateName;
			private Map> transistions = new HashMap>();
			private List> transistionBuilders = new LinkedList>();
			private boolean isEndState = false;
			private boolean isBlocking = false;
			private State state;

			private FSMBuilder fsmBuilder;

			private StateBuilder(FSMBuilder fsmBuilder, String stateName) {
				if (stateName == null || stateName.trim().equals("")) {
					throw new RuntimeException("You must provide a State name");
				}
				this.fsmBuilder = fsmBuilder;
				this.stateName = stateName;
			}

			/**
			 * Set whether this State is the End State
			 * @param isEndState
			 * @return StateBuilder
			 */
			public StateBuilder setEndState(boolean isEndState) {
				this.isEndState = isEndState;
				return this;
			}

			/**
			 * Set whether this State is a blocking State
			 * @param isBlocking
			 * @return StateBuilder
			 */
			public StateBuilder setBlockingState(boolean isBlocking) {
				this.isBlocking = isBlocking;
				return this;
			}

			/**
			 * Add a Transition with no Action.
			 * @param event Event that triggers the Transition
			 * @param toState The name of the State in which to transition to
			 * @return StateBuilder
			 */
			public StateBuilder addTransition(String event, String toState) {
				this.transistionBuilders.add(new TransitionBuilder(event, toState));
				return this;
			}

			/**
			 * Add a Transition with no Action.
			 * @param event Event that triggers the Transition
			 * @param toState The State in which to transition to
			 * @return StateBuilder
			 */
			public StateBuilder addTransition(String event, State toState) {
				return this.addTransition(event, toState, null);
			}

			/**
			 * Add a Transition with an Action
			 * @param event Event that triggers the Transition
			 * @param toState The name of the State in which to transition to
			 * @param action The Action to invoke
			 * @return StateBuilder
			 */
			public StateBuilder addTransition(String event, String toState, Action action) {
				this.transistionBuilders.add(new TransitionBuilder(event, toState, action));
				return this;
			}

			/**
			 * Add a Transition with an Action
			 * @param event Event that triggers the Transition
			 * @param toState The State in which to transition to
			 * @param action The Action to invoke
			 * @return StateBuilder
			 */
			public StateBuilder addTransition(String event, State toState, Action action) {
				this.fsmBuilder.addState(toState);
				this.transistionBuilders.add(new TransitionBuilder(event, toState, action));
				return this;
			}

			/**
			 * Add a Transition with no State change but with an Action
			 * @param event Event that triggers the Transition
			 * @param action The Action to invoke
			 * @return StateBuilder
			 */
			public StateBuilder addTransition(String event, Action action) {
				return this.addTransition(event, this.stateName, action);
			}

			/**
			 * Add an instantiated Transition
			 * @param event Event that triggers the Transition
			 * @return StateBuilder
			 */
			public StateBuilder addTransition(String event, Transition transition) {
				this.transistions.put(event, transition);
				return this;
			}

			/**
			 * Begin building a Transition
			 * @param event Event that triggers the Transition
			 * @return StateBuilder
			 */
			public TransitionBuilder buildTransition(String event) {
				TransitionBuilder transitionBuilder = new TransitionBuilder(event, this);
				this.transistionBuilders.add(transitionBuilder);
				return transitionBuilder;
			}

			/**
			 * Completes the setup for this State Builder
			 * @return FSMBuilder
			 */
			public FSMBuilder done() {
				return this.fsmBuilder;
			}

			private State buildState() {
				this.state = new StateImpl(this.stateName, this.transistions, this.isEndState, this.isBlocking);
				return this.state;
			}

			private void buildTransitions(Map> states) {
				for(TransitionBuilder transitionBuilder : this.transistionBuilders) {
					transitionBuilder.build(this.state, states);
				}
			}

		}

		private int retryAttempts = DEFAULT_RETRIES;
		private int retryInterval = DEFAULT_RETRY_INTERVAL;

		private Persister persister;
		private String name = "FSM";

		private HashMap> states = new HashMap>();
		private List> stateBuilders = new LinkedList>();
		private String startState;

		/**
		 * Build a new FSM Builder
		 * @param clazz Parameterized Class for the FSM
		 * @return FSMBuilder
		 */
		public static  FSMBuilder newBuilder(Class clazz) {
			return new FSMBuilder();
		}

		/**
		 * Set the Persister for the FSM.  If not specified, a MemoryPersister will be
		 * instantiated with persists the state only on the Stateful object
		 * @param persister The Persister
		 * @return FSMBuilder
		 */
		public FSMBuilder setPerister(Persister persister) {
			this.persister = persister;
			return this;
		}

		/**
		 * Sets the name of the FSM
		 * @param name Name of the FSM
		 * @return FSMBuilder
		 */
		public FSMBuilder setName(String name) {
			this.name = name;
			return this;
		}

		/**
		 * Sets the number of retry attempts for the FSM
		 * @param retryAttempts
		 * @return FSMBuilder
		 */
		public FSMBuilder setRetryAttempts(int retryAttempts) {
			this.retryAttempts = retryAttempts;
			return this;
		}

		/**
		 * Sets the retry interval for the FSM
		 * @param retryInterval
		 * @return FSMBuilder
		 */
		public FSMBuilder setRetryInterval(int retryInterval) {
			this.retryInterval = retryInterval;
			return this;
		}

		/**
		 * Add a specified State
		 * @param state State to add
		 * @return FSMBuilder
		 */
		public FSMBuilder addState(State state) {
			return this.addState(state, false);
		}

		/**
		 * Add a specified State and state whether or not it's the Start State for the FSM.
		 * If no State is explicitly set as the Start State, then the first State added will
		 * be the Start State
		 * 
		 * @param state State to add
		 * @param isStartState Whether or not the State is specified as the Start State
		 * @return FSMBuilder
		 */
		public FSMBuilder addState(State state, boolean isStartState) {
			this.startState = (this.startState == null || isStartState) ? state.getName() : this.startState;
			this.states.put(state.getName(), state);
			return this;
		}

		/**
		 * Begin building a State for the FSM
		 *
		 * @param name Name of the State
		 * @return StateBuilder
		 */
		public StateBuilder buildState(String name) {
			return this.buildState(name, false);
		}

		/**
		 * Begin building a State for the FSM and state whether or not it's the Start State for the FSM.
		 * If no State is explicitly set as the Start State, then the first State added will
		 * be the Start State
		 * 
		 * @param name Name of the State
		 * @param isStartState Whether or not the State is specified as the Start State
		 * @return StateBuilder
		 */
		public StateBuilder buildState(String name, boolean isStartState) {
			this.startState = (this.startState == null || isStartState) ? name : this.startState;
			StateBuilder stateBuilder = new StateBuilder(this, name);
			this.stateBuilders.add(stateBuilder);
			return stateBuilder;
		}

		/**
		 * Build the FSM and all of it's States and Transitions
		 * @return FSM
		 */
		public FSM build() {

			for(StateBuilder stateBuilder : this.stateBuilders) {
				State state = stateBuilder.buildState();
				this.states.put(state.getName(), state);
			}

			for(StateBuilder stateBuilder : this.stateBuilders) {
				stateBuilder.buildTransitions(this.states);
			}

			State startState = this.states.get(this.startState);
			if (startState == null) {
				throw new RuntimeException("No start state defined, state=" + this.startState);
			}

			if (this.persister == null) {
				this.persister = new MemoryPersisterImpl();
			}
			this.persister.setStates(this.states.values());
			this.persister.setStartState(startState);

			return new FSM(this.name, this.persister, this.retryAttempts, this.retryInterval);
		}
	}


	protected Transition getTransition(String event, State current) {
		return current.getTransition(event);
	}

	protected State transition(T stateful, State current, String event, Transition transition, Object... args) throws RetryException {
		StateActionPair pair = transition.getStateActionPair(stateful, event, args);
		setCurrent(stateful, current, pair.getState());
		executeAction(
				pair.getAction(),
				stateful,
				event,
				current.getName(),
				pair.getState().getName(),
				args);
		return pair.getState();
	}

	protected void setCurrent(T stateful, State current, State next) throws StaleStateException {
		persister.setCurrent(stateful, current, next);
	}

	protected void executeAction(
			Action action,
			T stateful,
			String event,
			String from,
			String to,
			Object... args) throws RetryException {

        if (logger.isDebugEnabled())
            logger.debug("{}({})::{}({})->{}/{}",
                    this.name,
                    stateful.getClass().getSimpleName(),
                    from,
                    event,
                    to,
                    (action == null) ? "noop" : action.toString());

		if (action != null) {
			action.execute(stateful, event, args);
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy