View Javadoc

1   /*
2    * Copyright (C) 2006  Tom Gibara
3    *
4    * This library is free software; you can redistribute it and/or
5    * modify it under the terms of the GNU Lesser General Public
6    * License as published by the Free Software Foundation; either
7    * version 2.1 of the License, or (at your option) any later version.
8    *
9    * This library is distributed in the hope that it will be useful,
10   * but WITHOUT ANY WARRANTY; without even the implied warranty of
11   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12   * Lesser General Public License for more details.
13   *
14   * You should have received a copy of the GNU Lesser General Public
15   * License along with this library; if not, write to the Free Software
16   * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
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      // statics
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      // fields
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      // constructors
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      // engine methods
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             // first filter the graph according to any supplied label
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     // private utility methods
182 
183     // must be called with the lock
184     private void doStateChange(final S newState) {
185         // weed our redundant calls
186         if (newState.equals(this.state)) return;
187         // ensure that the state graph contains the state
188         if (!getPossibleStates().contains(newState)) throw new ProntoStateException(String.format(
189                 "The state %s is not a possible state.", newState));
190         // now try to make the state change
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         // update our state
202         state = newState;
203         // clear the transition field, it doesn't apply now
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     // must be called with the lock
220     private void doTransition(final StateTransition<S, L> transition, final P parameter) {
221         // first a relatively cheap sanity check
222         if (!transition.getSource().equals(state)) throw new IllegalStateException();
223         // record the transition we are about to attempt
224         this.transition = transition;
225         // now try to make the transition
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         // success, update our state
237         this.state = transition.getTarget();
238         // and remember to clear the transition
239         this.transition = null;
240         // broadcast our succes to the world
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 }