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 }