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      * Returns an unmodifiable {@link Collection} of all {@link State}s used by
112      * this {@link StateMachine}.
113      * 
114      * @return the {@link State}s.
115      */
116     public Collection<State> getStates() {
117         return Collections.unmodifiableCollection(states.values());
118     }
119 
120     /**
121      * Processes the specified {@link Event} through this {@link StateMachine}.
122      * Normally you wouldn't call this directly but rather use
123      * {@link StateMachineProxyBuilder} to create a proxy for an interface of
124      * your choice. Any method calls on the proxy will be translated into
125      * {@link Event} objects and then fed to the {@link StateMachine} by the
126      * proxy using this method.
127      * 
128      * @param event the {@link Event} to be handled.
129      */
130     public void handle(Event event) {
131         StateContext context = event.getContext();
132 
133         synchronized (context) {
134             LinkedList<Event> eventQueue = eventQueueThreadLocal.get();
135             eventQueue.addLast(event);
136 
137             if (processingThreadLocal.get()) {
138                 /*
139                  * This thread is already processing an event. Queue this 
140                  * event.
141                  */
142                 if (LOGGER.isDebugEnabled()) {
143                     LOGGER.debug("State machine called recursively. Queuing event " + event + " for later processing.");
144                 }
145             } else {
146                 processingThreadLocal.set(true);
147                 try {
148                     if (context.getCurrentState() == null) {
149                         context.setCurrentState(startState);
150                     }
151                     processEvents(eventQueue);
152                 } finally {
153                     processingThreadLocal.set(false);
154                 }
155             }
156         }
157 
158     }
159 
160     private void processEvents(LinkedList<Event> eventQueue) {
161         while (!eventQueue.isEmpty()) {
162             Event event = eventQueue.removeFirst();
163             StateContext context = event.getContext();
164             handle(context.getCurrentState(), event);
165         }
166     }
167 
168     private void handle(State state, Event event) {
169         StateContext context = event.getContext();
170 
171         for (Transition t : state.getTransitions()) {
172             if (LOGGER.isDebugEnabled()) {
173                 LOGGER.debug("Trying transition " + t);
174             }
175 
176             try {
177                 if (t.execute(event)) {
178                     if (LOGGER.isDebugEnabled()) {
179                         LOGGER.debug("Transition " + t + " executed successfully.");
180                     }
181                     setCurrentState(context, t.getNextState());
182 
183                     return;
184                 }
185             } catch (BreakAndContinueException bace) {
186                 if (LOGGER.isDebugEnabled()) {
187                     LOGGER.debug("BreakAndContinueException thrown in " + "transition " + t
188                             + ". Continuing with next transition.");
189                 }
190             } catch (BreakAndGotoException bage) {
191                 State newState = getState(bage.getStateId());
192 
193                 if (bage.isNow()) {
194                     if (LOGGER.isDebugEnabled()) {
195                         LOGGER.debug("BreakAndGotoException thrown in " + "transition " + t + ". Moving to state "
196                                 + newState.getId() + " now.");
197                     }
198                     setCurrentState(context, newState);
199                     handle(newState, event);
200                 } else {
201                     if (LOGGER.isDebugEnabled()) {
202                         LOGGER.debug("BreakAndGotoException thrown in " + "transition " + t + ". Moving to state "
203                                 + newState.getId() + " next.");
204                     }
205                     setCurrentState(context, newState);
206                 }
207                 return;
208             } catch (BreakAndCallException bace) {
209                 State newState = getState(bace.getStateId());
210 
211                 Stack<State> callStack = getCallStack(context);
212                 State returnTo = bace.getReturnToStateId() != null ? getState(bace.getReturnToStateId()) : context
213                         .getCurrentState();
214                 callStack.push(returnTo);
215 
216                 if (bace.isNow()) {
217                     if (LOGGER.isDebugEnabled()) {
218                         LOGGER.debug("BreakAndCallException thrown in " + "transition " + t + ". Moving to state "
219                                 + newState.getId() + " now.");
220                     }
221                     setCurrentState(context, newState);
222                     handle(newState, event);
223                 } else {
224                     if (LOGGER.isDebugEnabled()) {
225                         LOGGER.debug("BreakAndCallException thrown in " + "transition " + t + ". Moving to state "
226                                 + newState.getId() + " next.");
227                     }
228                     setCurrentState(context, newState);
229                 }
230                 return;
231             } catch (BreakAndReturnException bare) {
232                 Stack<State> callStack = getCallStack(context);
233                 State newState = callStack.pop();
234 
235                 if (bare.isNow()) {
236                     if (LOGGER.isDebugEnabled()) {
237                         LOGGER.debug("BreakAndReturnException thrown in " + "transition " + t + ". Moving to state "
238                                 + newState.getId() + " now.");
239                     }
240                     setCurrentState(context, newState);
241                     handle(newState, event);
242                 } else {
243                     if (LOGGER.isDebugEnabled()) {
244                         LOGGER.debug("BreakAndReturnException thrown in " + "transition " + t + ". Moving to state "
245                                 + newState.getId() + " next.");
246                     }
247                     setCurrentState(context, newState);
248                 }
249                 return;
250             }
251         }
252 
253         /*
254          * No transition could handle the event. Try with the parent state if
255          * there is one.
256          */
257 
258         if (state.getParent() != null) {
259             handle(state.getParent(), event);
260         } else {
261             throw new UnhandledEventException(event);
262         }
263     }
264 
265     private Stack<State> getCallStack(StateContext context) {
266         @SuppressWarnings("unchecked")
267         Stack<State> callStack = (Stack<State>) context.getAttribute(CALL_STACK);
268         if (callStack == null) {
269             callStack = new Stack<State>();
270             context.setAttribute(CALL_STACK, callStack);
271         }
272         return callStack;
273     }
274 
275     private void setCurrentState(StateContext context, State newState) {
276         if (newState != null) {
277             if (LOGGER.isDebugEnabled()) {
278                 if (newState != context.getCurrentState()) {
279                     LOGGER.debug("Leaving state " + context.getCurrentState().getId());
280                     LOGGER.debug("Entering state " + newState.getId());
281                 }
282             }
283             executeOnExits(context, context.getCurrentState());
284             executeOnEntries(context, newState);
285             context.setCurrentState(newState);
286         }
287     }
288 
289     void executeOnExits(StateContext context, State state) {
290         List<SelfTransition> onExits = state.getOnExitSelfTransitions();
291         boolean isExecuted = false;
292 
293         if (onExits != null)
294             for (SelfTransition selfTransition : onExits) {
295                 selfTransition.execute(context, state);
296                 if (LOGGER.isDebugEnabled()) {
297                     isExecuted = true;
298                     LOGGER.debug("Executing onEntry action for " + state.getId());
299                 }
300             }
301         if (LOGGER.isDebugEnabled() && !isExecuted) {
302             LOGGER.debug("No onEntry action for " + state.getId());
303 
304         }
305     }
306 
307     void executeOnEntries(StateContext context, State state) {
308         List<SelfTransition> onEntries = state.getOnEntrySelfTransitions();
309         boolean isExecuted = false;
310 
311         if (onEntries != null)
312             for (SelfTransition selfTransition : onEntries) {
313                 selfTransition.execute(context, state);
314                 if (LOGGER.isDebugEnabled()) {
315                     isExecuted = true;
316                     LOGGER.debug("Executing onExit action for " + state.getId());
317                 }
318             }
319         if (LOGGER.isDebugEnabled() && !isExecuted) {
320             LOGGER.debug("No onEntry action for " + state.getId());
321 
322         }
323 
324     }
325 
326 }