View Javadoc
1   /*
2    *  Licensed to the Apache Software Foundation (ASF) under one
3    *  or more contributor license agreements.  See the NOTICE file
4    *  distributed with this work for additional information
5    *  regarding copyright ownership.  The ASF licenses this file
6    *  to you under the Apache License, Version 2.0 (the
7    *  "License"); you may not use this file except in compliance
8    *  with the License.  You may obtain a copy of the License at
9    *
10   *    http://www.apache.org/licenses/LICENSE-2.0
11   *
12   *  Unless required by applicable law or agreed to in writing,
13   *  software distributed under the License is distributed on an
14   *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   *  KIND, either express or implied.  See the License for the
16   *  specific language governing permissions and limitations
17   *  under the License.
18   *
19   */
20  package org.apache.mina.statemachine;
21  
22  import java.util.Collection;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.LinkedList;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Stack;
29  
30  import org.apache.mina.statemachine.context.StateContext;
31  import org.apache.mina.statemachine.event.Event;
32  import org.apache.mina.statemachine.event.UnhandledEventException;
33  import org.apache.mina.statemachine.transition.SelfTransition;
34  import org.apache.mina.statemachine.transition.Transition;
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  
38  /**
39   * Represents a complete state machine. Contains a collection of {@link State}
40   * objects connected by {@link Transition}s. Normally you wouldn't create 
41   * instances of this class directly but rather use the 
42   * {@link org.apache.mina.statemachine.annotation.State} annotation to define
43   * your states and then let {@link StateMachineFactory} create a 
44   * {@link StateMachine} for you.
45   *
46   * @author <a href="http://mina.apache.org">Apache MINA Project</a>
47   */
48  public class StateMachine {
49      private static final Logger LOGGER = LoggerFactory.getLogger(StateMachine.class);
50  
51      private static final String CALL_STACK = StateMachine.class.getName() + ".callStack";
52  
53      private final State startState;
54  
55      private final Map<String, State> states;
56  
57      private final ThreadLocal<Boolean> processingThreadLocal = new ThreadLocal<Boolean>() {
58          protected Boolean initialValue() {
59              return Boolean.FALSE;
60          }
61      };
62  
63      private final ThreadLocal<LinkedList<Event>> eventQueueThreadLocal = new ThreadLocal<LinkedList<Event>>() {
64          protected LinkedList<Event> initialValue() {
65              return new LinkedList<Event>();
66          }
67      };
68  
69      /**
70       * Creates a new instance using the specified {@link State}s and start
71       * state.
72       * 
73       * @param states the {@link State}s.
74       * @param startStateId the id of the start {@link State}.
75       */
76      public StateMachine(State[] states, String startStateId) {
77          this.states = new HashMap<String, State>();
78          for (State s : states) {
79              this.states.put(s.getId(), s);
80          }
81          this.startState = getState(startStateId);
82      }
83  
84      /**
85       * Creates a new instance using the specified {@link State}s and start
86       * state.
87       * 
88       * @param states the {@link State}s.
89       * @param startStateId the id of the start {@link State}.
90       */
91      public StateMachine(Collection<State> states, String startStateId) {
92          this(states.toArray(new State[0]), startStateId);
93      }
94  
95      /**
96       * Returns the {@link State} with the specified id.
97       * 
98       * @param id the id of the {@link State} to return.
99       * @return the {@link State}
100      * @throws NoSuchStateException if no matching {@link State} could be found.
101      */
102     public State getState(String id) throws NoSuchStateException {
103         State state = states.get(id);
104         if (state == null) {
105             throw new NoSuchStateException(id);
106         }
107         return state;
108     }
109 
110     /**
111      * @return an unmodifiable {@link Collection} of all {@link State}s used by
112      * this {@link StateMachine}.
113      */
114     public Collection<State> getStates() {
115         return Collections.unmodifiableCollection(states.values());
116     }
117 
118     /**
119      * Processes the specified {@link Event} through this {@link StateMachine}.
120      * Normally you wouldn't call this directly but rather use
121      * {@link StateMachineProxyBuilder} to create a proxy for an interface of
122      * your choice. Any method calls on the proxy will be translated into
123      * {@link Event} objects and then fed to the {@link StateMachine} by the
124      * proxy using this method.
125      * 
126      * @param event the {@link Event} to be handled.
127      */
128     public void handle(Event event) {
129         StateContext context = event.getContext();
130 
131         synchronized (context) {
132             LinkedList<Event> eventQueue = eventQueueThreadLocal.get();
133             eventQueue.addLast(event);
134 
135             if (processingThreadLocal.get()) {
136                 /*
137                  * This thread is already processing an event. Queue this 
138                  * event.
139                  */
140                 if (LOGGER.isDebugEnabled()) {
141                     LOGGER.debug("State machine called recursively. Queuing event " + event + " for later processing.");
142                 }
143             } else {
144                 processingThreadLocal.set(true);
145                 try {
146                     if (context.getCurrentState() == null) {
147                         context.setCurrentState(startState);
148                     }
149                     processEvents(eventQueue);
150                 } finally {
151                     processingThreadLocal.set(false);
152                 }
153             }
154         }
155 
156     }
157 
158     private void processEvents(LinkedList<Event> eventQueue) {
159         while (!eventQueue.isEmpty()) {
160             Event event = eventQueue.removeFirst();
161             StateContext context = event.getContext();
162             handle(context.getCurrentState(), event);
163         }
164     }
165 
166     private void handle(State state, Event event) {
167         StateContext context = event.getContext();
168 
169         for (Transition t : state.getTransitions()) {
170             if (LOGGER.isDebugEnabled()) {
171                 LOGGER.debug("Trying transition " + t);
172             }
173 
174             try {
175                 if (t.execute(event)) {
176                     if (LOGGER.isDebugEnabled()) {
177                         LOGGER.debug("Transition " + t + " executed successfully.");
178                     }
179                     setCurrentState(context, t.getNextState());
180 
181                     return;
182                 }
183             } catch (BreakAndContinueException bace) {
184                 if (LOGGER.isDebugEnabled()) {
185                     LOGGER.debug("BreakAndContinueException thrown in " + "transition " + t
186                             + ". Continuing with next transition.");
187                 }
188             } catch (BreakAndGotoException bage) {
189                 State newState = getState(bage.getStateId());
190 
191                 if (bage.isNow()) {
192                     if (LOGGER.isDebugEnabled()) {
193                         LOGGER.debug("BreakAndGotoException thrown in " + "transition " + t + ". Moving to state "
194                                 + newState.getId() + " now.");
195                     }
196                     setCurrentState(context, newState);
197                     handle(newState, event);
198                 } else {
199                     if (LOGGER.isDebugEnabled()) {
200                         LOGGER.debug("BreakAndGotoException thrown in " + "transition " + t + ". Moving to state "
201                                 + newState.getId() + " next.");
202                     }
203                     setCurrentState(context, newState);
204                 }
205                 return;
206             } catch (BreakAndCallException bace) {
207                 State newState = getState(bace.getStateId());
208 
209                 Stack<State> callStack = getCallStack(context);
210                 State returnTo = bace.getReturnToStateId() != null ? getState(bace.getReturnToStateId()) : context
211                         .getCurrentState();
212                 callStack.push(returnTo);
213 
214                 if (bace.isNow()) {
215                     if (LOGGER.isDebugEnabled()) {
216                         LOGGER.debug("BreakAndCallException thrown in " + "transition " + t + ". Moving to state "
217                                 + newState.getId() + " now.");
218                     }
219                     setCurrentState(context, newState);
220                     handle(newState, event);
221                 } else {
222                     if (LOGGER.isDebugEnabled()) {
223                         LOGGER.debug("BreakAndCallException thrown in " + "transition " + t + ". Moving to state "
224                                 + newState.getId() + " next.");
225                     }
226                     setCurrentState(context, newState);
227                 }
228                 return;
229             } catch (BreakAndReturnException bare) {
230                 Stack<State> callStack = getCallStack(context);
231                 State newState = callStack.pop();
232 
233                 if (bare.isNow()) {
234                     if (LOGGER.isDebugEnabled()) {
235                         LOGGER.debug("BreakAndReturnException thrown in " + "transition " + t + ". Moving to state "
236                                 + newState.getId() + " now.");
237                     }
238                     setCurrentState(context, newState);
239                     handle(newState, event);
240                 } else {
241                     if (LOGGER.isDebugEnabled()) {
242                         LOGGER.debug("BreakAndReturnException thrown in " + "transition " + t + ". Moving to state "
243                                 + newState.getId() + " next.");
244                     }
245                     setCurrentState(context, newState);
246                 }
247                 return;
248             }
249         }
250 
251         /*
252          * No transition could handle the event. Try with the parent state if
253          * there is one.
254          */
255 
256         if (state.getParent() != null) {
257             handle(state.getParent(), event);
258         } else {
259             throw new UnhandledEventException(event);
260         }
261     }
262 
263     private Stack<State> getCallStack(StateContext context) {
264         @SuppressWarnings("unchecked")
265         Stack<State> callStack = (Stack<State>) context.getAttribute(CALL_STACK);
266         if (callStack == null) {
267             callStack = new Stack<State>();
268             context.setAttribute(CALL_STACK, callStack);
269         }
270         return callStack;
271     }
272 
273     private void setCurrentState(StateContext context, State newState) {
274         if (newState != null) {
275             if (LOGGER.isDebugEnabled()) {
276                 if (newState != context.getCurrentState()) {
277                     LOGGER.debug("Leaving state " + context.getCurrentState().getId());
278                     LOGGER.debug("Entering state " + newState.getId());
279                 }
280             }
281             executeOnExits(context, context.getCurrentState());
282             executeOnEntries(context, newState);
283             context.setCurrentState(newState);
284         }
285     }
286 
287     void executeOnExits(StateContext context, State state) {
288         List<SelfTransition> onExits = state.getOnExitSelfTransitions();
289         boolean isExecuted = false;
290 
291         if (onExits != null)
292             for (SelfTransition selfTransition : onExits) {
293                 selfTransition.execute(context, state);
294                 if (LOGGER.isDebugEnabled()) {
295                     isExecuted = true;
296                     LOGGER.debug("Executing onEntry action for " + state.getId());
297                 }
298             }
299         if (LOGGER.isDebugEnabled() && !isExecuted) {
300             LOGGER.debug("No onEntry action for " + state.getId());
301 
302         }
303     }
304 
305     void executeOnEntries(StateContext context, State state) {
306         List<SelfTransition> onEntries = state.getOnEntrySelfTransitions();
307         boolean isExecuted = false;
308 
309         if (onEntries != null)
310             for (SelfTransition selfTransition : onEntries) {
311                 selfTransition.execute(context, state);
312                 if (LOGGER.isDebugEnabled()) {
313                     isExecuted = true;
314                     LOGGER.debug("Executing onExit action for " + state.getId());
315                 }
316             }
317         if (LOGGER.isDebugEnabled() && !isExecuted) {
318             LOGGER.debug("No onEntry action for " + state.getId());
319 
320         }
321 
322     }
323 
324 }