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