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, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.apache.statemachine;
19  
20  import org.slf4j.Logger;
21  import org.slf4j.LoggerFactory;
22  
23  import java.lang.reflect.InvocationTargetException;
24  import java.lang.reflect.Method;
25  import java.util.ArrayDeque;
26  import java.util.ArrayList;
27  import java.util.List;
28  import java.util.Queue;
29  import java.util.concurrent.ConcurrentHashMap;
30  import java.util.concurrent.Future;
31  import java.util.concurrent.ScheduledExecutorService;
32  import java.util.concurrent.TimeUnit;
33  
34  public class StateMachine {
35  
36      private static final Logger LOG = LoggerFactory.getLogger(StateMachine.class);
37  
38      private static final String HANDLER_METHOD_NAME = "handleEvent";
39  
40      private static ConcurrentHashMap<Class<?>, ConcurrentHashMap<Class<?>, Method>> stateCaches;
41  
42      static {
43          stateCaches = new ConcurrentHashMap<>();
44      }
45  
46      public static abstract class State {
47  
48          protected final Fsm fsm;
49          private final ConcurrentHashMap<Class<?>, Method> handlerCache;
50  
51          public State(Fsm fsm) {
52              this.fsm = fsm;
53  
54              ConcurrentHashMap<Class<?>, Method> handlerCache = stateCaches.get(getClass());
55              if (handlerCache == null) {
56                  handlerCache = new ConcurrentHashMap<>();
57                  ConcurrentHashMap<Class<?>, Method> old = stateCaches.putIfAbsent(getClass(), handlerCache);
58                  if (old != null) {
59                      handlerCache = old;
60                  }
61              }
62              this.handlerCache = handlerCache;
63          }
64  
65          private Method findHandlerInternal(Class<?> state, Class<?> e) throws NoSuchMethodException {
66              Method[] methods = state.getMethods();
67              List<Method> candidates = new ArrayList<>();
68              for (Method m : methods) {
69                  if (m.getName().equals(HANDLER_METHOD_NAME)
70                          && State.class.isAssignableFrom(m.getReturnType())
71                          && m.getGenericParameterTypes().length == 1) {
72                      candidates.add(m);
73                  }
74              }
75  
76              Method best = null;
77              for (Method m : candidates) {
78                  if (m.getParameterTypes()[0].isAssignableFrom(e)) {
79                      if (best == null) {
80                          best = m;
81                      } else if (best.getParameterTypes()[0]
82                              .isAssignableFrom(m.getParameterTypes()[0])) {
83                          best = m;
84                      }
85                  }
86              }
87              if (best != null) {
88                  best.setAccessible(true);
89                  return best;
90              }
91              throw new NoSuchMethodException("Handler doesn't exist");
92          }
93  
94          private Method findHandler(Class<?> event) throws NoSuchMethodException {
95              Method m = handlerCache.get(event);
96              if (m == null) {
97                  m = findHandlerInternal(getClass(), event);
98                  Method m2 = handlerCache.putIfAbsent(event, m);
99                  if (m2 != null) {
100                     m = m2;
101                 }
102             }
103             return m;
104         }
105 
106         State dispatch(Event e) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException {
107             return (State) findHandler(e.getClass()).invoke(this, e);
108         }
109 
110     }
111 
112     public interface Event {
113     }
114 
115     public interface Fsm {
116         Fsm newChildFsm();
117 
118         void setInitState(State initState);
119 
120         void sendEvent(Event e);
121 
122         Future<?> sendEvent(Event e, long delay, TimeUnit unit);
123 
124         void deferEvent(DeferrableEvent e);
125     }
126 
127     public static class FsmImpl implements Fsm {
128         ScheduledExecutorService executor;
129         private State state;
130         private Queue<DeferrableEvent> deferred;
131 
132         public FsmImpl(ScheduledExecutorService executor) {
133             this.executor = executor;
134             state = null;
135             deferred = new ArrayDeque<>();
136         }
137 
138         private void errorDeferredEvents(Throwable t) {
139             Queue<DeferrableEvent> oldDeferred = deferred;
140             deferred = new ArrayDeque<>();
141 
142             for (DeferrableEvent e : oldDeferred) {
143                 e.error(new IllegalStateException(t));
144             }
145         }
146 
147         @Override
148         public Fsm newChildFsm() {
149             return new FsmImpl(executor);
150         }
151 
152         @Override
153         public void setInitState(State initState) {
154             assert (state == null);
155             state = initState;
156         }
157 
158         public State getState() {
159             return state;
160         }
161 
162         void setState(final State curState, final State newState) {
163             if (curState != state) {
164                 LOG.error("FSM-{}: Tried to transition from {} to {}, but current state is {}",
165                           getFsmId(), state, newState, curState);
166                 throw new IllegalArgumentException();
167             }
168             state = newState;
169 
170             if (LOG.isDebugEnabled()) {
171                 LOG.debug("FSM-{}: State transition {} -> {}", getFsmId(), curState, newState);
172             }
173         }
174 
175         boolean processEvent(Event e) {
176             if (LOG.isDebugEnabled()) {
177                 LOG.debug("FSM-{}: Received event {}@{} in state {}@{}",
178                           getFsmId(), e.getClass().getSimpleName(),
179                           System.identityHashCode(e),
180                           state.getClass().getSimpleName(),
181                           System.identityHashCode(state));
182             }
183             try {
184                 State newState = state.dispatch(e);
185 
186                 if (newState != state) {
187                     setState(state, newState);
188                     return true;
189                 }
190             } catch (Throwable t) {
191                 LOG.error("Caught throwable while handling event", t);
192                 errorDeferredEvents(t);
193             }
194             return false;
195         }
196 
197         class FSMRunnable implements Runnable {
198             final Event e;
199 
200             FSMRunnable(Event e) {
201                 this.e = e;
202             }
203 
204             @Override
205             public void run() {
206                 boolean stateChanged = processEvent(e);
207                 while (stateChanged) {
208                     stateChanged = false;
209                     Queue<DeferrableEvent> prevDeferred = deferred;
210                     deferred = new ArrayDeque<>();
211                     for (DeferrableEvent d : prevDeferred) {
212                         if (stateChanged) {
213                             deferred.add(d);
214                         } else if (processEvent(d)) {
215                             stateChanged = true;
216                         }
217                     }
218                 }
219             }
220         }
221 
222         @Override
223         public void sendEvent(final Event e) {
224             executor.submit(new FSMRunnable(e));
225         }
226 
227         @Override
228         public Future<?> sendEvent(final Event e, final long delay, final TimeUnit unit) {
229             return executor.schedule(new FSMRunnable(e), delay, unit);
230         }
231 
232         @Override
233         public void deferEvent(DeferrableEvent e) {
234             if (LOG.isDebugEnabled()) {
235                 LOG.debug("FSM-{}: deferred {}@{}",
236                           getFsmId(), e.getClass().getSimpleName(), System.identityHashCode(e));
237             }
238             deferred.add(e);
239         }
240 
241         int getFsmId() {
242             return System.identityHashCode(this);
243         }
244 
245         @Override
246         protected void finalize() throws Throwable {
247             super.finalize();
248             LOG.debug("FSM-{}: Finalizing", getFsmId());
249         }
250     }
251 
252     public interface DeferrableEvent extends Event {
253         void error(Throwable exception);
254     }
255 
256 }