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