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