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}