001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.mina.statemachine;
021
022import java.util.Collection;
023import java.util.Collections;
024import java.util.HashMap;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Map;
028import java.util.Stack;
029
030import org.apache.mina.statemachine.context.StateContext;
031import org.apache.mina.statemachine.event.Event;
032import org.apache.mina.statemachine.event.UnhandledEventException;
033import org.apache.mina.statemachine.transition.SelfTransition;
034import org.apache.mina.statemachine.transition.Transition;
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038/**
039 * Represents a complete state machine. Contains a collection of {@link State}
040 * objects connected by {@link Transition}s. Normally you wouldn't create 
041 * instances of this class directly but rather use the 
042 * {@link org.apache.mina.statemachine.annotation.State} annotation to define
043 * your states and then let {@link StateMachineFactory} create a 
044 * {@link StateMachine} for you.
045 *
046 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
047 */
048public class StateMachine {
049    private static final Logger LOGGER = LoggerFactory.getLogger(StateMachine.class);
050
051    private static final String CALL_STACK = StateMachine.class.getName() + ".callStack";
052
053    private final State startState;
054
055    private final Map<String, State> states;
056
057    private final ThreadLocal<Boolean> processingThreadLocal = new ThreadLocal<Boolean>() {
058        protected Boolean initialValue() {
059            return Boolean.FALSE;
060        }
061    };
062
063    private final ThreadLocal<LinkedList<Event>> eventQueueThreadLocal = new ThreadLocal<LinkedList<Event>>() {
064        protected LinkedList<Event> initialValue() {
065            return new LinkedList<Event>();
066        }
067    };
068
069    /**
070     * Creates a new instance using the specified {@link State}s and start
071     * state.
072     * 
073     * @param states the {@link State}s.
074     * @param startStateId the id of the start {@link State}.
075     */
076    public StateMachine(State[] states, String startStateId) {
077        this.states = new HashMap<String, State>();
078        for (State s : states) {
079            this.states.put(s.getId(), s);
080        }
081        this.startState = getState(startStateId);
082    }
083
084    /**
085     * Creates a new instance using the specified {@link State}s and start
086     * state.
087     * 
088     * @param states the {@link State}s.
089     * @param startStateId the id of the start {@link State}.
090     */
091    public StateMachine(Collection<State> states, String startStateId) {
092        this(states.toArray(new State[0]), startStateId);
093    }
094
095    /**
096     * Returns the {@link State} with the specified id.
097     * 
098     * @param id the id of the {@link State} to return.
099     * @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     * @return an unmodifiable {@link Collection} of all {@link State}s used by
112     * this {@link StateMachine}.
113     */
114    public Collection<State> getStates() {
115        return Collections.unmodifiableCollection(states.values());
116    }
117
118    /**
119     * Processes the specified {@link Event} through this {@link StateMachine}.
120     * Normally you wouldn't call this directly but rather use
121     * {@link StateMachineProxyBuilder} to create a proxy for an interface of
122     * your choice. Any method calls on the proxy will be translated into
123     * {@link Event} objects and then fed to the {@link StateMachine} by the
124     * proxy using this method.
125     * 
126     * @param event the {@link Event} to be handled.
127     */
128    public void handle(Event event) {
129        StateContext context = event.getContext();
130
131        synchronized (context) {
132            LinkedList<Event> eventQueue = eventQueueThreadLocal.get();
133            eventQueue.addLast(event);
134
135            if (processingThreadLocal.get()) {
136                /*
137                 * This thread is already processing an event. Queue this 
138                 * event.
139                 */
140                if (LOGGER.isDebugEnabled()) {
141                    LOGGER.debug("State machine called recursively. Queuing event " + event + " for later processing.");
142                }
143            } else {
144                processingThreadLocal.set(true);
145                try {
146                    if (context.getCurrentState() == null) {
147                        context.setCurrentState(startState);
148                    }
149                    processEvents(eventQueue);
150                } finally {
151                    processingThreadLocal.set(false);
152                }
153            }
154        }
155
156    }
157
158    private void processEvents(LinkedList<Event> eventQueue) {
159        while (!eventQueue.isEmpty()) {
160            Event event = eventQueue.removeFirst();
161            StateContext context = event.getContext();
162            handle(context.getCurrentState(), event);
163        }
164    }
165
166    private void handle(State state, Event event) {
167        StateContext context = event.getContext();
168
169        for (Transition t : state.getTransitions()) {
170            if (LOGGER.isDebugEnabled()) {
171                LOGGER.debug("Trying transition " + t);
172            }
173
174            try {
175                if (t.execute(event)) {
176                    if (LOGGER.isDebugEnabled()) {
177                        LOGGER.debug("Transition " + t + " executed successfully.");
178                    }
179                    setCurrentState(context, t.getNextState());
180
181                    return;
182                }
183            } catch (BreakAndContinueException bace) {
184                if (LOGGER.isDebugEnabled()) {
185                    LOGGER.debug("BreakAndContinueException thrown in " + "transition " + t
186                            + ". Continuing with next transition.");
187                }
188            } catch (BreakAndGotoException bage) {
189                State newState = getState(bage.getStateId());
190
191                if (bage.isNow()) {
192                    if (LOGGER.isDebugEnabled()) {
193                        LOGGER.debug("BreakAndGotoException thrown in " + "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 " + "transition " + t + ". Moving to state "
201                                + newState.getId() + " next.");
202                    }
203                    setCurrentState(context, newState);
204                }
205                return;
206            } catch (BreakAndCallException bace) {
207                State newState = getState(bace.getStateId());
208
209                Stack<State> callStack = getCallStack(context);
210                State returnTo = bace.getReturnToStateId() != null ? getState(bace.getReturnToStateId()) : context
211                        .getCurrentState();
212                callStack.push(returnTo);
213
214                if (bace.isNow()) {
215                    if (LOGGER.isDebugEnabled()) {
216                        LOGGER.debug("BreakAndCallException thrown in " + "transition " + t + ". Moving to state "
217                                + newState.getId() + " now.");
218                    }
219                    setCurrentState(context, newState);
220                    handle(newState, event);
221                } else {
222                    if (LOGGER.isDebugEnabled()) {
223                        LOGGER.debug("BreakAndCallException thrown in " + "transition " + t + ". Moving to state "
224                                + newState.getId() + " next.");
225                    }
226                    setCurrentState(context, newState);
227                }
228                return;
229            } catch (BreakAndReturnException bare) {
230                Stack<State> callStack = getCallStack(context);
231                State newState = callStack.pop();
232
233                if (bare.isNow()) {
234                    if (LOGGER.isDebugEnabled()) {
235                        LOGGER.debug("BreakAndReturnException thrown in " + "transition " + t + ". Moving to state "
236                                + newState.getId() + " now.");
237                    }
238                    setCurrentState(context, newState);
239                    handle(newState, event);
240                } else {
241                    if (LOGGER.isDebugEnabled()) {
242                        LOGGER.debug("BreakAndReturnException thrown in " + "transition " + t + ". Moving to state "
243                                + newState.getId() + " next.");
244                    }
245                    setCurrentState(context, newState);
246                }
247                return;
248            }
249        }
250
251        /*
252         * No transition could handle the event. Try with the parent state if
253         * there is one.
254         */
255
256        if (state.getParent() != null) {
257            handle(state.getParent(), event);
258        } else {
259            throw new UnhandledEventException(event);
260        }
261    }
262
263    private Stack<State> getCallStack(StateContext context) {
264        @SuppressWarnings("unchecked")
265        Stack<State> callStack = (Stack<State>) context.getAttribute(CALL_STACK);
266        if (callStack == null) {
267            callStack = new Stack<State>();
268            context.setAttribute(CALL_STACK, callStack);
269        }
270        return callStack;
271    }
272
273    private void setCurrentState(StateContext context, State newState) {
274        if (newState != null) {
275            if (LOGGER.isDebugEnabled()) {
276                if (newState != context.getCurrentState()) {
277                    LOGGER.debug("Leaving state " + context.getCurrentState().getId());
278                    LOGGER.debug("Entering state " + newState.getId());
279                }
280            }
281            executeOnExits(context, context.getCurrentState());
282            executeOnEntries(context, newState);
283            context.setCurrentState(newState);
284        }
285    }
286
287    void executeOnExits(StateContext context, State state) {
288        List<SelfTransition> onExits = state.getOnExitSelfTransitions();
289        boolean isExecuted = false;
290
291        if (onExits != null)
292            for (SelfTransition selfTransition : onExits) {
293                selfTransition.execute(context, state);
294                if (LOGGER.isDebugEnabled()) {
295                    isExecuted = true;
296                    LOGGER.debug("Executing onEntry action for " + state.getId());
297                }
298            }
299        if (LOGGER.isDebugEnabled() && !isExecuted) {
300            LOGGER.debug("No onEntry action for " + state.getId());
301
302        }
303    }
304
305    void executeOnEntries(StateContext context, State state) {
306        List<SelfTransition> onEntries = state.getOnEntrySelfTransitions();
307        boolean isExecuted = false;
308
309        if (onEntries != null)
310            for (SelfTransition selfTransition : onEntries) {
311                selfTransition.execute(context, state);
312                if (LOGGER.isDebugEnabled()) {
313                    isExecuted = true;
314                    LOGGER.debug("Executing onExit action for " + state.getId());
315                }
316            }
317        if (LOGGER.isDebugEnabled() && !isExecuted) {
318            LOGGER.debug("No onEntry action for " + state.getId());
319
320        }
321
322    }
323
324}