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 
26 package javax.management;
27 
28 import com.sun.jmx.mbeanserver.MXBeanProxy;
29 
30 import java.lang.ref.WeakReference;
31 import java.lang.reflect.InvocationHandler;
32 import java.lang.reflect.Method;
33 import java.lang.reflect.Proxy;
34 import java.util.Arrays;
35 import 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  */
89 public 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      */
MBeanServerInvocationHandler(MBeanServerConnection connection, ObjectName objectName)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      */
MBeanServerInvocationHandler(MBeanServerConnection connection, ObjectName objectName, boolean isMXBean)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      */
getMBeanServerConnection()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      */
getObjectName()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      */
isMXBean()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      */
newProxyInstance(MBeanServerConnection connection, ObjectName objectName, Class<T> interfaceClass, boolean notificationBroadcaster)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 
invoke(Object proxy, Method method, Object[] args)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 
findMXBeanProxy(Class<?> mxbeanInterface)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 
invokeBroadcasterMethod(Object proxy, Method method, Object[] args)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 
shouldDoLocally(Object proxy, Method method)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 
doLocally(Object proxy, Method method, Object[] args)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 
isLocal(Object proxy, Method method)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