1/*
2 * Copyright (c) 2002, 2013, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.  Oracle designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Oracle in the LICENSE file that accompanied this code.
10 *
11 * This code is distributed in the hope that it will be useful, but WITHOUT
12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14 * version 2 for more details (a copy is included in the LICENSE file that
15 * accompanied this code).
16 *
17 * You should have received a copy of the GNU General Public License version
18 * 2 along with this work; if not, write to the Free Software Foundation,
19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20 *
21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22 * or visit www.oracle.com if you need additional information or have any
23 * questions.
24 */
25
26package javax.management;
27
28import com.sun.jmx.mbeanserver.MXBeanProxy;
29
30import java.lang.ref.WeakReference;
31import java.lang.reflect.InvocationHandler;
32import java.lang.reflect.Method;
33import java.lang.reflect.Proxy;
34import java.util.Arrays;
35import java.util.WeakHashMap;
36
37/**
38 * <p>{@link InvocationHandler} that forwards methods in an MBean's
39 * management interface through the MBean server to the MBean.</p>
40 *
41 * <p>Given an {@link MBeanServerConnection}, the {@link ObjectName}
42 * of an MBean within that MBean server, and a Java interface
43 * <code>Intf</code> that describes the management interface of the
44 * MBean using the patterns for a Standard MBean or an MXBean, this
45 * class can be used to construct a proxy for the MBean.  The proxy
46 * implements the interface <code>Intf</code> such that all of its
47 * methods are forwarded through the MBean server to the MBean.</p>
48 *
49 * <p>If the {@code InvocationHandler} is for an MXBean, then the parameters of
50 * a method are converted from the type declared in the MXBean
51 * interface into the corresponding mapped type, and the return value
52 * is converted from the mapped type into the declared type.  For
53 * example, with the method<br>
54
55 * {@code public List<String> reverse(List<String> list);}<br>
56
57 * and given that the mapped type for {@code List<String>} is {@code
58 * String[]}, a call to {@code proxy.reverse(someList)} will convert
59 * {@code someList} from a {@code List<String>} to a {@code String[]},
60 * call the MBean operation {@code reverse}, then convert the returned
61 * {@code String[]} into a {@code List<String>}.</p>
62 *
63 * <p>The method Object.toString(), Object.hashCode(), or
64 * Object.equals(Object), when invoked on a proxy using this
65 * invocation handler, is forwarded to the MBean server as a method on
66 * the proxied MBean only if it appears in one of the proxy's
67 * interfaces.  For a proxy created with {@link
68 * JMX#newMBeanProxy(MBeanServerConnection, ObjectName, Class)
69 * JMX.newMBeanProxy} or {@link
70 * JMX#newMXBeanProxy(MBeanServerConnection, ObjectName, Class)
71 * JMX.newMXBeanProxy}, this means that the method must appear in the
72 * Standard MBean or MXBean interface.  Otherwise these methods have
73 * the following behavior:
74 * <ul>
75 * <li>toString() returns a string representation of the proxy
76 * <li>hashCode() returns a hash code for the proxy such
77 * that two equal proxies have the same hash code
78 * <li>equals(Object)
79 * returns true if and only if the Object argument is of the same
80 * proxy class as this proxy, with an MBeanServerInvocationHandler
81 * that has the same MBeanServerConnection and ObjectName; if one
82 * of the {@code MBeanServerInvocationHandler}s was constructed with
83 * a {@code Class} argument then the other must have been constructed
84 * with the same {@code Class} for {@code equals} to return true.
85 * </ul>
86 *
87 * @since 1.5
88 */
89public class MBeanServerInvocationHandler implements InvocationHandler {
90    /**
91     * <p>Invocation handler that forwards methods through an MBean
92     * server to a Standard MBean.  This constructor may be called
93     * instead of relying on {@link
94     * JMX#newMBeanProxy(MBeanServerConnection, ObjectName, Class)
95     * JMX.newMBeanProxy}, for instance if you need to supply a
96     * different {@link ClassLoader} to {@link Proxy#newProxyInstance
97     * Proxy.newProxyInstance}.</p>
98     *
99     * <p>This constructor is not appropriate for an MXBean.  Use
100     * {@link #MBeanServerInvocationHandler(MBeanServerConnection,
101     * ObjectName, boolean)} for that.  This constructor is equivalent
102     * to {@code new MBeanServerInvocationHandler(connection,
103     * objectName, false)}.</p>
104     *
105     * @param connection the MBean server connection through which all
106     * methods of a proxy using this handler will be forwarded.
107     *
108     * @param objectName the name of the MBean within the MBean server
109     * to which methods will be forwarded.
110     */
111    public MBeanServerInvocationHandler(MBeanServerConnection connection,
112                                        ObjectName objectName) {
113
114        this(connection, objectName, false);
115    }
116
117    /**
118     * <p>Invocation handler that can forward methods through an MBean
119     * server to a Standard MBean or MXBean.  This constructor may be called
120     * instead of relying on {@link
121     * JMX#newMXBeanProxy(MBeanServerConnection, ObjectName, Class)
122     * JMX.newMXBeanProxy}, for instance if you need to supply a
123     * different {@link ClassLoader} to {@link Proxy#newProxyInstance
124     * Proxy.newProxyInstance}.</p>
125     *
126     * @param connection the MBean server connection through which all
127     * methods of a proxy using this handler will be forwarded.
128     *
129     * @param objectName the name of the MBean within the MBean server
130     * to which methods will be forwarded.
131     *
132     * @param isMXBean if true, the proxy is for an {@link MXBean}, and
133     * appropriate mappings will be applied to method parameters and return
134     * values.
135     *
136     * @since 1.6
137     */
138    public MBeanServerInvocationHandler(MBeanServerConnection connection,
139                                        ObjectName objectName,
140                                        boolean isMXBean) {
141        if (connection == null) {
142            throw new IllegalArgumentException("Null connection");
143        }
144        if (Proxy.isProxyClass(connection.getClass())) {
145            if (MBeanServerInvocationHandler.class.isAssignableFrom(
146                    Proxy.getInvocationHandler(connection).getClass())) {
147                throw new IllegalArgumentException("Wrapping MBeanServerInvocationHandler");
148            }
149        }
150        if (objectName == null) {
151            throw new IllegalArgumentException("Null object name");
152        }
153        this.connection = connection;
154        this.objectName = objectName;
155        this.isMXBean = isMXBean;
156    }
157
158    /**
159     * <p>The MBean server connection through which the methods of
160     * a proxy using this handler are forwarded.</p>
161     *
162     * @return the MBean server connection.
163     *
164     * @since 1.6
165     */
166    public MBeanServerConnection getMBeanServerConnection() {
167        return connection;
168    }
169
170    /**
171     * <p>The name of the MBean within the MBean server to which methods
172     * are forwarded.
173     *
174     * @return the object name.
175     *
176     * @since 1.6
177     */
178    public ObjectName getObjectName() {
179        return objectName;
180    }
181
182    /**
183     * <p>If true, the proxy is for an MXBean, and appropriate mappings
184     * are applied to method parameters and return values.
185     *
186     * @return whether the proxy is for an MXBean.
187     *
188     * @since 1.6
189     */
190    public boolean isMXBean() {
191        return isMXBean;
192    }
193
194    /**
195     * <p>Return a proxy that implements the given interface by
196     * forwarding its methods through the given MBean server to the
197     * named MBean.  As of 1.6, the methods {@link
198     * JMX#newMBeanProxy(MBeanServerConnection, ObjectName, Class)} and
199     * {@link JMX#newMBeanProxy(MBeanServerConnection, ObjectName, Class,
200     * boolean)} are preferred to this method.</p>
201     *
202     * <p>This method is equivalent to {@link Proxy#newProxyInstance
203     * Proxy.newProxyInstance}<code>(interfaceClass.getClassLoader(),
204     * interfaces, handler)</code>.  Here <code>handler</code> is the
205     * result of {@link #MBeanServerInvocationHandler new
206     * MBeanServerInvocationHandler(connection, objectName)}, and
207     * <code>interfaces</code> is an array that has one element if
208     * <code>notificationBroadcaster</code> is false and two if it is
209     * true.  The first element of <code>interfaces</code> is
210     * <code>interfaceClass</code> and the second, if present, is
211     * <code>NotificationEmitter.class</code>.
212     *
213     * @param connection the MBean server to forward to.
214     * @param objectName the name of the MBean within
215     * <code>connection</code> to forward to.
216     * @param interfaceClass the management interface that the MBean
217     * exports, which will also be implemented by the returned proxy.
218     * @param notificationBroadcaster make the returned proxy
219     * implement {@link NotificationEmitter} by forwarding its methods
220     * via <code>connection</code>. A call to {@link
221     * NotificationBroadcaster#addNotificationListener} on the proxy will
222     * result in a call to {@link
223     * MBeanServerConnection#addNotificationListener(ObjectName,
224     * NotificationListener, NotificationFilter, Object)}, and likewise
225     * for the other methods of {@link NotificationBroadcaster} and {@link
226     * NotificationEmitter}.
227     *
228     * @param <T> allows the compiler to know that if the {@code
229     * interfaceClass} parameter is {@code MyMBean.class}, for example,
230     * then the return type is {@code MyMBean}.
231     *
232     * @return the new proxy instance.
233     *
234     * @see JMX#newMBeanProxy(MBeanServerConnection, ObjectName, Class, boolean)
235     */
236    public static <T> T newProxyInstance(MBeanServerConnection connection,
237                                         ObjectName objectName,
238                                         Class<T> interfaceClass,
239                                         boolean notificationBroadcaster) {
240        return JMX.newMBeanProxy(connection, objectName, interfaceClass, notificationBroadcaster);
241    }
242
243    public Object invoke(Object proxy, Method method, Object[] args)
244            throws Throwable {
245        final Class<?> methodClass = method.getDeclaringClass();
246
247        if (methodClass.equals(NotificationBroadcaster.class)
248            || methodClass.equals(NotificationEmitter.class))
249            return invokeBroadcasterMethod(proxy, method, args);
250
251        // local or not: equals, toString, hashCode
252        if (shouldDoLocally(proxy, method))
253            return doLocally(proxy, method, args);
254
255        try {
256            if (isMXBean()) {
257                MXBeanProxy p = findMXBeanProxy(methodClass);
258                return p.invoke(connection, objectName, method, args);
259            } else {
260                final String methodName = method.getName();
261                final Class<?>[] paramTypes = method.getParameterTypes();
262                final Class<?> returnType = method.getReturnType();
263
264                /* Inexplicably, InvocationHandler specifies that args is null
265                   when the method takes no arguments rather than a
266                   zero-length array.  */
267                final int nargs = (args == null) ? 0 : args.length;
268
269                if (methodName.startsWith("get")
270                    && methodName.length() > 3
271                    && nargs == 0
272                    && !returnType.equals(Void.TYPE)) {
273                    return connection.getAttribute(objectName,
274                        methodName.substring(3));
275                }
276
277                if (methodName.startsWith("is")
278                    && methodName.length() > 2
279                    && nargs == 0
280                    && (returnType.equals(Boolean.TYPE)
281                    || returnType.equals(Boolean.class))) {
282                    return connection.getAttribute(objectName,
283                        methodName.substring(2));
284                }
285
286                if (methodName.startsWith("set")
287                    && methodName.length() > 3
288                    && nargs == 1
289                    && returnType.equals(Void.TYPE)) {
290                    Attribute attr = new Attribute(methodName.substring(3), args[0]);
291                    connection.setAttribute(objectName, attr);
292                    return null;
293                }
294
295                final String[] signature = new String[paramTypes.length];
296                for (int i = 0; i < paramTypes.length; i++)
297                    signature[i] = paramTypes[i].getName();
298                return connection.invoke(objectName, methodName,
299                                         args, signature);
300            }
301        } catch (MBeanException e) {
302            throw e.getTargetException();
303        } catch (RuntimeMBeanException re) {
304            throw re.getTargetException();
305        } catch (RuntimeErrorException rre) {
306            throw rre.getTargetError();
307        }
308        /* The invoke may fail because it can't get to the MBean, with
309           one of the these exceptions declared by
310           MBeanServerConnection.invoke:
311           - RemoteException: can't talk to MBeanServer;
312           - InstanceNotFoundException: objectName is not registered;
313           - ReflectionException: objectName is registered but does not
314             have the method being invoked.
315           In all of these cases, the exception will be wrapped by the
316           proxy mechanism in an UndeclaredThrowableException unless
317           it happens to be declared in the "throws" clause of the
318           method being invoked on the proxy.
319         */
320    }
321
322    private static MXBeanProxy findMXBeanProxy(Class<?> mxbeanInterface) {
323        synchronized (mxbeanProxies) {
324            WeakReference<MXBeanProxy> proxyRef =
325                    mxbeanProxies.get(mxbeanInterface);
326            MXBeanProxy p = (proxyRef == null) ? null : proxyRef.get();
327            if (p == null) {
328                try {
329                    p = new MXBeanProxy(mxbeanInterface);
330                } catch (IllegalArgumentException e) {
331                    String msg = "Cannot make MXBean proxy for " +
332                            mxbeanInterface.getName() + ": " + e.getMessage();
333                    IllegalArgumentException iae =
334                            new IllegalArgumentException(msg, e.getCause());
335                    iae.setStackTrace(e.getStackTrace());
336                    throw iae;
337                }
338                mxbeanProxies.put(mxbeanInterface,
339                                  new WeakReference<MXBeanProxy>(p));
340            }
341            return p;
342        }
343    }
344    private static final WeakHashMap<Class<?>, WeakReference<MXBeanProxy>>
345            mxbeanProxies = new WeakHashMap<Class<?>, WeakReference<MXBeanProxy>>();
346
347    private Object invokeBroadcasterMethod(Object proxy, Method method,
348                                           Object[] args) throws Exception {
349        final String methodName = method.getName();
350        final int nargs = (args == null) ? 0 : args.length;
351
352        if (methodName.equals("addNotificationListener")) {
353            /* The various throws of IllegalArgumentException here
354               should not happen, since we know what the methods in
355               NotificationBroadcaster and NotificationEmitter
356               are.  */
357            if (nargs != 3) {
358                final String msg =
359                    "Bad arg count to addNotificationListener: " + nargs;
360                throw new IllegalArgumentException(msg);
361            }
362            /* Other inconsistencies will produce ClassCastException
363               below.  */
364
365            NotificationListener listener = (NotificationListener) args[0];
366            NotificationFilter filter = (NotificationFilter) args[1];
367            Object handback = args[2];
368            connection.addNotificationListener(objectName,
369                                               listener,
370                                               filter,
371                                               handback);
372            return null;
373
374        } else if (methodName.equals("removeNotificationListener")) {
375
376            /* NullPointerException if method with no args, but that
377               shouldn't happen because removeNL does have args.  */
378            NotificationListener listener = (NotificationListener) args[0];
379
380            switch (nargs) {
381            case 1:
382                connection.removeNotificationListener(objectName, listener);
383                return null;
384
385            case 3:
386                NotificationFilter filter = (NotificationFilter) args[1];
387                Object handback = args[2];
388                connection.removeNotificationListener(objectName,
389                                                      listener,
390                                                      filter,
391                                                      handback);
392                return null;
393
394            default:
395                final String msg =
396                    "Bad arg count to removeNotificationListener: " + nargs;
397                throw new IllegalArgumentException(msg);
398            }
399
400        } else if (methodName.equals("getNotificationInfo")) {
401
402            if (args != null) {
403                throw new IllegalArgumentException("getNotificationInfo has " +
404                                                   "args");
405            }
406
407            MBeanInfo info = connection.getMBeanInfo(objectName);
408            return info.getNotifications();
409
410        } else {
411            throw new IllegalArgumentException("Bad method name: " +
412                                               methodName);
413        }
414    }
415
416    private boolean shouldDoLocally(Object proxy, Method method) {
417        final String methodName = method.getName();
418        if ((methodName.equals("hashCode") || methodName.equals("toString"))
419            && method.getParameterTypes().length == 0
420            && isLocal(proxy, method))
421            return true;
422        if (methodName.equals("equals")
423            && Arrays.equals(method.getParameterTypes(),
424                             new Class<?>[] {Object.class})
425            && isLocal(proxy, method))
426            return true;
427        if (methodName.equals("finalize")
428            && method.getParameterTypes().length == 0) {
429            return true;
430        }
431        return false;
432    }
433
434    private Object doLocally(Object proxy, Method method, Object[] args) {
435        final String methodName = method.getName();
436
437        if (methodName.equals("equals")) {
438
439            if (this == args[0]) {
440                return true;
441            }
442
443            if (!(args[0] instanceof Proxy)) {
444                return false;
445            }
446
447            final InvocationHandler ihandler =
448                Proxy.getInvocationHandler(args[0]);
449
450            if (ihandler == null ||
451                !(ihandler instanceof MBeanServerInvocationHandler)) {
452                return false;
453            }
454
455            final MBeanServerInvocationHandler handler =
456                (MBeanServerInvocationHandler)ihandler;
457
458            return connection.equals(handler.connection) &&
459                objectName.equals(handler.objectName) &&
460                proxy.getClass().equals(args[0].getClass());
461        } else if (methodName.equals("toString")) {
462            return (isMXBean() ? "MX" : "M") + "BeanProxy(" +
463                connection + "[" + objectName + "])";
464        } else if (methodName.equals("hashCode")) {
465            return objectName.hashCode()+connection.hashCode();
466        } else if (methodName.equals("finalize")) {
467            // ignore the finalizer invocation via proxy
468            return null;
469        }
470
471        throw new RuntimeException("Unexpected method name: " + methodName);
472    }
473
474    private static boolean isLocal(Object proxy, Method method) {
475        final Class<?>[] interfaces = proxy.getClass().getInterfaces();
476        if(interfaces == null) {
477            return true;
478        }
479
480        final String methodName = method.getName();
481        final Class<?>[] params = method.getParameterTypes();
482        for (Class<?> intf : interfaces) {
483            try {
484                intf.getMethod(methodName, params);
485                return false; // found method in one of our interfaces
486            } catch (NoSuchMethodException nsme) {
487                // OK.
488            }
489        }
490
491        return true;  // did not find in any interface
492    }
493
494    private final MBeanServerConnection connection;
495    private final ObjectName objectName;
496    private final boolean isMXBean;
497}
498