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.transition;
021
022import java.lang.reflect.InvocationTargetException;
023import java.lang.reflect.Method;
024import java.util.Arrays;
025
026import org.apache.mina.statemachine.State;
027import org.apache.mina.statemachine.StateMachine;
028import org.apache.mina.statemachine.StateMachineFactory;
029import org.apache.mina.statemachine.annotation.Transition;
030import org.apache.mina.statemachine.context.StateContext;
031import org.apache.mina.statemachine.event.Event;
032import org.slf4j.Logger;
033import org.slf4j.LoggerFactory;
034
035/**
036 * {@link Transition} which invokes a {@link Method}. The {@link Method} will
037 * only be invoked if its argument types actually matches a subset of the 
038 * {@link Event}'s argument types. The argument types are matched in order so
039 * you must make sure the order of the method's arguments corresponds to the
040 * order of the event's arguments. 
041 *<p>
042 * If the first method argument type matches
043 * {@link Event} the current {@link Event} will be bound to that argument. In
044 * the same manner the second argument (or first if the method isn't interested 
045 * in the current {@link Event}) can have the {@link StateContext} type and will
046 * in that case be bound to the current {@link StateContext}.
047 * </p>
048 * <p>
049 * Normally you wouldn't create instances of this class directly but rather use the 
050 * {@link Transition} annotation to define the methods which should be used as
051 * transitions in your state machine and then let {@link StateMachineFactory} create a 
052 * {@link StateMachine} for you.
053 * </p>
054 *
055 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
056 */
057public class MethodTransition extends AbstractTransition {
058    private static final Logger LOGGER = LoggerFactory.getLogger(MethodTransition.class);
059
060    private static final Object[] EMPTY_ARGUMENTS = new Object[0];
061
062    private final Method method;
063
064    private final Object target;
065
066    /**
067     * Creates a new instance with the specified {@link State} as next state 
068     * and for the specified {@link Event} id.
069     * 
070     * @param eventId the {@link Event} id.
071     * @param nextState the next {@link State}.
072     * @param method the target method.
073     * @param target the target object.
074     */
075    public MethodTransition(Object eventId, State nextState, Method method, Object target) {
076        super(eventId, nextState);
077        this.method = method;
078        this.target = target;
079    }
080
081    /**
082     * Creates a new instance which will loopback to the same {@link State} 
083     * for the specified {@link Event} id.
084     * 
085     * @param eventId the {@link Event} id.
086     * @param method the target method.
087     * @param target the target object.
088     */
089    public MethodTransition(Object eventId, Method method, Object target) {
090        this(eventId, null, method, target);
091    }
092
093    /**
094     * Creates a new instance with the specified {@link State} as next state 
095     * and for the specified {@link Event} id. The target {@link Method} will
096     * be the method in the specified target object with the same name as the
097     * specified {@link Event} id.
098     * 
099     * @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}