/* * Copyright 2007-2008 Sun Microsystems, Inc. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Sun designates this * particular file as subject to the "Classpath" exception as provided * by Sun in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, * CA 95054 USA or visit www.sun.com if you need additional information or * have any questions. */ package javax.management; import com.sun.jmx.interceptor.SingleMBeanForwarder; import com.sun.jmx.namespace.RoutingConnectionProxy; import com.sun.jmx.namespace.RoutingProxy; import com.sun.jmx.namespace.RoutingServerProxy; import java.io.UnsupportedEncodingException; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.URLDecoder; import java.net.URLEncoder; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.StringTokenizer; import java.util.TreeMap; import java.util.concurrent.Callable; import java.util.logging.Level; import java.util.logging.Logger; import static javax.management.namespace.JMXNamespaces.NAMESPACE_SEPARATOR; import javax.management.namespace.JMXNamespaces; import javax.management.namespace.JMXNamespace; import javax.management.namespace.JMXNamespaceMBean; import javax.management.namespace.MBeanServerSupport; import javax.management.remote.IdentityMBeanServerForwarder; import javax.management.remote.MBeanServerForwarder; /** *

Methods to communicate a client context to MBeans. A context is * a {@literal Map} that is provided by the client and * that an MBean can consult using the {@link #getContext()} method. * The context is set on a per-thread basis and can be consulted by any * code that the target MBean calls within the thread.

* *

One common usage of client context is to communicate the client's * {@link Locale} to MBeans. For example, if an MBean has a String attribute * {@code LastProblemDescription}, the value of that attribute could be * a description of the last problem encountered by the MBean, translated * into the client's locale. Different clients accessing this attribute * from different locales would each see the appropriate version for their * locale.

* *

The locale case is sufficiently important that it has a special * shorthand, the {@link #getLocale()} method. This method calls * {@link #getContext()}.get({@link #LOCALE_KEY}) and converts the * resultant String into a Locale object.

* *

Here is what an MBean with a localized {@code LastProblemDescription} * attribute might look like:

* *
 * public class LocaleSensitive implements LocaleSensitiveMBean {
 *     ...
 *     public String getLastProblemDescription() {
 *         Locale loc = {@link #getLocale() ClientContext.getLocale()};
 *         ResourceBundle rb = ResourceBundle.getBundle("MyResources", loc);
 *         String resourceKey = getLastProblemResourceKey();
 *         return rb.getString(resourceKey);
 *     }
 *     ...
 * }
 * 
* *

Here is how a client can communicate its locale to the target * MBean:

* *
 * JMXConnector connector = JMXConnectorFactory.connect(url);
 * MBeanServerConnection connection = connector.getMBeanServerConnection();
 * MBeanServerConnection localizedConnection =
 *     {@link #withLocale(MBeanServerConnection, Locale)
 *      ClientContext.withLocale}(connection, Locale.getDefault());
 * String problem = localizedConnection.getAttribute(
 *          objectName, "LastProblemDescription");
 * 
* *

In the more general case where the client wants to communicate context * other than the locale, it can use {@link #withContext(MBeanServerConnection, * String, String) withContext} instead of {@code withLocale}, and the target * MBean can retrieve the context using {@link #getContext()}.

* * *

Remote use of contexts

* *

The various {@code with*} methods, for example {@link * #withLocale(javax.management.MBeanServer, java.util.Locale) withLocale}, * transmit the context of each request by encoding it in the ObjectName of * the request. For example, if a client creates a connection in the * French locale like this...

* *
 * MBeanServerConnection mbsc = ...;
 * Locale french = new Locale("fr");
 * MBeanServerConnection localizedConnection = ClientContext.withLocale(mbsc, french);
 * 
* *

...or, equivalently, like this...

* *
 * MBeanServerConnection localizedConnection =
 *     ClientContext.withContext(mbsc, {@link #LOCALE_KEY "jmx.locale"}, "fr");
 * 
* *

...then the context associates {@code "jmx.locale"} with {@code "fr"} * and a request such as
* {@code localizedConnection.getAttribute("java.lang:type=Runtime", "Name")}
* is translated into
* {@code mbsc.getAttribute("jmx.context//jmx.locale=fr//java.lang:Runtime", "Name")}.
* A special {@linkplain javax.management.namespace namespace} {@code jmx.context//} * extracts the context from the string {@code jmx.locale=fr} and establishes * it in the thread that will do
* {@code getAttribute("java.lang:Runtime", "Name")}.

* *

The details of how contexts are encoded into ObjectNames are explained * in the {@link #encode encode} method.

* *

The namespace {@code jmx.context//} just mentioned is only needed by * remote clients, since local clients can set the context directly using * {@link #doWithContext doWithContext}. Accordingly, this namespace is not * present by default in the {@code MBeanServer}. Instead, it is * simulated by the standard RMI connector using a special * {@link MBeanServerForwarder}. If you are using this connector, you do not * need to do anything special. Other connectors may or may not simulate this * namespace in the same way. If the connector server returns true from the * method {@link * javax.management.remote.JMXConnectorServer#supportsSystemMBeanServerForwarder() * supportsSystemMBeanServerForwarder} then it does simulate the namespace. * If you are using another connector, or if you want to be able to use the * {@code with*} methods locally, then you can install the {@code * MBeanServerForwarder} yourself as described in the method {@link * #newContextForwarder newContextForwarder}.

*/ public class ClientContext { /** *

The context key for the client locale. The string associated with * this key is an encoded locale such as {@code en_US} which could be * returned by {@link Locale#toString()}.

*/ public static final String LOCALE_KEY = "jmx.locale"; private static final Logger LOG = Logger.getLogger("javax.management.context"); /** *

The namespace that implements contexts, {@value}.

*/ public static final String NAMESPACE = "jmx.context"; private static final String NAMESPACE_PLUS_SEP = NAMESPACE + NAMESPACE_SEPARATOR; static final ObjectName CLIENT_CONTEXT_NAMESPACE_HANDLER = ObjectName.valueOf(NAMESPACE_PLUS_SEP + ":" + JMXNamespace.TYPE_ASSIGNMENT); private static final ObjectName NAMESPACE_HANDLER_WITHOUT_NAMESPACE = ObjectName.valueOf(":" + JMXNamespace.TYPE_ASSIGNMENT); private static final ThreadLocal> contextThreadLocal = new InheritableThreadLocal>() { @Override protected Map initialValue() { return Collections.emptyMap(); } }; /** There are no instances of this class. */ private ClientContext() { } /** *

Get the client context associated with the current thread. * * @return the client context associated with the current thread. * This may be an empty Map, but it cannot be null. The returned * Map cannot be modified. */ public static Map getContext() { return Collections.unmodifiableMap(contextThreadLocal.get()); } /** *

Get the client locale associated with the current thread. * If the client context includes the {@value #LOCALE_KEY} key * then the returned value is the Locale encoded in that key. * Otherwise the returned value is the {@linkplain Locale#getDefault() * default locale}. * * @return the client locale. */ public static Locale getLocale() { String localeS = getContext().get(LOCALE_KEY); if (localeS == null) return Locale.getDefault(); // Parse the locale string. Why isn't there a method in Locale for this? String language, country, variant; int ui = localeS.indexOf('_'); if (ui < 0) { language = localeS; country = variant = ""; } else { language = localeS.substring(0, ui); localeS = localeS.substring(ui + 1); ui = localeS.indexOf('_'); if (ui < 0) { country = localeS; variant = ""; } else { country = localeS.substring(0, ui); variant = localeS.substring(ui + 1); } } return new Locale(language, country, variant); } /** *

Execute the given {@code task} with the client context set to * the given Map. This Map will be the result of {@link #getContext()} * within the {@code task}.

* *

The {@code task} may include nested calls to {@code doWithContext}. * The value returned by {@link #getContext} at any point is the Map * provided to the most recent {@code doWithContext} (in the current thread) * that has not yet returned.

* *

The {@link #getContext()} method returns the same value immediately * after a call to this method as immediately before. In other words, * {@code doWithContext} only affects the context during the execution of * the {@code task}.

* *

As an example, suppose you want to get an attribute with whatever * context has already been set, plus the locale set to "fr". You could * write this:

* *
     * {@code Map} context =
     *     new {@code HashMap}(ClientContext.getContext());
     * context.put(ClientContext.LOCALE_KEY, "fr");
     * String lastProblemDescription =
     *     ClientContext.doWithContext(context, new {@code Callable}() {
     *         public String call() {
     *             return (String) mbeanServer.getAttribute(mbean, "LastProblemDescription");
     *         }
     *     });
     * 
* * @param the type of value that the task will return. This type * parameter is usually inferred from the type of the {@code task} * parameter. For example, if {@code task} is a {@code Callable} * then {@code T} is {@code String}. If the task does not return a value, * use a {@code Callable} and return null from its * {@link Callable#call call} method. * @param context the context to use while executing {@code task}. * @param task the task to run with the {@code key}={@code value} * binding. * @return the result of {@link Callable#call() task.call()}. * @throws IllegalArgumentException if either parameter is null, or * if any key in {@code context} is null or empty, or if any value * in {@code context} is null. * @throws Exception If {@link Callable#call() task.call()} throws an * exception, {@code doWithContext} throws the same exception. */ public static T doWithContext(Map context, Callable task) throws Exception { if (context == null || task == null) throw new IllegalArgumentException("Null parameter"); Map contextCopy = new TreeMap(context); validateContext(contextCopy); Map oldContextMap = contextThreadLocal.get(); try { contextThreadLocal.set(contextCopy); return task.call(); } finally { contextThreadLocal.set(oldContextMap); } } private static void validateContext(Map context) { for (Map.Entry entry : context.entrySet()) { // If the user passes a raw Map rather than a Map, // entries could contain objects other than Strings. If so, // we'll get a ClassCastException here. String key = entry.getKey(); String value = entry.getValue(); if (key == null || value == null) throw new IllegalArgumentException("Null key or value in context"); if (key.equals("")) throw new IllegalArgumentException("Empty key in context"); } } /** *

Return an MBeanServer object that is equivalent to the given * MBeanServer object except that operations on MBeans run with * the given Locale in their {@linkplain #getContext() thread context}. * Note that this will only work if the given MBeanServer supports * contexts, as described above.

* *

This method is equivalent to {@link #withContext(MBeanServer, * String, String) withContext}(mbs, {@value LOCALE_KEY}, * locale.toString()).

* * @throws IllegalArgumentException if either parameter is null, or if * {@code mbs} does not support contexts. In the second case only, * the cause of the {@code IllegalArgumentException} will be an {@link * InstanceNotFoundException}. */ public static MBeanServer withLocale(MBeanServer mbs, Locale locale) { return withLocale(mbs, MBeanServer.class, locale); } /** *

Return an MBeanServerConnection object that is equivalent to the given * MBeanServerConnection object except that operations on MBeans run with * the given Locale in their {@linkplain #getContext() thread context}. * Note that this will only work if the given MBeanServerConnection supports * contexts, as described above.

* *

This method is equivalent to {@link #withContext(MBeanServerConnection, * String, String) withContext}(mbs, {@value LOCALE_KEY}, * locale.toString()).

* * @throws IllegalArgumentException if either parameter is null, or if * the communication with {@code mbsc} fails, or if {@code mbsc} does not * support contexts. If the communication with {@code mbsc} fails, the * {@linkplain Throwable#getCause() cause} of this exception will be an * {@code IOException}. If {@code mbsc} does not support contexts, the * cause will be an {@link InstanceNotFoundException}. */ public static MBeanServerConnection withLocale( MBeanServerConnection mbsc, Locale locale) { return withLocale(mbsc, MBeanServerConnection.class, locale); } private static T withLocale( T mbsc, Class mbscClass, Locale locale) { if (locale == null) throw new IllegalArgumentException("Null locale"); return withContext(mbsc, mbscClass, LOCALE_KEY, locale.toString()); } /** *

Return an MBeanServer object that is equivalent to the given * MBeanServer object except that operations on MBeans run with * the given key bound to the given value in their {@linkplain * #getContext() thread context}. * Note that this will only work if the given MBeanServer supports * contexts, as described above.

* * @param mbs the original MBeanServer. * @param key the key to bind in the context of MBean operations * in the returned MBeanServer object. * @param value the value to bind to the key in the context of MBean * operations in the returned MBeanServer object. * @throws IllegalArgumentException if any parameter is null, or * if {@code key} is the empty string, or if {@code mbs} does not support * contexts. In the last case only, the cause of the {@code * IllegalArgumentException} will be an {@link InstanceNotFoundException}. */ public static MBeanServer withContext( MBeanServer mbs, String key, String value) { return withContext(mbs, MBeanServer.class, key, value); } /** *

Return an MBeanServerConnection object that is equivalent to the given * MBeanServerConnection object except that operations on MBeans run with * the given key bound to the given value in their {@linkplain * #getContext() thread context}. * Note that this will only work if the given MBeanServerConnection supports * contexts, as described above.

* * @param mbsc the original MBeanServerConnection. * @param key the key to bind in the context of MBean operations * in the returned MBeanServerConnection object. * @param value the value to bind to the key in the context of MBean * operations in the returned MBeanServerConnection object. * @throws IllegalArgumentException if any parameter is null, or * if {@code key} is the empty string, or if the communication with {@code * mbsc} fails, or if {@code mbsc} does not support contexts. If * the communication with {@code mbsc} fails, the {@linkplain * Throwable#getCause() cause} of this exception will be an {@code * IOException}. If {@code mbsc} does not support contexts, the cause will * be an {@link InstanceNotFoundException}. */ public static MBeanServerConnection withContext( MBeanServerConnection mbsc, String key, String value) { return withContext(mbsc, MBeanServerConnection.class, key, value); } /** *

Returns an MBeanServerConnection object that is equivalent to the * given MBeanServerConnection object except that remote operations on * MBeans run with the context that has been established by the client * using {@link #doWithContext doWithContext}. Note that this will * only work if the remote system supports contexts, as described above.

* *

For example, suppose the remote system does support contexts, and you * have created a {@code JMXConnector} like this:

* *
     * JMXServiceURL url = ...;
     * JMXConnector client = JMXConnectorFactory.connect(url);
     * MBeanServerConnection mbsc = client.getMBeanServerConnection();
     * mbsc = ClientContext.withDynamicContext(mbsc);
     * 
* *

Then if you do this...

* *
     * MBeanInfo mbi = ClientContext.doWithContext(
     *     Collections.singletonMap(ClientContext.LOCALE_KEY, "fr"),
     *     new {@code Callable}() {
     *         public MBeanInfo call() {
     *             return mbsc.getMBeanInfo(objectName);
     *         }
     *     });
     * 
* *

...then the context with the locale set to "fr" will be in place * when the {@code getMBeanInfo} is executed on the remote MBean Server.

* * @param mbsc the original MBeanServerConnection. * * @throws IllegalArgumentException if the {@code mbsc} parameter is null, * or if the communication with {@code mbsc} fails, or if {@code mbsc} * does not support contexts. If the communication with {@code mbsc} * fails, the {@linkplain Throwable#getCause() cause} of this exception * will be an {@code IOException}. If {@code mbsc} does not support * contexts, the cause will be an {@link InstanceNotFoundException}. */ public static MBeanServerConnection withDynamicContext( MBeanServerConnection mbsc) { // Probe mbsc to get the right exception if it's incommunicado or // doesn't support namespaces. JMXNamespaces.narrowToNamespace(mbsc, NAMESPACE); return (MBeanServerConnection) Proxy.newProxyInstance( MBeanServerConnection.class.getClassLoader(), new Class[] {MBeanServerConnection.class}, new DynamicContextIH(mbsc)); } private static class DynamicContextIH implements InvocationHandler { private final MBeanServerConnection mbsc; public DynamicContextIH(MBeanServerConnection mbsc) { this.mbsc = mbsc; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { MBeanServerConnection dynMBSC = withContext( mbsc, MBeanServerConnection.class, getContext(), false); try { return method.invoke(dynMBSC, args); } catch (InvocationTargetException e) { throw e.getCause(); } } } private static T withContext( T mbsc, Class mbscClass, String key, String value) { return withContext( mbsc, mbscClass, Collections.singletonMap(key, value), true); } private static T withContext( T mbsc, Class mbscClass, Map context, boolean probe) { if (mbsc == null || context == null) throw new IllegalArgumentException("Null parameter"); if (context.isEmpty()) return mbsc; validateContext(context); Map contextMap = null; if (mbsc.getClass() == RoutingServerProxy.class || mbsc.getClass() == RoutingProxy.class) { RoutingProxy nsp = (RoutingProxy) mbsc; String where = nsp.getSourceNamespace(); if (where.startsWith(NAMESPACE_PLUS_SEP)) { /* Try to merge the existing context namespace with the * new one. If it doesn't work, we fall back to just * prefixing jmx.context//key=value, which * might lead to a name like jmx.c//k1=v1//jmx.c//k2=v2//d:k=v. */ String encodedContext = where.substring(NAMESPACE_PLUS_SEP.length()); if (encodedContext.indexOf(NAMESPACE_SEPARATOR) < 0) { contextMap = stringToMapOrNull(encodedContext); if (contextMap != null) { contextMap.putAll(context); mbsc = mbscClass.cast(nsp.source()); } } } } if (contextMap == null) contextMap = context; String contextDir = NAMESPACE_PLUS_SEP + mapToString(contextMap); if (mbscClass == MBeanServer.class) { return mbscClass.cast(RoutingServerProxy.cd( (MBeanServer) mbsc, contextDir, probe)); } else if (mbscClass == MBeanServerConnection.class) { return mbscClass.cast(RoutingConnectionProxy.cd( mbsc, contextDir, probe)); } else throw new AssertionError("Bad MBSC: " + mbscClass); } /** *

Returns an encoded context prefix for ObjectNames. * If the given context is empty, {@code ""} is returned. * Otherwise, this method returns a string of the form * {@code "jmx.context//key=value;key=value;..."}. * For example, if the context has keys {@code "jmx.locale"} * and {@code "xid"} with respective values {@code "fr"} * and {@code "1234"}, this method will return * {@code "jmx.context//jmx.locale=fr;xid=1234"} or * {@code "jmx.context//xid=1234;jmx.locale=fr"}.

* *

Each key and each value in the encoded string is subject to * encoding as if by the method {@link URLEncoder#encode(String, String)} * with a character encoding of {@code "UTF-8"}, but with the additional * encoding of any {@code *} character as {@code "%2A"}. This ensures * that keys and values can contain any character. Without encoding, * characters such as {@code =} and {@code :} would pose problems.

* * @param context the context to encode. * * @return the context in encoded form. * * @throws IllegalArgumentException if the {@code context} parameter * is null or if it contains a null key or value. **/ public static String encode(Map context) { if (context == null) throw new IllegalArgumentException("Null context"); if (context.isEmpty()) return ""; StringBuilder sb = new StringBuilder(); for (Map.Entry entry : context.entrySet()) { String key = entry.getKey(); String value = entry.getValue(); if (key == null || value == null) throw new IllegalArgumentException("Null key or value"); if (sb.length() > 0) sb.append(";"); sb.append(encode(key)).append("=").append(encode(value)); } sb.insert(0, NAMESPACE_PLUS_SEP); return sb.toString(); } /** *

Create a new {@link MBeanServerForwarder} that applies the context * received from a client to the current thread. A client using * one of the various {@code with*} methods (for example {@link * #withContext(MBeanServerConnection, String, String) withContext}) will * encode that context into the {@code ObjectName} of each * {@code MBeanServer} request. The object returned by this method * decodes the context from that {@code ObjectName} and applies it * as described for {@link #doWithContext doWithContext} while performing * the {@code MBeanServer} request using the {@code ObjectName} without * the encoded context.

* *

This forwarder can be used in a number of ways:

* *
    *
  • *

    To add context decoding to a local {@code MBeanServer}, you can * write:

    *
         * MBeanServer mbs = {@link
         * java.lang.management.ManagementFactory#getPlatformMBeanServer()
         * ManagementFactory.getPlatformMBeanServer()};  // for example
         * mbs = ClientContext.newContextForwarder(mbs, null);
         * 
    * *
  • *

    To add context decoding to a {@linkplain * javax.management.remote.JMXConnectorServer connector server}:

    *
         * JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(...);
         * MBeanServer nextMBS = cs.getMBeanServer();
         * MBeanServerForwarder mbsf = ClientContext.newContextForwarder(nextMBS, null);
         * cs.{@link
         * javax.management.remote.JMXConnectorServer#setMBeanServerForwarder
         * setMBeanServerForwarder}(mbsf);
         * 
    * *
  • *

    For connectors, such as the standard RMI connector, that support * a {@linkplain * javax.management.remote.JMXConnectorServer#getSystemMBeanServerForwarder * system chain} of {@code MBeanServerForwarder}s, this forwarder will * be installed in that chain by default. See * {@link javax.management.remote.JMXConnectorServer#CONTEXT_FORWARDER * JMXConnectorServer.CONTEXT_FORWARDER}. *

    * *
* * @param nextMBS the next {@code MBeanServer} in the chain of * forwarders, which might be another {@code MBeanServerForwarder} or * a plain {@code MBeanServer}. This is the object to which {@code * MBeanServer} requests that do not include a context are sent. It * will be the value of {@link MBeanServerForwarder#getMBeanServer() * getMBeanServer()} on the returned object, and can be changed with {@link * MBeanServerForwarder#setMBeanServer setMBeanServer}. It can be null but * must be set to a non-null value before any {@code MBeanServer} requests * arrive. * * @param loopMBS the {@code MBeanServer} to which requests that contain * an encoded context should be sent once the context has been decoded. * For example, if the request is {@link MBeanServer#getAttribute * getAttribute}{@code ("jmx.context//jmx.locale=fr//java.lang:type=Runtime", * "Name")}, then the {@linkplain #getContext() context} of the thread * executing that request will have {@code "jmx.locale"} set to {@code "fr"} * while executing {@code loopMBS.getAttribute("java.lang:type=Runtime", * "Name")}. If this parameter is null, then these requests will be * sent to the newly-created {@code MBeanServerForwarder}. Usually * the parameter will either be null or will be the result of {@link * javax.management.remote.JMXConnectorServer#getSystemMBeanServerForwarder * getSystemMBeanServerForwarder()} for the connector server in which * this forwarder will be installed. * * @return a new {@code MBeanServerForwarder} that decodes client context * from {@code ObjectName}s. */ /* * What we're building here is confusing enough to need a diagram. * The MBSF that we return is actually the composition of two forwarders: * the first one simulates the existence of the MBean * jmx.context//:type=JMXNamespace, and the second one simulates the * existence of the namespace jmx.context//. Furthermore, that namespace * loops back to the composed forwarder, so that something like * jmx.context//foo=bar//jmxcontext//baz=buh will work. And the loopback * goes through yet another forwarder, which simulates the existence of * (e.g.) jmx.context//foo=bar//:type=JMXNamespace, which is needed * notably so that narrowToNamespace will work. * * | +--------------------------------------------------+ * v v | * +----------------+ | * | Handler MBSF |->accesses to jmx.context//:type=JMXNamespace | * +----------------+ (handled completely here) +-------------------+ * | | 2nd Handler MBSF | * v +-------------------+ * +----------------+ ^ * | Namespace MBSF |->accesses to jmx.context//**-------------------+ * +----------------+ (after attaching context to thread) * | * v accesses to anything else * * And finally, we need to ensure that from the outside the composed object * looks like a single forwarder, so that its get/setMBeanServer methods * will do the expected thing. That's what the anonymous subclass is for. */ public static MBeanServerForwarder newContextForwarder( MBeanServer nextMBS, MBeanServer loopMBS) { final MBeanServerForwarder mbsWrapper = new IdentityMBeanServerForwarder(nextMBS); DynamicMBean handlerMBean = new StandardMBean( new JMXNamespace(mbsWrapper), JMXNamespaceMBean.class, false); SingleMBeanForwarder handlerForwarder = new SingleMBeanForwarder( CLIENT_CONTEXT_NAMESPACE_HANDLER, handlerMBean, true) { @Override public MBeanServer getMBeanServer() { return ((MBeanServerForwarder) super.getMBeanServer()).getMBeanServer(); } @Override public void setMBeanServer(MBeanServer mbs1) { MBeanServerForwarder mbsf1 = (MBeanServerForwarder) super.getMBeanServer(); if (mbsf1 != null) mbsf1.setMBeanServer(mbs1); else super.setMBeanServer(mbs1); mbsWrapper.setMBeanServer(mbs1); } }; if (loopMBS == null) loopMBS = handlerForwarder; ContextInvocationHandler contextIH = new ContextInvocationHandler(nextMBS, loopMBS); MBeanServerForwarder contextForwarder = newForwarderProxy(contextIH); handlerForwarder.setMBeanServer(contextForwarder); return handlerForwarder; } /** *

Create a new {@link MBeanServerForwarder} that localizes * descriptions in {@code MBeanInfo} instances returned by * {@link MBeanServer#getMBeanInfo getMBeanInfo}. The {@code * MBeanServerForwarder} returned by this method passes all {@code * MBeanServer} methods through unchanged to the supplied object, {@code * mbs}, with the exception of {@code getMBeanInfo}. To handle {@code * getMBeanInfo(objectName)}, it calls {@code mbs.getMBeanInfo(objectName)} * to get an {@code MBeanInfo}, {@code mbi}; it calls {@link * MBeanServer#getClassLoaderFor mbs.getClassLoaderFor(objectName)} to * get a {@code ClassLoader}, {@code cl}; and it calls {@link * #getLocale} to get a {@code Locale}, {@code locale}. The order * of these three calls is not specified. Then the result is {@code * mbi.localizeDescriptions(locale, loader)}.

* *

This forwarder can be used in a number of ways:

* *
    *
  • *

    To add description localization to a local {@code MBeanServer}, you * can write:

    * *
         * MBeanServer mbs = {@link
         * java.lang.management.ManagementFactory#getPlatformMBeanServer()
         * ManagementFactory.getPlatformMBeanServer()};  // for example
         * mbs = ClientContext.newLocalizeMBeanInfoForwarder(mbs);
         * 
    * *
  • *

    To add description localization to a {@linkplain * javax.management.remote.JMXConnectorServer connector server}, you will * need to add both a {@linkplain #newContextForwarder context forwarder} * and a localization forwarder, for example like this:

    * *
         * JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(...);
         * MBeanServer nextMBS = cs.getMBeanServer();
         * MBeanServerForwarder localizeMBSF =
         *     ClientContext.newLocalizeMBeanInfoForwarder(nextMBS);
         * MBeanServerForwarder contextMBSF =
         *     ClientContext.newContextForwarder(localizeMBSF, null);
         * cs.{@link
         * javax.management.remote.JMXConnectorServer#setMBeanServerForwarder
         * setMBeanServerForwarder}(contextMBSF);
         * 
    * *

    Notice that the context forwarder must run before the localization * forwarder, so that the locale is correctly established when the latter * runs. So the {@code nextMBS} parameter of the context forwarder must * be the localization forwarder, and not vice versa.

    * *
  • *

    For connectors, such as the standard RMI connector, that support * a {@linkplain * javax.management.remote.JMXConnectorServer#getSystemMBeanServerForwarder * system chain} of {@code MBeanServerForwarder}s, the context forwarder and * the localization forwarder will be installed in that chain, in the right * order, if you include * {@link * javax.management.remote.JMXConnectorServer#LOCALIZE_MBEAN_INFO_FORWARDER * LOCALIZE_MBEAN_INFO_FORWARDER} in the environment {@code Map} with * the value {@code "true"}, for example like this:

    *

    *
         * MBeanServer mbs = ...;
         * JMXServiceURL url = new JMXServiceURL("service:jmx:rmi://...");
         * {@code Map} env = new {@code HashMap}();
         * env.put(JMXConnectorServer.LOCALIZE_MBEAN_INFO_FORWARDER, "true");
         * JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(
         *     url, env, mbs);
         * 
    * *
* * @param mbs the next {@code MBeanServer} in the chain of * forwarders, which might be another {@code MBeanServerForwarder} * or a plain {@code MBeanServer}. It will be the value of * {@link MBeanServerForwarder#getMBeanServer() getMBeanServer()} * on the returned object, and can be changed with {@link * MBeanServerForwarder#setMBeanServer setMBeanServer}. It can be null but * must be set to a non-null value before any {@code MBeanServer} requests * arrive. * * @return a new {@code MBeanServerForwarder} that localizes descriptions * in the result of {@code getMBeanInfo}. */ public static MBeanServerForwarder newLocalizeMBeanInfoForwarder( MBeanServer mbs) { return new IdentityMBeanServerForwarder(mbs) { @Override public MBeanInfo getMBeanInfo(ObjectName name) throws InstanceNotFoundException, IntrospectionException, ReflectionException { MBeanInfo mbi = super.getMBeanInfo(name); Locale locale = getLocale(); ClassLoader loader = getClassLoaderFor(name); return mbi.localizeDescriptions(locale, loader); } }; } private static MBeanServerForwarder newForwarderProxy(InvocationHandler ih) { return (MBeanServerForwarder) Proxy.newProxyInstance( MBeanServerForwarder.class.getClassLoader(), new Class[] {MBeanServerForwarder.class}, ih); } // A proxy connection that will strip the 'contextDir' at input (routing), // and put it back at output (createMBean / registerMBean / query* / // getObjectInstance). Usually RoutingProxy / RoutingServerProxy are used // the other way round (they are used for 'cd' - where they need to add // something at input and remove it at output). // For 'cd' operations we create RoutingProxys with a non empty sourceDir, // and a possibly non-empty targetDir. This is the only case where we use // RoutingProxies with an empty sourceDir (sourceDir is what we add at input // and remove at output, targetDir is what we remove at input and add at // output. // // Note that using a transient ContextRoutingConnection // is possible only because RoutingProxys don't rewrite // notifications sources - otherwise we would have to // keep the ContextRoutingConnection - just to preserve // the 'wrapping listeners' // private static final class ContextRoutingConnection extends RoutingServerProxy { public ContextRoutingConnection(MBeanServer source, String contextDir) { super(source, "", contextDir, false); } // Not really needed - but this is safer and more optimized. // See RoutingProxy for more details. // @Override public Integer getMBeanCount() { return source().getMBeanCount(); } // Not really needed - but this is safer and more optimized. // See RoutingProxy for more details. // @Override public String[] getDomains() { return source().getDomains(); } // Not really needed - but this is safer and more optimized. // See RoutingProxy for more details. // @Override public String getDefaultDomain() { return source().getDefaultDomain(); } } private static class ContextInvocationHandler implements InvocationHandler { /* * MBeanServer requests that don't include jmx.context//foo=bar// * are forwarded to forwardMBS, which is the unadorned MBeanServer * that knows nothing about the context namespace. * MBeanServer requests that do include this prefix will * usually (depending on the value of the loopMBS parameter to * newContextForwarder) loop back to the combined MBeanServerForwarder * that first implements * jmx.context//:type=JMXNamespace and then implements * jmx.context//foo=bar//. The reason is that it is valid * to have jmx.context//foo=bar//jmx.context//baz=buh//, although * usually that will be combined into jmx.context//foo=bar;baz=buh//. * * Before forwarding to loopMBS, we must check for :type=JMXNamespace * so that jmx.context//foo=bar//:type=JMXNamespace will exist. Its * existence is partial because it must remain "invisible": it should * not show up in queryNames or getMBeanCount even though it does * accept getAttribute and isRegistered and all other methods that * reference a single MBean. */ private MBeanServer forwardMBS; private final MBeanServer loopMBS; private static final MBeanServer emptyMBS = new MBeanServerSupport() { @Override public DynamicMBean getDynamicMBeanFor(ObjectName name) throws InstanceNotFoundException { throw new InstanceNotFoundException(name.toString()); } @Override protected Set getNames() { return Collections.emptySet(); } }; ContextInvocationHandler(MBeanServer forwardMBS, MBeanServer loopMBS) { this.forwardMBS = forwardMBS; DynamicMBean handlerMBean = new StandardMBean( new JMXNamespace(loopMBS), JMXNamespaceMBean.class, false); MBeanServerForwarder handlerMBS = new SingleMBeanForwarder( NAMESPACE_HANDLER_WITHOUT_NAMESPACE, handlerMBean, false); handlerMBS.setMBeanServer(loopMBS); this.loopMBS = handlerMBS; } public Object invoke(Object proxy, final Method method, final Object[] args) throws Throwable { String methodName = method.getName(); Class[] paramTypes = method.getParameterTypes(); // If this is a method from MBeanServerForwarder, handle it here. // There are only two such methods: getMBeanServer() and // setMBeanServer(mbs). if (methodName.equals("getMBeanServer")) return forwardMBS; else if (methodName.equals("setMBeanServer")) { this.forwardMBS = (MBeanServer) args[0]; return null; } // It is a method from MBeanServer. // Find the first parameter whose declared type is ObjectName, // and see if it is in the context namespace. If so we need to // trigger the logic for that namespace. If not, we simply // forward to the next MBeanServer in the chain. This logic // depends on the fact that if a method in the MBeanServer interface // has a "routing" ObjectName parameter, it is always the first // parameter of that type. Conversely, if a method has an // ObjectName parameter, then it makes sense to "route" that // method. Except for deserialize and instantiate, but if we // recognize a context namespace in those methods' ObjectName // parameters it is pretty harmless. int objectNameI = -1; for (int i = 0; i < paramTypes.length; i++) { if (paramTypes[i] == ObjectName.class) { objectNameI = i; break; } } if (objectNameI < 0) return invoke(method, forwardMBS, args); ObjectName target = (ObjectName) args[objectNameI]; if (target == null || !target.getDomain().startsWith(NAMESPACE_PLUS_SEP)) return invoke(method, forwardMBS, args); String domain = target.getDomain().substring(NAMESPACE_PLUS_SEP.length()); // The method routes through the (simulated) context namespace. // Decode the context after it, e.g. jmx.context//jmx.locale=fr//... // If there is no context part, we can throw an exception, // because a forwarder has already handled the unique MBean // jmx.context//:type=JMXNamespace. int sep = domain.indexOf(NAMESPACE_SEPARATOR); if (sep < 0) return invoke(method, emptyMBS, args); // throw exception final String encodedContext = domain.substring(0, sep); if (method.getName().startsWith("query") && (encodedContext.contains("*") || encodedContext.contains("?"))) { // Queries like jmx.context//*//d:k=v return // an empty set, consistent with "real" namespaces. return Collections.EMPTY_SET; } Map ctx = new TreeMap(getContext()); ctx.putAll(stringToMap(encodedContext)); return doWithContext(ctx, new Callable() { public Object call() throws Exception { // Create a proxy connection that will strip // "jmx.context//" + encodedContext + "//" on input, // and put it back on output. // // Note that using a transient ContextRoutingConnection // is possible only because it doesn't rewrite // notification sources - otherwise we would have to // keep the ContextRoutingConnection - just to preserve // the 'wrapping listeners' // String namespace = NAMESPACE_PLUS_SEP + encodedContext; final ContextRoutingConnection route = new ContextRoutingConnection(loopMBS, namespace); if (LOG.isLoggable(Level.FINE)) LOG.fine("context="+encodedContext); if (LOG.isLoggable(Level.FINER)) LOG.finer(method.getName()+""+ ((args==null)?"()":(""+Arrays.asList(args)))); return invoke(method, route, args); } }); } private static Object invoke(Method method, Object target, Object[] args) throws Exception { try { return method.invoke(target, args); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof Error) throw (Error) cause; throw (Exception) cause; } } } private static String mapToString(Map map) { StringBuilder sb = new StringBuilder(); for (Map.Entry entry : map.entrySet()) { String key = encode(entry.getKey()); String value = encode(entry.getValue()); if (sb.length() > 0) sb.append(";"); sb.append(key).append("=").append(value); } return sb.toString(); } private static Map stringToMap(String encodedContext) { Map map = stringToMapOrNull(encodedContext); if (map == null) { throw new IllegalArgumentException( "Invalid encoded context: " + encodedContext); } return map; } private static Map stringToMapOrNull(String encodedContext) { Map map = new LinkedHashMap(); StringTokenizer stok = new StringTokenizer(encodedContext, ";"); while (stok.hasMoreTokens()) { String tok = stok.nextToken(); int eq = tok.indexOf('='); if (eq < 0) return null; String key = decode(tok.substring(0, eq)); if (key.equals("")) return null; String value = decode(tok.substring(eq + 1)); map.put(key, value); } return map; } private static String encode(String s) { try { s = URLEncoder.encode(s, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); // Should not happen } return s.replace("*", "%2A"); // The * character is left intact in URL encodings, but for us it // is special (an ObjectName wildcard) so we must map it. // We are assuming that URLDecoder will decode it the same way as any // other hex escape. } private static String decode(String s) { try { return URLDecoder.decode(s, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } }