1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package com.tomgibara.pronto.state.impl;
20
21 import static java.util.logging.Level.FINER;
22 import static java.util.logging.Level.FINEST;
23
24 import java.util.Collections;
25 import java.util.HashSet;
26 import java.util.List;
27 import java.util.Set;
28 import java.util.logging.Level;
29 import java.util.logging.Logger;
30
31 import com.tomgibara.pronto.state.DefaultStateEnginePolicy;
32 import com.tomgibara.pronto.state.PathType;
33 import com.tomgibara.pronto.state.ProntoStateException;
34 import com.tomgibara.pronto.state.StateActivator;
35 import com.tomgibara.pronto.state.StateEngine;
36 import com.tomgibara.pronto.state.StateEnginePolicy;
37 import com.tomgibara.pronto.state.StateGraph;
38 import com.tomgibara.pronto.state.StateGraphEditor;
39 import com.tomgibara.pronto.state.StateListener;
40 import com.tomgibara.pronto.state.StateTransition;
41 import com.tomgibara.pronto.state.StateTransitionParameters;
42 import com.tomgibara.pronto.util.Arguments;
43
44 public class StateEngineImpl<S, L, P> implements StateEngine<S, L, P> {
45
46
47
48 /**
49 * The log for exceptions and other log worthy events.
50 */
51
52 private static final Logger LOGGER = Logger.getLogger(StateEngineImpl.class.getPackage().getName());
53
54 /**
55 * The policy to be used if none (or null) is specified.
56 */
57
58 private static final DefaultStateEnginePolicy DEFAULT_POLICY = new DefaultStateEnginePolicy();
59
60
61
62 private final Object lock = new Object();
63 private final StateGraph<S, L> graph;
64 private final StateActivator<S, L, P> activator;
65 private final HashSet<StateListener<S, L>> listeners = new HashSet<StateListener<S, L>>();
66 private StateEnginePolicy policy = DEFAULT_POLICY;
67 private S state = null;
68 private StateTransition<S, L> transition = null;
69
70
71
72 public StateEngineImpl(final StateGraph<S, L> graph, final StateActivator<S, L, P> activator) {
73 Arguments.notNull(graph, "graph");
74 Arguments.notNull(activator, "activator");
75
76 this.graph = graph;
77 this.activator = activator;
78
79 LOGGER.log(Level.FINE, "Created new state engine over {0}", graph);
80 }
81
82
83
84 public void setPolicy(StateEnginePolicy policy) {
85 if (policy == null) policy = DEFAULT_POLICY;
86 synchronized (lock) {
87 if (this.policy != policy) {
88 this.policy = policy;
89 }
90 }
91 }
92
93 public StateEnginePolicy getPolicy() {
94 synchronized (lock) {
95 return policy;
96 }
97 }
98
99 public StateGraph<S, L> getGraph() {
100 return graph;
101 }
102
103 public Set<S> getPossibleStates() {
104 synchronized (lock) {
105 if (state == null) return graph.states();
106 if (transition != null) {
107 HashSet<S> set = new HashSet<S>();
108 set.add(transition.getSource());
109 set.add(transition.getTarget());
110 return Collections.unmodifiableSet(set);
111 }
112 return Collections.singleton(state);
113 }
114 }
115
116 public S getState() {
117 synchronized (lock) {
118 return state;
119 }
120 }
121
122 public void setState(final S state) throws ProntoStateException {
123 Arguments.notNull(state, "state");
124 synchronized (lock) {
125 doStateChange(state);
126 }
127 }
128
129 public void transition(final S state, final L label, final P parameter) throws ProntoStateException {
130 synchronized (lock) {
131 if (this.state == null) throw new ProntoStateException(
132 "Cannot transition, engine is in an indeterminate state.");
133
134 Set<StateTransition<S, L>> transitions = graph.getTransitionsMatching(this.state, label, state);
135 int size = transitions.size();
136 if (size == 0) throw new ProntoStateException(String.format(
137 "No transition from state %s labelled %s to state %s.", this.state, label, state));
138 else if (size > 1) throw new ProntoStateException(String.format(
139 "More than one transition from state %s labelled %s to state %s.", this.state, label, state));
140 else {
141 doTransition(transitions.iterator().next(), parameter);
142 }
143 }
144 }
145
146 public void pathTransition(final S state, final L label, final PathType type,
147 final StateTransitionParameters<S, L, P> parameters) throws ProntoStateException, IllegalArgumentException {
148 synchronized (lock) {
149 if (this.state == null) throw new ProntoStateException(
150 "Cannot transition, engine is in an indeterminate state.");
151
152
153 StateGraph<S, L> graph = this.graph;
154 if (label != null) {
155 StateGraphEditor<S, L> editor = graph.newEditor();
156 editor.retainTransitionsLabelled(Collections.singleton(label));
157 graph = editor.getGraph();
158 }
159
160 List<StateTransition<S, L>> path = graph.getPath(this.state, state, type);
161 if (path == null) throw new ProntoStateException(String.format("No path from %s to %s.", this.state, state));
162 for (StateTransition<S, L> transition : path) {
163 P parameter = parameters == null ? null : parameters.getParameter(transition);
164 doTransition(transition, parameter);
165 }
166 }
167 }
168
169 public boolean addStateListener(final StateListener<S, L> listener) {
170 synchronized (lock) {
171 return listeners.add(listener);
172 }
173 }
174
175 public boolean removeStateListener(final StateListener<S, L> listener) {
176 synchronized (lock) {
177 return listeners.remove(listener);
178 }
179 }
180
181
182
183
184 private void doStateChange(final S newState) {
185
186 if (newState.equals(this.state)) return;
187
188 if (!getPossibleStates().contains(newState)) throw new ProntoStateException(String.format(
189 "The state %s is not a possible state.", newState));
190
191 try {
192 activator.changeState(newState);
193 LOGGER.log(FINER, "Successful state change to {0}", newState);
194 } catch (ProntoStateException e) {
195 LOGGER.log(FINER, String.format("Vetoed state change to %s", newState), e);
196 throw e;
197 } catch (RuntimeException e) {
198 LOGGER.log(Level.WARNING, String.format("Failed in change to state %s", newState), e);
199 throw new ProntoStateException(e);
200 }
201
202 state = newState;
203
204 transition = null;
205 for (StateListener<S, L> listener : listeners) {
206 try {
207 LOGGER.log(FINEST, "Broadcasting change to state {0}", state);
208 listener.stateChanged(state);
209 } catch (RuntimeException e) {
210 if (policy.isListenerExceptionLogged()) {
211 LOGGER.log(Level.WARNING, String.format(
212 "Exception in listener %s in response to change to state %s", listener, newState));
213 }
214 if (policy.isListenerExceptionThrown()) throw e;
215 }
216 }
217 }
218
219
220 private void doTransition(final StateTransition<S, L> transition, final P parameter) {
221
222 if (!transition.getSource().equals(state)) throw new IllegalStateException();
223
224 this.transition = transition;
225
226 try {
227 activator.transitionState(transition, parameter);
228 LOGGER.log(FINER, "Successful transition via {0}", transition);
229 } catch (ProntoStateException e) {
230 LOGGER.log(FINER, String.format("Vetoed transition on %s", transition), e);
231 throw e;
232 } catch (RuntimeException e) {
233 LOGGER.log(Level.WARNING, String.format("Failed transition on %s", transition), e);
234 throw new ProntoStateException(e);
235 }
236
237 this.state = transition.getTarget();
238
239 this.transition = null;
240
241 for (StateListener<S, L> listener : listeners) {
242 try {
243 LOGGER.log(FINEST, "Broadcasting transition via {0} to listener", transition);
244 listener.stateTransitioned(transition);
245 } catch (RuntimeException e) {
246 if (policy.isListenerExceptionLogged()) {
247 LOGGER.log(Level.WARNING, String.format(
248 "Exception in listener %s in response to transition via %s", listener, transition));
249 }
250 if (policy.isListenerExceptionThrown()) throw e;
251 }
252 }
253 }
254
255 }