/*
* Copyright (C) 2014 GG-Net GmbH - Oliver Günther.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; If not, see <http://www.gnu.org/licenses/>.
*/
package eu.ggnet.statemachine;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A StateMachine. The Usage is best shown on an example, a Box: <ul> <li>Two States: open, close</li> <li>Two Transitions: closing and opening</li> <li>The
* Graph: open → closing → closed; closed → opening → open</li> </ul> The Box: (For simplicity, a Box is already a
* {@link StateCharacteristic}
* <pre><code> public class Box implements StateCharacteristic<Box> {
*
* private boolean closed;
*
* public boolean isClosed() { return closed; }
*
* public void setClosed(boolean closed) { this.closed = closed; }
*
* public int hashCode() { return 89 * 3 + (closed ? 1 : 0); }
*
* public boolean equals(Object obj) {
* if (obj == null) { return false; }
* if (getClass() != obj.getClass()) { return false; }
* final BoxStateCharacteristics other = (BoxStateCharacteristics) obj;
* if (this.closed != other.closed) { return false; }
* return true;
* }
*
* }</code></pre> The {@link StateCharacteristicFactory}:
* <pre><code> public class BoxStateCharacteristicFactory implements StateCharacteristicFactory<Box> {
*
* public StateCharacteristic<Box> characterize(Box t) { return t; }
*
* }</code></pre> Create the States:
* <pre><code> public class BoxStates {
*
* public final static State OPEN = new State("OPEN", new Box(false));
* public final static State CLOSED = new State("CLOSED", new Box(true));
*
* }</code></pre> Create the Transitions:
* <pre><code> public class BoxTransitions {
* public final static StateTransition<Box> OPENING = new StateTransition<Box>("OPENING") {
* public void apply(Box instance) { instance.setClosed(false); }
* };
*
* public final static StateTransition<Box> CLOSING = new StateTransition<Box>("OPENING") {
* public void apply(Box instance) { instance.setClosed(true); }
* };
*
* }</code></pre> Create the Machine and use it:
* <pre><code> public class Main {
*
* public static void main(String ... args) {
* // Init the Machine
* StateMachine<Box> m = new StateMachine<>(new BoxStateCharacteristicFactory());
* m.add(BoxStates.OPEN, BoxTransitions.CLOSING, BoxStates.CLOSED);
* m.add(BoxStates.CLOSED, BoxTransitions.OPENING, BoxStates.OPEN);
* // Create a Box (default closed=false)
* Box b = new Box();
* // Use the Machine to open or close it.
* m.stateChange(b, BoxStates.CLOSING);
* System.out.println("Box should be closed: closed=" + b.isClosed());
* m.stateChange(b, BoxStates.OPENING);
* System.out.println("Box should be open: closed=" + b.isClosed());
* m.show(): // Display the Graph
* }
*
* }</code></pre>
* Notes from the author: This implementation of a StateMachine is far from being an all round solution.
* But it serves it's cause, by being used in RedTape.
*
* @author oliver.guenther
*/
public class StateMachine<T> {
private final static Logger L = LoggerFactory.getLogger(StateMachine.class);
private boolean debug;
private StateCharacteristicFactory<T> factory;
private StateFormater<T> formater;
private Set<Link<T>> links = new HashSet<>();
private Set<State<T>> states = new HashSet<>();
private Map<StateCharacteristic<T>, State> allCharacteristics = new HashMap<>();
public StateMachine(StateCharacteristicFactory<T> factory) {
this.factory = factory;
}
/**
* Creates a new empty state machine using the same factory.
* <p/>
* @param stateMachine the initial state machine.
*/
public StateMachine(StateMachine<T> stateMachine) {
this.factory = stateMachine.factory;
}
public boolean isDebug() {
return debug;
}
public StateFormater<T> getFormater() {
return formater;
}
public void setFormater(StateFormater<T> formater) {
this.formater = formater;
}
/**
* Enables some System.out Debug. TODO: Change to some Logger
*
* @param debug if true, enables debug.
*/
public void setDebug(boolean debug) {
this.debug = debug;
}
/**
* Changes the State of an instance.
* Validates if, the instance is in a definite state and if the transition can be applied.
*
* @param instance the instance to change the state
* @param transition the transition to be used.
* @throws IllegalArgumentException if the instance is not in a definite state or the transition cannot be applied.
*
*/
public void stateChange(T instance, StateTransition<T> transition) throws IllegalArgumentException {
L.debug("stateChange for {} with {}", instance, transition);
if ( debug ) {
System.out.println("Transfer: " + instance);
System.out.println(" - " + getState(instance));
System.out.println(" - " + transition);
}
if ( getState(instance) == null ) throw new IllegalArgumentException(instance + " has no State defined");
if ( !getPossibleTransitions(instance).contains(transition) ) throw new IllegalArgumentException(transition + " is not possible on State" + instance);
transition.apply(instance);
if ( debug ) {
System.out.println(" - " + getState(instance));
}
}
/**
* Adds a new Link consisting of two states and a transition to the machine.
*
* @param source the source state.
* @param transition the transition between the two states.
* @param destination the destination state.
*/
public void add(State<T> source, StateTransition<T> transition, State<T> destination) {
add(new Link<>(source, transition, destination));
}
/**
* Adds a new Link consisting of two states and a transition to the machine.
* <p/>
* @param link the link to add
*/
public void add(Link<T> link) {
if ( links.contains(link) ) throw new IllegalArgumentException("Adding an existing Link, not allowed: " + link);
validAdd(link.getSource());
validAdd(link.getDestination());
links.add(link);
}
private void validAdd(State<T> state) {
if ( states.contains(state) ) return;
for (StateCharacteristic<T> c : state.getCharacteristics()) {
if ( allCharacteristics.keySet().contains(c) ) {
throw new IllegalArgumentException("The added state has an overlapping characteristic. Added " + state.getName()
+ ", existing " + allCharacteristics.get(c).getName() + ", overlapping " + c);
}
}
states.add(state);
for (StateCharacteristic<T> c : state.getCharacteristics()) {
allCharacteristics.put(c, state);
}
}
/**
* Return all States of the machine.
*
* @return all States.
*/
public Set<State<T>> getAllStates() {
return new HashSet<>(states);
}
/**
* Returns the State, which is identifies the instance or null if no State matches.
*
* @param instance the instance
* @return the State, which is identifies the instance or null if no State matches.
*/
public State<T> getState(T instance) {
StateCharacteristic<T> characteristic = factory.characterize(instance);
if ( characteristic == null ) return null;
for (State<T> state : getAllStates()) {
if ( state.getCharacteristics().contains(characteristic) ) {
return state;
}
}
return null;
}
/**
* Return all Links.
*
* @return all Links.
*/
public Set<Link<T>> getLinks() {
return new HashSet<>(links);
}
/**
* Returns a possible empty List of transactions, which can be applied to the instance.
*
* @param instance the instance.
* @return a possible empty List of transactions, which can be applied to the instance.
*/
public List<StateTransition<T>> getPossibleTransitions(T instance) {
StateCharacteristic<T> characteristic = factory.characterize(instance);
List<StateTransition<T>> sts = new ArrayList<>();
for (Link<T> link : links) {
if ( link.getSource().getCharacteristics().contains(characteristic) ) {
sts.add(link.getTransition());
}
}
return sts;
}
}