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.lang.reflect.InvocationHandler;
023import java.lang.reflect.Method;
024import java.lang.reflect.Proxy;
025
026import org.apache.mina.statemachine.context.SingletonStateContextLookup;
027import org.apache.mina.statemachine.context.StateContext;
028import org.apache.mina.statemachine.context.StateContextLookup;
029import org.apache.mina.statemachine.event.DefaultEventFactory;
030import org.apache.mina.statemachine.event.Event;
031import org.apache.mina.statemachine.event.EventArgumentsInterceptor;
032import org.apache.mina.statemachine.event.EventFactory;
033import org.apache.mina.statemachine.event.UnhandledEventException;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036
037/**
038 * Used to create proxies which will forward all method calls on them to a
039 * {@link StateMachine}.
040 *
041 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
042 */
043public class StateMachineProxyBuilder {
044    private static final Logger log = LoggerFactory.getLogger(StateMachineProxyBuilder.class);
045
046    private static final Object[] EMPTY_ARGUMENTS = new Object[0];
047
048    private StateContextLookup contextLookup = new SingletonStateContextLookup();
049
050    private EventFactory eventFactory = new DefaultEventFactory();
051
052    private EventArgumentsInterceptor interceptor = null;
053
054    private boolean ignoreUnhandledEvents = false;
055
056    private boolean ignoreStateContextLookupFailure = false;
057
058    private String name = null;
059
060    /*
061     * The classloader to use. Iif null we will use the current thread's 
062     * context classloader.
063     */
064    private ClassLoader defaultCl = null;
065
066    public StateMachineProxyBuilder() {
067    }
068
069    /**
070     * Sets the name of the proxy created by this builder. This will be used 
071     * by the proxies <code>toString()</code> method. If not specified a default
072     * auto generated name will be used.
073     * 
074     * @param name the name.
075     * @return this {@link StateMachineProxyBuilder} for method chaining.
076     */
077    public StateMachineProxyBuilder setName(String name) {
078        this.name = name;
079        return this;
080    }
081
082    /**
083     * Sets the {@link StateContextLookup} to be used. The default is to use
084     * a {@link SingletonStateContextLookup}.
085     * 
086     * @param contextLookup the {@link StateContextLookup} to use.
087     * @return this {@link StateMachineProxyBuilder} for method chaining. 
088     */
089    public StateMachineProxyBuilder setStateContextLookup(StateContextLookup contextLookup) {
090        this.contextLookup = contextLookup;
091        return this;
092    }
093
094    /**
095     * Sets the {@link EventFactory} to be used. The default is to use a 
096     * {@link DefaultEventFactory}.
097     * 
098     * @param eventFactory the {@link EventFactory} to use.
099     * @return this {@link StateMachineProxyBuilder} for method chaining.
100     */
101    public StateMachineProxyBuilder setEventFactory(EventFactory eventFactory) {
102        this.eventFactory = eventFactory;
103        return this;
104    }
105
106    /**
107     * Sets the {@link EventArgumentsInterceptor} to be used. By default no
108     * {@link EventArgumentsInterceptor} will be used.
109     * 
110     * @param interceptor the {@link EventArgumentsInterceptor} to use.
111     * @return this {@link StateMachineProxyBuilder} for method chaining.
112     */
113    public StateMachineProxyBuilder setEventArgumentsInterceptor(EventArgumentsInterceptor interceptor) {
114        this.interceptor = interceptor;
115        return this;
116    }
117
118    /**
119     * Sets whether events which have no handler in the current state will raise 
120     * an exception or be silently ignored. The default is to raise an 
121     * exception. 
122     * 
123     * @param b <tt>true</tt> to ignore context lookup failures.
124     * @return this {@link StateMachineProxyBuilder} for method chaining. 
125     */
126    public StateMachineProxyBuilder setIgnoreUnhandledEvents(boolean b) {
127        this.ignoreUnhandledEvents = b;
128        return this;
129    }
130
131    /**
132     * Sets whether the failure to lookup a {@link StateContext} corresponding
133     * to a method call on the proxy produced by this builder will raise an
134     * exception or be silently ignored. The default is to raise an exception.
135     * 
136     * @param b <tt>true</tt> to ignore context lookup failures.
137     * @return this {@link StateMachineProxyBuilder} for method chaining. 
138     */
139    public StateMachineProxyBuilder setIgnoreStateContextLookupFailure(boolean b) {
140        this.ignoreStateContextLookupFailure = b;
141        return this;
142    }
143
144    /**
145     * Sets the class loader to use for instantiating proxies. The default is
146     * to use the current threads context {@link ClassLoader} as returned by 
147     * {@link Thread#getContextClassLoader()}.
148     * 
149     * @param cl the class loader
150     * @return this {@link StateMachineProxyBuilder} for method chaining. 
151     */
152    public StateMachineProxyBuilder setClassLoader(ClassLoader cl) {
153        this.defaultCl = cl;
154        return this;
155    }
156
157    /**
158     * Creates a proxy for the specified interface and which uses the specified 
159     * {@link StateMachine}.
160     * 
161     * @param <T> The specified interface type
162     * @param iface the interface the proxy will implement.
163     * @param sm the {@link StateMachine} which will receive the events 
164     *        generated by the method calls on the proxy.
165     * @return the proxy object.
166     */
167    @SuppressWarnings("unchecked")
168    public <T> T create(Class<T> iface, StateMachine sm) {
169        return (T) create(new Class[] { iface }, sm);
170    }
171
172    /**
173     * Creates a proxy for the specified interfaces and which uses the specified 
174     * {@link StateMachine}.
175     * 
176     * @param ifaces the interfaces the proxy will implement.
177     * @param sm the {@link StateMachine} which will receive the events 
178     *        generated by the method calls on the proxy.
179     * @return the proxy object.
180     */
181    public Object create(Class<?>[] ifaces, StateMachine sm) {
182        ClassLoader cl = defaultCl;
183        if (cl == null) {
184            cl = Thread.currentThread().getContextClassLoader();
185        }
186
187        InvocationHandler handler = new MethodInvocationHandler(sm, contextLookup, interceptor, eventFactory,
188                ignoreUnhandledEvents, ignoreStateContextLookupFailure, name);
189
190        return Proxy.newProxyInstance(cl, ifaces, handler);
191    }
192
193    private static class MethodInvocationHandler implements InvocationHandler {
194        private final StateMachine sm;
195
196        private final StateContextLookup contextLookup;
197
198        private final EventArgumentsInterceptor interceptor;
199
200        private final EventFactory eventFactory;
201
202        private final boolean ignoreUnhandledEvents;
203
204        private final boolean ignoreStateContextLookupFailure;
205
206        private final String name;
207
208        public MethodInvocationHandler(StateMachine sm, StateContextLookup contextLookup,
209                EventArgumentsInterceptor interceptor, EventFactory eventFactory, boolean ignoreUnhandledEvents,
210                boolean ignoreStateContextLookupFailure, String name) {
211
212            this.contextLookup = contextLookup;
213            this.sm = sm;
214            this.interceptor = interceptor;
215            this.eventFactory = eventFactory;
216            this.ignoreUnhandledEvents = ignoreUnhandledEvents;
217            this.ignoreStateContextLookupFailure = ignoreStateContextLookupFailure;
218            this.name = name;
219        }
220
221        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
222            if ("hashCode".equals(method.getName()) && args == null) {
223                return Integer.valueOf(System.identityHashCode(proxy));
224            }
225            if ("equals".equals(method.getName()) && args.length == 1) {
226                return Boolean.valueOf(proxy == args[0]);
227            }
228            if ("toString".equals(method.getName()) && args == null) {
229                return (name != null ? name : proxy.getClass().getName()) + "@"
230                        + Integer.toHexString(System.identityHashCode(proxy));
231            }
232
233            if (log.isDebugEnabled()) {
234                log.debug("Method invoked: " + method);
235            }
236
237            args = args == null ? EMPTY_ARGUMENTS : args;
238            if (interceptor != null) {
239                args = interceptor.modify(args);
240            }
241
242            StateContext context = contextLookup.lookup(args);
243
244            if (context == null) {
245                if (ignoreStateContextLookupFailure) {
246                    return null;
247                }
248                throw new IllegalStateException("Cannot determine state " + "context for method invocation: " + method);
249            }
250
251            Event event = eventFactory.create(context, method, args);
252
253            try {
254                sm.handle(event);
255            } catch (UnhandledEventException uee) {
256                if (!ignoreUnhandledEvents) {
257                    throw uee;
258                }
259            }
260
261            return null;
262        }
263    }
264}