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.transition;
21  
22  import java.lang.reflect.InvocationTargetException;
23  import java.lang.reflect.Method;
24  import java.util.Arrays;
25  
26  import org.apache.mina.statemachine.State;
27  import org.apache.mina.statemachine.StateMachine;
28  import org.apache.mina.statemachine.StateMachineFactory;
29  import org.apache.mina.statemachine.annotation.Transition;
30  import org.apache.mina.statemachine.context.StateContext;
31  import org.apache.mina.statemachine.event.Event;
32  import org.slf4j.Logger;
33  import org.slf4j.LoggerFactory;
34  
35  /**
36   * {@link Transition} which invokes a {@link Method}. The {@link Method} will
37   * only be invoked if its argument types actually matches a subset of the 
38   * {@link Event}'s argument types. The argument types are matched in order so
39   * you must make sure the order of the method's arguments corresponds to the
40   * order of the event's arguments. 
41   *<p>
42   * If the first method argument type matches
43   * {@link Event} the current {@link Event} will be bound to that argument. In
44   * the same manner the second argument (or first if the method isn't interested 
45   * in the current {@link Event}) can have the {@link StateContext} type and will
46   * in that case be bound to the current {@link StateContext}.
47   * </p>
48   * <p>
49   * Normally you wouldn't create instances of this class directly but rather use the 
50   * {@link Transition} annotation to define the methods which should be used as
51   * transitions in your state machine and then let {@link StateMachineFactory} create a 
52   * {@link StateMachine} for you.
53   * </p>
54   *
55   * @author <a href="http://mina.apache.org">Apache MINA Project</a>
56   */
57  public class MethodTransition extends AbstractTransition {
58      private static final Logger LOGGER = LoggerFactory.getLogger(MethodTransition.class);
59  
60      private static final Object[] EMPTY_ARGUMENTS = new Object[0];
61  
62      private final Method method;
63  
64      private final Object target;
65  
66      /**
67       * Creates a new instance with the specified {@link State} as next state 
68       * and for the specified {@link Event} id.
69       * 
70       * @param eventId the {@link Event} id.
71       * @param nextState the next {@link State}.
72       * @param method the target method.
73       * @param target the target object.
74       */
75      public MethodTransition(Object eventId, State nextState, Method method, Object target) {
76          super(eventId, nextState);
77          this.method = method;
78          this.target = target;
79      }
80  
81      /**
82       * Creates a new instance which will loopback to the same {@link State} 
83       * for the specified {@link Event} id.
84       * 
85       * @param eventId the {@link Event} id.
86       * @param method the target method.
87       * @param target the target object.
88       */
89      public MethodTransition(Object eventId, Method method, Object target) {
90          this(eventId, null, method, target);
91      }
92  
93      /**
94       * Creates a new instance with the specified {@link State} as next state 
95       * and for the specified {@link Event} id. The target {@link Method} will
96       * be the method in the specified target object with the same name as the
97       * specified {@link Event} id.
98       * 
99       * @param eventId the {@link Event} id.
100      * @param nextState the next {@link State}.
101      * @param target the target object.
102      * @throws NoSuchMethodException if no method could be found with a name 
103      *         equal to the {@link Event} id.
104      * @throws AmbiguousMethodException if more than one method was found with 
105      *         a name equal to the {@link Event} id.
106      */
107     public MethodTransition(Object eventId, State nextState, Object target) {
108         this(eventId, nextState, eventId.toString(), target);
109     }
110 
111     /**
112      * Creates a new instance which will loopback to the same {@link State} 
113      * for the specified {@link Event} id. The target {@link Method} will
114      * be the method in the specified target object with the same name as the
115      * specified {@link Event} id.
116      * 
117      * @param eventId the {@link Event} id.
118      * @param target the target object.
119      * @throws NoSuchMethodException if no method could be found with a name 
120      *         equal to the {@link Event} id.
121      * @throws AmbiguousMethodException if more than one method was found with 
122      *         a name equal to the {@link Event} id.
123      */
124     public MethodTransition(Object eventId, Object target) {
125         this(eventId, eventId.toString(), target);
126     }
127 
128     /**
129      * Creates a new instance which will loopback to the same {@link State} 
130      * for the specified {@link Event} id.
131      * 
132      * @param eventId the {@link Event} id.
133      * @param methodName the name of the target {@link Method}.
134      * @param target the target object.
135      * @throws NoSuchMethodException if the method could not be found.
136      * @throws AmbiguousMethodException if there are more than one method with 
137      *         the specified name.
138      */
139     public MethodTransition(Object eventId, String methodName, Object target) {
140         this(eventId, null, methodName, target);
141     }
142 
143     /**
144      * Creates a new instance with the specified {@link State} as next state 
145      * and for the specified {@link Event} id.
146      * 
147      * @param eventId the {@link Event} id.
148      * @param nextState the next {@link State}.
149      * @param methodName the name of the target {@link Method}.
150      * @param target the target object.
151      * @throws NoSuchMethodException if the method could not be found.
152      * @throws AmbiguousMethodException if there are more than one method with 
153      *         the specified name.
154      */
155     public MethodTransition(Object eventId, State nextState, String methodName, Object target) {
156         super(eventId, nextState);
157 
158         this.target = target;
159 
160         Method[] candidates = target.getClass().getMethods();
161         Method result = null;
162         for (int i = 0; i < candidates.length; i++) {
163             if (candidates[i].getName().equals(methodName)) {
164                 if (result != null) {
165                     throw new AmbiguousMethodException(methodName);
166                 }
167                 result = candidates[i];
168             }
169         }
170 
171         if (result == null) {
172             throw new NoSuchMethodException(methodName);
173         }
174 
175         this.method = result;
176     }
177 
178     /**
179      * @return the target {@link Method}.
180      */
181     public Method getMethod() {
182         return method;
183     }
184 
185     /**
186      * @return the target object.
187      */
188     public Object getTarget() {
189         return target;
190     }
191 
192     public boolean doExecute(Event event) {
193         Class<?>[] types = method.getParameterTypes();
194 
195         if (types.length == 0) {
196             invokeMethod(EMPTY_ARGUMENTS);
197             return true;
198         }
199 
200         if (types.length > 2 + event.getArguments().length) {
201             return false;
202         }
203 
204         Object[] args = new Object[types.length];
205 
206         int i = 0;
207         if (match(types[i], event, Event.class)) {
208             args[i++] = event;
209         }
210         if (i < args.length && match(types[i], event.getContext(), StateContext.class)) {
211             args[i++] = event.getContext();
212         }
213         Object[] eventArgs = event.getArguments();
214         for (int j = 0; i < args.length && j < eventArgs.length; j++) {
215             if (match(types[i], eventArgs[j], Object.class)) {
216                 args[i++] = eventArgs[j];
217             }
218         }
219 
220         if (args.length > i) {
221             return false;
222         }
223 
224         invokeMethod(args);
225 
226         return true;
227     }
228 
229     private boolean match(Class<?> paramType, Object arg, Class<?> argType) {
230         if (paramType.isPrimitive()) {
231             if (paramType.equals(Boolean.TYPE)) {
232                 return arg instanceof Boolean;
233             }
234             if (paramType.equals(Integer.TYPE)) {
235                 return arg instanceof Integer;
236             }
237             if (paramType.equals(Long.TYPE)) {
238                 return arg instanceof Long;
239             }
240             if (paramType.equals(Short.TYPE)) {
241                 return arg instanceof Short;
242             }
243             if (paramType.equals(Byte.TYPE)) {
244                 return arg instanceof Byte;
245             }
246             if (paramType.equals(Double.TYPE)) {
247                 return arg instanceof Double;
248             }
249             if (paramType.equals(Float.TYPE)) {
250                 return arg instanceof Float;
251             }
252             if (paramType.equals(Character.TYPE)) {
253                 return arg instanceof Character;
254             }
255         }
256         return argType.isAssignableFrom(paramType) && paramType.isAssignableFrom(arg.getClass());
257     }
258 
259     private void invokeMethod(Object[] arguments) {
260         try {
261             if (LOGGER.isDebugEnabled()) {
262                 LOGGER.debug("Executing method " + method + " with arguments " + Arrays.asList(arguments));
263             }
264             method.invoke(target, arguments);
265         } catch (InvocationTargetException ite) {
266             if (ite.getCause() instanceof RuntimeException) {
267                 throw (RuntimeException) ite.getCause();
268             }
269             throw new MethodInvocationException(method, ite);
270         } catch (IllegalAccessException iae) {
271             throw new MethodInvocationException(method, iae);
272         }
273     }
274 
275     public boolean equals(Object o) {
276         if (o == this) {
277             return true;
278         }
279 
280         if (!(o instanceof MethodTransition)) {
281             return false;
282         }
283         
284         MethodTransition that = (MethodTransition) o;
285         
286         return method.equals(that.method) && target.equals(that.target);
287     }
288 
289     public int hashCode() {
290         int h = 17;
291         h = h*37 + super.hashCode();
292         h = h*37 + method.hashCode();
293         h = h*37 + target.hashCode();
294         
295         return h;
296     }
297 
298     public String toString() {
299         StringBuilder sb = new StringBuilder();
300         
301         sb.append("MethodTransition[");
302         sb.append(super.toString());
303         sb.append(",method=").append(method);
304         sb.append(']');
305         
306         return sb.toString();
307     }
308 }