jdk/src/share/classes/javax/management/ClientContext.java
changeset 4159 9e3aae7675f1
parent 4158 0b4d21bc8b5c
parent 4156 acaa49a2768a
child 4160 bda0a85afcb7
equal deleted inserted replaced
4158:0b4d21bc8b5c 4159:9e3aae7675f1
     1 /*
       
     2  * Copyright 2007-2008 Sun Microsystems, Inc.  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.  Sun designates this
       
     8  * particular file as subject to the "Classpath" exception as provided
       
     9  * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
       
    22  * CA 95054 USA or visit www.sun.com if you need additional information or
       
    23  * have any questions.
       
    24  */
       
    25 
       
    26 package javax.management;
       
    27 
       
    28 import com.sun.jmx.interceptor.SingleMBeanForwarder;
       
    29 import com.sun.jmx.namespace.RoutingConnectionProxy;
       
    30 import com.sun.jmx.namespace.RoutingProxy;
       
    31 import com.sun.jmx.namespace.RoutingServerProxy;
       
    32 import java.io.UnsupportedEncodingException;
       
    33 import java.lang.reflect.InvocationHandler;
       
    34 import java.lang.reflect.InvocationTargetException;
       
    35 import java.lang.reflect.Method;
       
    36 import java.lang.reflect.Proxy;
       
    37 import java.net.URLDecoder;
       
    38 import java.net.URLEncoder;
       
    39 import java.util.Arrays;
       
    40 import java.util.Collections;
       
    41 import java.util.LinkedHashMap;
       
    42 import java.util.Locale;
       
    43 import java.util.Map;
       
    44 import java.util.Set;
       
    45 import java.util.StringTokenizer;
       
    46 import java.util.TreeMap;
       
    47 import java.util.concurrent.Callable;
       
    48 import java.util.logging.Level;
       
    49 import java.util.logging.Logger;
       
    50 import static javax.management.namespace.JMXNamespaces.NAMESPACE_SEPARATOR;
       
    51 import javax.management.namespace.JMXNamespaces;
       
    52 import javax.management.namespace.JMXNamespace;
       
    53 import javax.management.namespace.JMXNamespaceMBean;
       
    54 import javax.management.namespace.MBeanServerSupport;
       
    55 import javax.management.remote.IdentityMBeanServerForwarder;
       
    56 import javax.management.remote.MBeanServerForwarder;
       
    57 
       
    58 /**
       
    59  * <p>Methods to communicate a client context to MBeans.  A context is
       
    60  * a {@literal Map<String, String>} that is provided by the client and
       
    61  * that an MBean can consult using the {@link #getContext()} method.
       
    62  * The context is set on a per-thread basis and can be consulted by any
       
    63  * code that the target MBean calls within the thread.</p>
       
    64  *
       
    65  * <p>One common usage of client context is to communicate the client's
       
    66  * {@link Locale} to MBeans.  For example, if an MBean has a String attribute
       
    67  * {@code LastProblemDescription}, the value of that attribute could be
       
    68  * a description of the last problem encountered by the MBean, translated
       
    69  * into the client's locale.  Different clients accessing this attribute
       
    70  * from different locales would each see the appropriate version for their
       
    71  * locale.</p>
       
    72  *
       
    73  * <p>The locale case is sufficiently important that it has a special
       
    74  * shorthand, the {@link #getLocale()} method.  This method calls
       
    75  * <code>{@link #getContext()}.get({@link #LOCALE_KEY})</code> and converts the
       
    76  * resultant String into a Locale object.</p>
       
    77  *
       
    78  * <p>Here is what an MBean with a localized {@code LastProblemDescription}
       
    79  * attribute might look like:</p>
       
    80  *
       
    81  * <pre>
       
    82  * public class LocaleSensitive implements LocaleSensitiveMBean {
       
    83  *     ...
       
    84  *     public String getLastProblemDescription() {
       
    85  *         Locale loc = {@link #getLocale() ClientContext.getLocale()};
       
    86  *         ResourceBundle rb = ResourceBundle.getBundle("MyResources", loc);
       
    87  *         String resourceKey = getLastProblemResourceKey();
       
    88  *         return rb.getString(resourceKey);
       
    89  *     }
       
    90  *     ...
       
    91  * }
       
    92  * </pre>
       
    93  *
       
    94  * <p>Here is how a client can communicate its locale to the target
       
    95  * MBean:</p>
       
    96  *
       
    97  * <pre>
       
    98  * JMXConnector connector = JMXConnectorFactory.connect(url);
       
    99  * MBeanServerConnection connection = connector.getMBeanServerConnection();
       
   100  * <b>MBeanServerConnection localizedConnection =
       
   101  *     {@link #withLocale(MBeanServerConnection, Locale)
       
   102  *      ClientContext.withLocale}(connection, Locale.getDefault());</b>
       
   103  * String problem = localizedConnection.getAttribute(
       
   104  *          objectName, "LastProblemDescription");
       
   105  * </pre>
       
   106  *
       
   107  * <p>In the more general case where the client wants to communicate context
       
   108  * other than the locale, it can use {@link #withContext(MBeanServerConnection,
       
   109  * String, String) withContext} instead of {@code withLocale}, and the target
       
   110  * MBean can retrieve the context using {@link #getContext()}.</p>
       
   111  *
       
   112  *
       
   113  * <h3 id="remote-use">Remote use of contexts</h3>
       
   114  *
       
   115  * <p>The various {@code with*} methods, for example {@link
       
   116  * #withLocale(javax.management.MBeanServer, java.util.Locale) withLocale},
       
   117  * transmit the context of each request by encoding it in the ObjectName of
       
   118  * the request.  For example, if a client creates a connection in the
       
   119  * French locale like this...</p>
       
   120  *
       
   121  * <pre>
       
   122  * MBeanServerConnection mbsc = ...;
       
   123  * Locale french = new Locale("fr");
       
   124  * MBeanServerConnection localizedConnection = ClientContext.withLocale(mbsc, french);
       
   125  * </pre>
       
   126  *
       
   127  * <p>...or, equivalently, like this...</p>
       
   128  *
       
   129  * <pre>
       
   130  * MBeanServerConnection localizedConnection =
       
   131  *     ClientContext.withContext(mbsc, {@link #LOCALE_KEY "jmx.locale"}, "fr");
       
   132  * </pre>
       
   133  *
       
   134  * <p>...then the context associates {@code "jmx.locale"} with {@code "fr"}
       
   135  * and a request such as<br>
       
   136  * {@code localizedConnection.getAttribute("java.lang:type=Runtime", "Name")}<br>
       
   137  * is translated into<br>
       
   138  * {@code mbsc.getAttribute("jmx.context//jmx.locale=fr//java.lang:Runtime", "Name")}.<br>
       
   139  * A special {@linkplain javax.management.namespace namespace} {@code jmx.context//}
       
   140  * extracts the context from the string {@code jmx.locale=fr} and establishes
       
   141  * it in the thread that will do<br>
       
   142  * {@code getAttribute("java.lang:Runtime", "Name")}.</p>
       
   143  *
       
   144  * <p>The details of how contexts are encoded into ObjectNames are explained
       
   145  * in the {@link #encode encode} method.</p>
       
   146  *
       
   147  * <p>The namespace {@code jmx.context//} just mentioned is only needed by
       
   148  * remote clients, since local clients can set the context directly using
       
   149  * {@link #doWithContext doWithContext}.  Accordingly, this namespace is not
       
   150  * present by default in the {@code MBeanServer}.  Instead, it is
       
   151  * <em>simulated</em> by the standard RMI connector using a special
       
   152  * {@link MBeanServerForwarder}.  If you are using this connector, you do not
       
   153  * need to do anything special.  Other connectors may or may not simulate this
       
   154  * namespace in the same way.  If the connector server returns true from the
       
   155  * method {@link
       
   156  * javax.management.remote.JMXConnectorServer#supportsSystemMBeanServerForwarder()
       
   157  * supportsSystemMBeanServerForwarder} then it does simulate the namespace.
       
   158  * If you are using another connector, or if you want to be able to use the
       
   159  * {@code with*} methods locally, then you can install the {@code
       
   160  * MBeanServerForwarder} yourself as described in the method {@link
       
   161  * #newContextForwarder newContextForwarder}.</p>
       
   162  */
       
   163 public class ClientContext {
       
   164     /**
       
   165      * <p>The context key for the client locale.  The string associated with
       
   166      * this key is an encoded locale such as {@code en_US} which could be
       
   167      * returned by {@link Locale#toString()}.</p>
       
   168      */
       
   169     public static final String LOCALE_KEY = "jmx.locale";
       
   170 
       
   171     private static final Logger LOG =
       
   172             Logger.getLogger("javax.management.context");
       
   173 
       
   174     /**
       
   175      * <p>The namespace that implements contexts, {@value}.</p>
       
   176      */
       
   177     public static final String
       
   178             NAMESPACE = "jmx.context";
       
   179     private static final String NAMESPACE_PLUS_SEP =
       
   180             NAMESPACE + NAMESPACE_SEPARATOR;
       
   181     static final ObjectName CLIENT_CONTEXT_NAMESPACE_HANDLER =
       
   182             ObjectName.valueOf(NAMESPACE_PLUS_SEP + ":" +
       
   183                     JMXNamespace.TYPE_ASSIGNMENT);
       
   184     private static final ObjectName NAMESPACE_HANDLER_WITHOUT_NAMESPACE =
       
   185             ObjectName.valueOf(":" + JMXNamespace.TYPE_ASSIGNMENT);
       
   186 
       
   187     private static final ThreadLocal<Map<String, String>> contextThreadLocal =
       
   188             new InheritableThreadLocal<Map<String, String>>() {
       
   189         @Override
       
   190         protected Map<String, String> initialValue() {
       
   191             return Collections.emptyMap();
       
   192         }
       
   193     };
       
   194 
       
   195     /** There are no instances of this class. */
       
   196     private ClientContext() {
       
   197     }
       
   198 
       
   199     /**
       
   200      * <p>Get the client context associated with the current thread.
       
   201      *
       
   202      * @return the client context associated with the current thread.
       
   203      * This may be an empty Map, but it cannot be null.  The returned
       
   204      * Map cannot be modified.
       
   205      */
       
   206     public static Map<String, String> getContext() {
       
   207         return Collections.unmodifiableMap(contextThreadLocal.get());
       
   208     }
       
   209 
       
   210     /**
       
   211      * <p>Get the client locale associated with the current thread.
       
   212      * If the client context includes the {@value #LOCALE_KEY} key
       
   213      * then the returned value is the Locale encoded in that key.
       
   214      * Otherwise the returned value is the {@linkplain Locale#getDefault()
       
   215      * default locale}.
       
   216      *
       
   217      * @return the client locale.
       
   218      */
       
   219     public static Locale getLocale() {
       
   220         String localeS = getContext().get(LOCALE_KEY);
       
   221         if (localeS == null)
       
   222             return Locale.getDefault();
       
   223         // Parse the locale string.  Why isn't there a method in Locale for this?
       
   224         String language, country, variant;
       
   225         int ui = localeS.indexOf('_');
       
   226         if (ui < 0) {
       
   227             language = localeS;
       
   228             country = variant = "";
       
   229         } else {
       
   230             language = localeS.substring(0, ui);
       
   231             localeS = localeS.substring(ui + 1);
       
   232             ui = localeS.indexOf('_');
       
   233             if (ui < 0) {
       
   234                 country = localeS;
       
   235                 variant = "";
       
   236             } else {
       
   237                 country = localeS.substring(0, ui);
       
   238                 variant = localeS.substring(ui + 1);
       
   239             }
       
   240         }
       
   241         return new Locale(language, country, variant);
       
   242     }
       
   243 
       
   244     /**
       
   245      * <p>Execute the given {@code task} with the client context set to
       
   246      * the given Map.  This Map will be the result of {@link #getContext()}
       
   247      * within the {@code task}.</p>
       
   248      *
       
   249      * <p>The {@code task} may include nested calls to {@code doWithContext}.
       
   250      * The value returned by {@link #getContext} at any point is the Map
       
   251      * provided to the most recent {@code doWithContext} (in the current thread)
       
   252      * that has not yet returned.</p>
       
   253      *
       
   254      * <p>The {@link #getContext()} method returns the same value immediately
       
   255      * after a call to this method as immediately before.  In other words,
       
   256      * {@code doWithContext} only affects the context during the execution of
       
   257      * the {@code task}.</p>
       
   258      *
       
   259      * <p>As an example, suppose you want to get an attribute with whatever
       
   260      * context has already been set, plus the locale set to "fr".  You could
       
   261      * write this:</p>
       
   262      *
       
   263      * <pre>
       
   264      * {@code Map<String, String>} context =
       
   265      *     new {@code HashMap<String, String>}(ClientContext.getContext());
       
   266      * context.put(ClientContext.LOCALE_KEY, "fr");
       
   267      * String lastProblemDescription =
       
   268      *     ClientContext.doWithContext(context, new {@code Callable<String>}() {
       
   269      *         public String call() {
       
   270      *             return (String) mbeanServer.getAttribute(mbean, "LastProblemDescription");
       
   271      *         }
       
   272      *     });
       
   273      * </pre>
       
   274      *
       
   275      * @param <T> the type of value that the task will return.  This type
       
   276      * parameter is usually inferred from the type of the {@code task}
       
   277      * parameter.  For example, if {@code task} is a {@code Callable<String>}
       
   278      * then {@code T} is {@code String}.  If the task does not return a value,
       
   279      * use a {@code Callable<Void>} and return null from its
       
   280      * {@link Callable#call call} method.
       
   281      * @param context the context to use while executing {@code task}.
       
   282      * @param task the task to run with the {@code key}={@code value}
       
   283      * binding.
       
   284      * @return the result of {@link Callable#call() task.call()}.
       
   285      * @throws IllegalArgumentException if either parameter is null, or
       
   286      * if any key in {@code context} is null or empty, or if any value
       
   287      * in {@code context} is null.
       
   288      * @throws Exception If {@link Callable#call() task.call()} throws an
       
   289      * exception, {@code doWithContext} throws the same exception.
       
   290      */
       
   291     public static <T> T doWithContext(Map<String, String> context, Callable<T> task)
       
   292     throws Exception {
       
   293         if (context == null || task == null)
       
   294             throw new IllegalArgumentException("Null parameter");
       
   295         Map<String, String> contextCopy = new TreeMap<String, String>(context);
       
   296         validateContext(contextCopy);
       
   297         Map<String, String> oldContextMap = contextThreadLocal.get();
       
   298         try {
       
   299             contextThreadLocal.set(contextCopy);
       
   300             return task.call();
       
   301         } finally {
       
   302             contextThreadLocal.set(oldContextMap);
       
   303         }
       
   304     }
       
   305 
       
   306     private static void validateContext(Map<String, String> context) {
       
   307         for (Map.Entry<String, String> entry : context.entrySet()) {
       
   308             // If the user passes a raw Map rather than a Map<String, String>,
       
   309             // entries could contain objects other than Strings.  If so,
       
   310             // we'll get a ClassCastException here.
       
   311             String key = entry.getKey();
       
   312             String value = entry.getValue();
       
   313             if (key == null || value == null)
       
   314                 throw new IllegalArgumentException("Null key or value in context");
       
   315             if (key.equals(""))
       
   316                 throw new IllegalArgumentException("Empty key in context");
       
   317         }
       
   318     }
       
   319 
       
   320     /**
       
   321      * <p>Return an MBeanServer object that is equivalent to the given
       
   322      * MBeanServer object except that operations on MBeans run with
       
   323      * the given Locale in their {@linkplain #getContext() thread context}.
       
   324      * Note that this will only work if the given MBeanServer supports
       
   325      * contexts, as described <a href="#remote-use">above</a>.</p>
       
   326      *
       
   327      * <p>This method is equivalent to {@link #withContext(MBeanServer,
       
   328      * String, String) withContext}<code>(mbs, {@value LOCALE_KEY},
       
   329      * locale.toString())</code>.</p>
       
   330      *
       
   331      * @throws IllegalArgumentException if either parameter is null, or if
       
   332      * {@code mbs} does not support contexts.  In the second case only,
       
   333      * the cause of the {@code IllegalArgumentException} will be an {@link
       
   334      * InstanceNotFoundException}.
       
   335      */
       
   336     public static MBeanServer withLocale(MBeanServer mbs, Locale locale) {
       
   337         return withLocale(mbs, MBeanServer.class, locale);
       
   338     }
       
   339 
       
   340     /**
       
   341      * <p>Return an MBeanServerConnection object that is equivalent to the given
       
   342      * MBeanServerConnection object except that operations on MBeans run with
       
   343      * the given Locale in their {@linkplain #getContext() thread context}.
       
   344      * Note that this will only work if the given MBeanServerConnection supports
       
   345      * contexts, as described <a href="#remote-use">above</a>.</p>
       
   346      *
       
   347      * <p>This method is equivalent to {@link #withContext(MBeanServerConnection,
       
   348      * String, String) withContext}<code>(mbs, {@value LOCALE_KEY},
       
   349      * locale.toString())</code>.</p>
       
   350      *
       
   351      * @throws IllegalArgumentException if either parameter is null, or if
       
   352      * the communication with {@code mbsc} fails, or if {@code mbsc} does not
       
   353      * support contexts.  If the communication with {@code mbsc} fails, the
       
   354      * {@linkplain Throwable#getCause() cause} of this exception will be an
       
   355      * {@code IOException}.  If {@code mbsc} does not support contexts, the
       
   356      * cause will be an {@link InstanceNotFoundException}.
       
   357      */
       
   358     public static MBeanServerConnection withLocale(
       
   359             MBeanServerConnection mbsc, Locale locale) {
       
   360         return withLocale(mbsc, MBeanServerConnection.class, locale);
       
   361     }
       
   362 
       
   363     private static <T extends MBeanServerConnection> T withLocale(
       
   364             T mbsc, Class<T> mbscClass, Locale locale) {
       
   365         if (locale == null)
       
   366             throw new IllegalArgumentException("Null locale");
       
   367         return withContext(mbsc, mbscClass, LOCALE_KEY, locale.toString());
       
   368     }
       
   369 
       
   370     /**
       
   371      * <p>Return an MBeanServer object that is equivalent to the given
       
   372      * MBeanServer object except that operations on MBeans run with
       
   373      * the given key bound to the given value in their {@linkplain
       
   374      * #getContext() thread context}.
       
   375      * Note that this will only work if the given MBeanServer supports
       
   376      * contexts, as described <a href="#remote-use">above</a>.</p>
       
   377      *
       
   378      * @param mbs the original MBeanServer.
       
   379      * @param key the key to bind in the context of MBean operations
       
   380      * in the returned MBeanServer object.
       
   381      * @param value the value to bind to the key in the context of MBean
       
   382      * operations in the returned MBeanServer object.
       
   383      * @throws IllegalArgumentException if any parameter is null, or
       
   384      * if {@code key} is the empty string, or if {@code mbs} does not support
       
   385      * contexts.  In the last case only, the cause of the {@code
       
   386      * IllegalArgumentException} will be an {@link InstanceNotFoundException}.
       
   387      */
       
   388     public static MBeanServer withContext(
       
   389             MBeanServer mbs, String key, String value) {
       
   390         return withContext(mbs, MBeanServer.class, key, value);
       
   391     }
       
   392 
       
   393     /**
       
   394      * <p>Return an MBeanServerConnection object that is equivalent to the given
       
   395      * MBeanServerConnection object except that operations on MBeans run with
       
   396      * the given key bound to the given value in their {@linkplain
       
   397      * #getContext() thread context}.
       
   398      * Note that this will only work if the given MBeanServerConnection supports
       
   399      * contexts, as described <a href="#remote-use">above</a>.</p>
       
   400      *
       
   401      * @param mbsc the original MBeanServerConnection.
       
   402      * @param key the key to bind in the context of MBean operations
       
   403      * in the returned MBeanServerConnection object.
       
   404      * @param value the value to bind to the key in the context of MBean
       
   405      * operations in the returned MBeanServerConnection object.
       
   406      * @throws IllegalArgumentException if any parameter is null, or
       
   407      * if {@code key} is the empty string, or if the communication with {@code
       
   408      * mbsc} fails, or if {@code mbsc} does not support contexts.  If
       
   409      * the communication with {@code mbsc} fails, the {@linkplain
       
   410      * Throwable#getCause() cause} of this exception will be an {@code
       
   411      * IOException}.  If {@code mbsc} does not support contexts, the cause will
       
   412      * be an {@link InstanceNotFoundException}.
       
   413      */
       
   414     public static MBeanServerConnection withContext(
       
   415             MBeanServerConnection mbsc, String key, String value) {
       
   416         return withContext(mbsc, MBeanServerConnection.class, key, value);
       
   417     }
       
   418 
       
   419 
       
   420     /**
       
   421      * <p>Returns an MBeanServerConnection object that is equivalent to the
       
   422      * given MBeanServerConnection object except that remote operations on
       
   423      * MBeans run with the context that has been established by the client
       
   424      * using {@link #doWithContext doWithContext}.  Note that this will
       
   425      * only work if the remote system supports contexts, as described <a
       
   426      * href="#remote-use">above</a>.</p>
       
   427      *
       
   428      * <p>For example, suppose the remote system does support contexts, and you
       
   429      * have created a {@code JMXConnector} like this:</p>
       
   430      *
       
   431      * <pre>
       
   432      * JMXServiceURL url = ...;
       
   433      * JMXConnector client = JMXConnectorFactory.connect(url);
       
   434      * MBeanServerConnection mbsc = client.getMBeanServerConnection();
       
   435      * <b>mbsc = ClientContext.withDynamicContext(mbsc);</b>
       
   436      * </pre>
       
   437      *
       
   438      * <p>Then if you do this...</p>
       
   439      *
       
   440      * <pre>
       
   441      * MBeanInfo mbi = ClientContext.doWithContext(
       
   442      *     Collections.singletonMap(ClientContext.LOCALE_KEY, "fr"),
       
   443      *     new {@code Callable<MBeanInfo>}() {
       
   444      *         public MBeanInfo call() {
       
   445      *             return mbsc.getMBeanInfo(objectName);
       
   446      *         }
       
   447      *     });
       
   448      * </pre>
       
   449      *
       
   450      * <p>...then the context with the locale set to "fr" will be in place
       
   451      * when the {@code getMBeanInfo} is executed on the remote MBean Server.</p>
       
   452      *
       
   453      * @param mbsc the original MBeanServerConnection.
       
   454      *
       
   455      * @throws IllegalArgumentException if the {@code mbsc} parameter is null,
       
   456      * or if the communication with {@code mbsc} fails, or if {@code mbsc}
       
   457      * does not support contexts.  If the communication with {@code mbsc}
       
   458      * fails, the {@linkplain Throwable#getCause() cause} of this exception
       
   459      * will be an {@code IOException}.  If {@code mbsc} does not support
       
   460      * contexts, the cause will be an {@link InstanceNotFoundException}.
       
   461      */
       
   462     public static MBeanServerConnection withDynamicContext(
       
   463             MBeanServerConnection mbsc) {
       
   464         // Probe mbsc to get the right exception if it's incommunicado or
       
   465         // doesn't support namespaces.
       
   466         JMXNamespaces.narrowToNamespace(mbsc, NAMESPACE);
       
   467         return (MBeanServerConnection) Proxy.newProxyInstance(
       
   468                 MBeanServerConnection.class.getClassLoader(),
       
   469                 new Class<?>[] {MBeanServerConnection.class},
       
   470                 new DynamicContextIH(mbsc));
       
   471     }
       
   472 
       
   473     private static class DynamicContextIH implements InvocationHandler {
       
   474         private final MBeanServerConnection mbsc;
       
   475 
       
   476         public DynamicContextIH(MBeanServerConnection mbsc) {
       
   477             this.mbsc = mbsc;
       
   478         }
       
   479 
       
   480         public Object invoke(Object proxy, Method method, Object[] args)
       
   481                 throws Throwable {
       
   482             MBeanServerConnection dynMBSC = withContext(
       
   483                     mbsc, MBeanServerConnection.class, getContext(), false);
       
   484             try {
       
   485                 return method.invoke(dynMBSC, args);
       
   486             } catch (InvocationTargetException e) {
       
   487                 throw e.getCause();
       
   488             }
       
   489         }
       
   490     }
       
   491 
       
   492     private static <T extends MBeanServerConnection> T withContext(
       
   493             T mbsc, Class<T> mbscClass, String key, String value) {
       
   494         return withContext(
       
   495                 mbsc, mbscClass, Collections.singletonMap(key, value), true);
       
   496     }
       
   497 
       
   498     private static <T extends MBeanServerConnection> T withContext(
       
   499             T mbsc, Class<T> mbscClass, Map<String, String> context,
       
   500             boolean probe) {
       
   501         if (mbsc == null || context == null)
       
   502             throw new IllegalArgumentException("Null parameter");
       
   503         if (context.isEmpty())
       
   504             return mbsc;
       
   505         validateContext(context);
       
   506         Map<String, String> contextMap = null;
       
   507         if (mbsc.getClass() == RoutingServerProxy.class ||
       
   508                 mbsc.getClass() == RoutingProxy.class) {
       
   509             RoutingProxy<?> nsp = (RoutingProxy<?>) mbsc;
       
   510             String where = nsp.getSourceNamespace();
       
   511             if (where.startsWith(NAMESPACE_PLUS_SEP)) {
       
   512                 /* Try to merge the existing context namespace with the
       
   513                  * new one.  If it doesn't work, we fall back to just
       
   514                  * prefixing jmx.context//key=value, which
       
   515                  * might lead to a name like jmx.c//k1=v1//jmx.c//k2=v2//d:k=v.
       
   516                  */
       
   517                 String encodedContext =
       
   518                         where.substring(NAMESPACE_PLUS_SEP.length());
       
   519                 if (encodedContext.indexOf(NAMESPACE_SEPARATOR) < 0) {
       
   520                     contextMap = stringToMapOrNull(encodedContext);
       
   521                     if (contextMap != null) {
       
   522                         contextMap.putAll(context);
       
   523                         mbsc = mbscClass.cast(nsp.source());
       
   524                     }
       
   525                 }
       
   526             }
       
   527         }
       
   528         if (contextMap == null)
       
   529             contextMap = context;
       
   530         String contextDir = NAMESPACE_PLUS_SEP + mapToString(contextMap);
       
   531         if (mbscClass == MBeanServer.class) {
       
   532             return mbscClass.cast(RoutingServerProxy.cd(
       
   533                     (MBeanServer) mbsc, contextDir, probe));
       
   534         } else if (mbscClass == MBeanServerConnection.class) {
       
   535             return mbscClass.cast(RoutingConnectionProxy.cd(
       
   536                     mbsc, contextDir, probe));
       
   537         } else
       
   538             throw new AssertionError("Bad MBSC: " + mbscClass);
       
   539     }
       
   540 
       
   541     /**
       
   542      * <p>Returns an encoded context prefix for ObjectNames.
       
   543      * If the given context is empty, {@code ""} is returned.
       
   544      * Otherwise, this method returns a string of the form
       
   545      * {@code "jmx.context//key=value;key=value;..."}.
       
   546      * For example, if the context has keys {@code "jmx.locale"}
       
   547      * and {@code "xid"} with respective values {@code "fr"}
       
   548      * and {@code "1234"}, this method will return
       
   549      * {@code "jmx.context//jmx.locale=fr;xid=1234"} or
       
   550      * {@code "jmx.context//xid=1234;jmx.locale=fr"}.</p>
       
   551      *
       
   552      * <p>Each key and each value in the encoded string is subject to
       
   553      * encoding as if by the method {@link URLEncoder#encode(String, String)}
       
   554      * with a character encoding of {@code "UTF-8"}, but with the additional
       
   555      * encoding of any {@code *} character as {@code "%2A"}.  This ensures
       
   556      * that keys and values can contain any character.  Without encoding,
       
   557      * characters such as {@code =} and {@code :} would pose problems.</p>
       
   558      *
       
   559      * @param context the context to encode.
       
   560      *
       
   561      * @return the context in encoded form.
       
   562      *
       
   563      * @throws IllegalArgumentException if the {@code context} parameter
       
   564      * is null or if it contains a null key or value.
       
   565      **/
       
   566     public static String encode(Map<String, String> context) {
       
   567         if (context == null)
       
   568             throw new IllegalArgumentException("Null context");
       
   569         if (context.isEmpty())
       
   570             return "";
       
   571         StringBuilder sb = new StringBuilder();
       
   572         for (Map.Entry<String, String> entry : context.entrySet()) {
       
   573             String key = entry.getKey();
       
   574             String value = entry.getValue();
       
   575             if (key == null || value == null)
       
   576                 throw new IllegalArgumentException("Null key or value");
       
   577             if (sb.length() > 0)
       
   578                 sb.append(";");
       
   579             sb.append(encode(key)).append("=").append(encode(value));
       
   580         }
       
   581         sb.insert(0, NAMESPACE_PLUS_SEP);
       
   582         return sb.toString();
       
   583     }
       
   584 
       
   585     /**
       
   586      * <p>Create a new {@link MBeanServerForwarder} that applies the context
       
   587      * received from a client to the current thread.  A client using
       
   588      * one of the various {@code with*} methods (for example {@link
       
   589      * #withContext(MBeanServerConnection, String, String) withContext}) will
       
   590      * encode that context into the {@code ObjectName} of each
       
   591      * {@code MBeanServer} request.  The object returned by this method
       
   592      * decodes the context from that {@code ObjectName} and applies it
       
   593      * as described for {@link #doWithContext doWithContext} while performing
       
   594      * the {@code MBeanServer} request using the {@code ObjectName} without
       
   595      * the encoded context.</p>
       
   596      *
       
   597      * <p>This forwarder can be used in a number of ways:</p>
       
   598      *
       
   599      * <ul>
       
   600      * <li>
       
   601      * <p>To add context decoding to a local {@code MBeanServer}, you can
       
   602      * write:</p>
       
   603      * <pre>
       
   604      * MBeanServer mbs = {@link
       
   605      * java.lang.management.ManagementFactory#getPlatformMBeanServer()
       
   606      * ManagementFactory.getPlatformMBeanServer()};  // for example
       
   607      * mbs = ClientContext.newContextForwarder(mbs, null);
       
   608      * </pre>
       
   609      *
       
   610      * <li>
       
   611      * <p>To add context decoding to a {@linkplain
       
   612      * javax.management.remote.JMXConnectorServer connector server}:</p>
       
   613      * <pre>
       
   614      * JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(...);
       
   615      * MBeanServer nextMBS = cs.getMBeanServer();
       
   616      * MBeanServerForwarder mbsf = ClientContext.newContextForwarder(nextMBS, null);
       
   617      * cs.{@link
       
   618      * javax.management.remote.JMXConnectorServer#setMBeanServerForwarder
       
   619      * setMBeanServerForwarder}(mbsf);
       
   620      * </pre>
       
   621      *
       
   622      * <li>
       
   623      * <p>For connectors, such as the standard RMI connector, that support
       
   624      * a {@linkplain
       
   625      * javax.management.remote.JMXConnectorServer#getSystemMBeanServerForwarder
       
   626      * system chain} of {@code MBeanServerForwarder}s, this forwarder will
       
   627      * be installed in that chain by default.  See
       
   628      * {@link javax.management.remote.JMXConnectorServer#CONTEXT_FORWARDER
       
   629      * JMXConnectorServer.CONTEXT_FORWARDER}.
       
   630      * </p>
       
   631      *
       
   632      * </ul>
       
   633      *
       
   634      * @param nextMBS the next {@code MBeanServer} in the chain of
       
   635      * forwarders, which might be another {@code MBeanServerForwarder} or
       
   636      * a plain {@code MBeanServer}.  This is the object to which {@code
       
   637      * MBeanServer} requests that do not include a context are sent.  It
       
   638      * will be the value of {@link MBeanServerForwarder#getMBeanServer()
       
   639      * getMBeanServer()} on the returned object, and can be changed with {@link
       
   640      * MBeanServerForwarder#setMBeanServer setMBeanServer}.  It can be null but
       
   641      * must be set to a non-null value before any {@code MBeanServer} requests
       
   642      * arrive.
       
   643      *
       
   644      * @param loopMBS the {@code MBeanServer} to which requests that contain
       
   645      * an encoded context should be sent once the context has been decoded.
       
   646      * For example, if the request is {@link MBeanServer#getAttribute
       
   647      * getAttribute}{@code ("jmx.context//jmx.locale=fr//java.lang:type=Runtime",
       
   648      * "Name")}, then the {@linkplain #getContext() context} of the thread
       
   649      * executing that request will have {@code "jmx.locale"} set to {@code "fr"}
       
   650      * while executing {@code loopMBS.getAttribute("java.lang:type=Runtime",
       
   651      * "Name")}.  If this parameter is null, then these requests will be
       
   652      * sent to the newly-created {@code MBeanServerForwarder}.  Usually
       
   653      * the parameter will either be null or will be the result of {@link
       
   654      * javax.management.remote.JMXConnectorServer#getSystemMBeanServerForwarder
       
   655      * getSystemMBeanServerForwarder()} for the connector server in which
       
   656      * this forwarder will be installed.
       
   657      *
       
   658      * @return a new {@code MBeanServerForwarder} that decodes client context
       
   659      * from {@code ObjectName}s.
       
   660      */
       
   661     /*
       
   662      * What we're building here is confusing enough to need a diagram.
       
   663      * The MBSF that we return is actually the composition of two forwarders:
       
   664      * the first one simulates the existence of the MBean
       
   665      * jmx.context//:type=JMXNamespace, and the second one simulates the
       
   666      * existence of the namespace jmx.context//.  Furthermore, that namespace
       
   667      * loops back to the composed forwarder, so that something like
       
   668      * jmx.context//foo=bar//jmxcontext//baz=buh will work.  And the loopback
       
   669      * goes through yet another forwarder, which simulates the existence of
       
   670      * (e.g.) jmx.context//foo=bar//:type=JMXNamespace, which is needed
       
   671      * notably so that narrowToNamespace will work.
       
   672      *
       
   673      *          |     +--------------------------------------------------+
       
   674      *          v     v                                                  |
       
   675      * +----------------+                                                |
       
   676      * | Handler MBSF   |->accesses to jmx.context//:type=JMXNamespace   |
       
   677      * +----------------+    (handled completely here)   +-------------------+
       
   678      *          |                                        | 2nd Handler MBSF  |
       
   679      *          v                                        +-------------------+
       
   680      * +----------------+                                                ^
       
   681      * | Namespace MBSF |->accesses to jmx.context//**-------------------+
       
   682      * +----------------+    (after attaching context to thread)
       
   683      *          |
       
   684      *          v          accesses to anything else
       
   685      *
       
   686      * And finally, we need to ensure that from the outside the composed object
       
   687      * looks like a single forwarder, so that its get/setMBeanServer methods
       
   688      * will do the expected thing.  That's what the anonymous subclass is for.
       
   689      */
       
   690     public static MBeanServerForwarder newContextForwarder(
       
   691             MBeanServer nextMBS, MBeanServer loopMBS) {
       
   692         final MBeanServerForwarder mbsWrapper =
       
   693                 new IdentityMBeanServerForwarder(nextMBS);
       
   694         DynamicMBean handlerMBean = new StandardMBean(
       
   695                 new JMXNamespace(mbsWrapper), JMXNamespaceMBean.class, false);
       
   696         SingleMBeanForwarder handlerForwarder = new SingleMBeanForwarder(
       
   697                 CLIENT_CONTEXT_NAMESPACE_HANDLER, handlerMBean, true) {
       
   698             @Override
       
   699             public MBeanServer getMBeanServer() {
       
   700                 return ((MBeanServerForwarder) super.getMBeanServer()).getMBeanServer();
       
   701             }
       
   702 
       
   703             @Override
       
   704             public void setMBeanServer(MBeanServer mbs1) {
       
   705                 MBeanServerForwarder mbsf1 = (MBeanServerForwarder)
       
   706                         super.getMBeanServer();
       
   707                 if (mbsf1 != null)
       
   708                     mbsf1.setMBeanServer(mbs1);
       
   709                 else
       
   710                     super.setMBeanServer(mbs1);
       
   711                 mbsWrapper.setMBeanServer(mbs1);
       
   712             }
       
   713         };
       
   714         if (loopMBS == null)
       
   715             loopMBS = handlerForwarder;
       
   716         ContextInvocationHandler contextIH =
       
   717                 new ContextInvocationHandler(nextMBS, loopMBS);
       
   718         MBeanServerForwarder contextForwarder = newForwarderProxy(contextIH);
       
   719         handlerForwarder.setMBeanServer(contextForwarder);
       
   720         return handlerForwarder;
       
   721     }
       
   722 
       
   723     /**
       
   724      * <p>Create a new {@link MBeanServerForwarder} that localizes
       
   725      * descriptions in {@code MBeanInfo} instances returned by
       
   726      * {@link MBeanServer#getMBeanInfo getMBeanInfo}.  The {@code
       
   727      * MBeanServerForwarder} returned by this method passes all {@code
       
   728      * MBeanServer} methods through unchanged to the supplied object, {@code
       
   729      * mbs}, with the exception of {@code getMBeanInfo}.  To handle {@code
       
   730      * getMBeanInfo(objectName)}, it calls {@code mbs.getMBeanInfo(objectName)}
       
   731      * to get an {@code MBeanInfo}, {@code mbi}; it calls {@link
       
   732      * MBeanServer#getClassLoaderFor mbs.getClassLoaderFor(objectName)} to
       
   733      * get a {@code ClassLoader}, {@code cl}; and it calls {@link
       
   734      * #getLocale} to get a {@code Locale}, {@code locale}.  The order
       
   735      * of these three calls is not specified.  Then the result is {@code
       
   736      * mbi.localizeDescriptions(locale, loader)}.</p>
       
   737      *
       
   738      * <p>This forwarder can be used in a number of ways:</p>
       
   739      *
       
   740      * <ul>
       
   741      * <li>
       
   742      * <p>To add description localization to a local {@code MBeanServer}, you
       
   743      * can write:</p>
       
   744      *
       
   745      * <pre>
       
   746      * MBeanServer mbs = {@link
       
   747      * java.lang.management.ManagementFactory#getPlatformMBeanServer()
       
   748      * ManagementFactory.getPlatformMBeanServer()};  // for example
       
   749      * mbs = ClientContext.newLocalizeMBeanInfoForwarder(mbs);
       
   750      * </pre>
       
   751      *
       
   752      * <li>
       
   753      * <p>To add description localization to a {@linkplain
       
   754      * javax.management.remote.JMXConnectorServer connector server}, you will
       
   755      * need to add both a {@linkplain #newContextForwarder context forwarder}
       
   756      * and a localization forwarder, for example like this:</p>
       
   757      *
       
   758      * <pre>
       
   759      * JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(...);
       
   760      * MBeanServer nextMBS = cs.getMBeanServer();
       
   761      * MBeanServerForwarder localizeMBSF =
       
   762      *     ClientContext.newLocalizeMBeanInfoForwarder(nextMBS);
       
   763      * MBeanServerForwarder contextMBSF =
       
   764      *     ClientContext.newContextForwarder(localizeMBSF, null);
       
   765      * cs.{@link
       
   766      * javax.management.remote.JMXConnectorServer#setMBeanServerForwarder
       
   767      * setMBeanServerForwarder}(contextMBSF);
       
   768      * </pre>
       
   769      *
       
   770      * <p>Notice that the context forwarder must run before the localization
       
   771      * forwarder, so that the locale is correctly established when the latter
       
   772      * runs.  So the {@code nextMBS} parameter of the context forwarder must
       
   773      * be the localization forwarder, and not vice versa.</p>
       
   774      *
       
   775      * <li>
       
   776      * <p>For connectors, such as the standard RMI connector, that support
       
   777      * a {@linkplain
       
   778      * javax.management.remote.JMXConnectorServer#getSystemMBeanServerForwarder
       
   779      * system chain} of {@code MBeanServerForwarder}s, the context forwarder and
       
   780      * the localization forwarder will be installed in that chain, in the right
       
   781      * order, if you include
       
   782      * {@link
       
   783      * javax.management.remote.JMXConnectorServer#LOCALIZE_MBEAN_INFO_FORWARDER
       
   784      * LOCALIZE_MBEAN_INFO_FORWARDER} in the environment {@code Map} with
       
   785      * the value {@code "true"}, for example like this:</p>
       
   786      * </p>
       
   787      * <pre>
       
   788      * MBeanServer mbs = ...;
       
   789      * JMXServiceURL url = new JMXServiceURL("service:jmx:rmi://...");
       
   790      * {@code Map<String, Object>} env = new {@code HashMap<String, Object>}();
       
   791      * env.put(JMXConnectorServer.LOCALIZE_MBEAN_INFO_FORWARDER, "true");
       
   792      * JMXConnectorServer cs = JMXConnectorServerFactory.newJMXConnectorServer(
       
   793      *     url, env, mbs);
       
   794      * </pre>
       
   795      *
       
   796      * </ul>
       
   797      *
       
   798      * @param mbs the next {@code MBeanServer} in the chain of
       
   799      * forwarders, which might be another {@code MBeanServerForwarder}
       
   800      * or a plain {@code MBeanServer}.  It will be the value of
       
   801      * {@link MBeanServerForwarder#getMBeanServer() getMBeanServer()}
       
   802      * on the returned object, and can be changed with {@link
       
   803      * MBeanServerForwarder#setMBeanServer setMBeanServer}.  It can be null but
       
   804      * must be set to a non-null value before any {@code MBeanServer} requests
       
   805      * arrive.
       
   806      *
       
   807      * @return a new {@code MBeanServerForwarder} that localizes descriptions
       
   808      * in the result of {@code getMBeanInfo}.
       
   809      */
       
   810     public static MBeanServerForwarder newLocalizeMBeanInfoForwarder(
       
   811             MBeanServer mbs) {
       
   812         return new IdentityMBeanServerForwarder(mbs) {
       
   813             @Override
       
   814             public MBeanInfo getMBeanInfo(ObjectName name)
       
   815                     throws InstanceNotFoundException, IntrospectionException,
       
   816                            ReflectionException {
       
   817                 MBeanInfo mbi = super.getMBeanInfo(name);
       
   818                 Locale locale = getLocale();
       
   819                 ClassLoader loader = getClassLoaderFor(name);
       
   820                 return mbi.localizeDescriptions(locale, loader);
       
   821             }
       
   822         };
       
   823     }
       
   824 
       
   825     private static MBeanServerForwarder newForwarderProxy(InvocationHandler ih) {
       
   826         return (MBeanServerForwarder) Proxy.newProxyInstance(
       
   827                 MBeanServerForwarder.class.getClassLoader(),
       
   828                 new Class<?>[] {MBeanServerForwarder.class},
       
   829                 ih);
       
   830     }
       
   831 
       
   832     // A proxy connection that will strip the 'contextDir' at input (routing),
       
   833     // and put it back at output (createMBean / registerMBean / query* /
       
   834     // getObjectInstance). Usually RoutingProxy / RoutingServerProxy are used
       
   835     // the other way round (they are used for 'cd' - where they need to add
       
   836     // something at input and remove it at output).
       
   837     // For 'cd' operations we create RoutingProxys with a non empty sourceDir,
       
   838     // and a possibly non-empty targetDir. This is the only case where we use
       
   839     // RoutingProxies with an empty sourceDir (sourceDir is what we add at input
       
   840     // and remove at output, targetDir is what we remove at input and add at
       
   841     // output.
       
   842     //
       
   843     // Note that using a transient ContextRoutingConnection
       
   844     // is possible only because RoutingProxys don't rewrite
       
   845     // notifications sources - otherwise we would have to
       
   846     // keep the ContextRoutingConnection - just to preserve
       
   847     // the 'wrapping listeners'
       
   848     //
       
   849     private static final class ContextRoutingConnection
       
   850             extends RoutingServerProxy {
       
   851         public ContextRoutingConnection(MBeanServer source,
       
   852                                  String contextDir) {
       
   853             super(source, "", contextDir, false);
       
   854         }
       
   855 
       
   856         // Not really needed - but this is safer and more optimized.
       
   857         // See RoutingProxy for more details.
       
   858         //
       
   859         @Override
       
   860         public Integer getMBeanCount() {
       
   861             return source().getMBeanCount();
       
   862         }
       
   863 
       
   864         // Not really needed - but this is safer and more optimized.
       
   865         // See RoutingProxy for more details.
       
   866         //
       
   867         @Override
       
   868         public String[] getDomains() {
       
   869             return source().getDomains();
       
   870         }
       
   871 
       
   872         // Not really needed - but this is safer and more optimized.
       
   873         // See RoutingProxy for more details.
       
   874         //
       
   875         @Override
       
   876         public String getDefaultDomain() {
       
   877             return source().getDefaultDomain();
       
   878         }
       
   879 
       
   880     }
       
   881 
       
   882     private static class ContextInvocationHandler implements InvocationHandler {
       
   883         /*
       
   884          * MBeanServer requests that don't include jmx.context//foo=bar//
       
   885          * are forwarded to forwardMBS, which is the unadorned MBeanServer
       
   886          * that knows nothing about the context namespace.
       
   887          * MBeanServer requests that do include this prefix will
       
   888          * usually (depending on the value of the loopMBS parameter to
       
   889          * newContextForwarder) loop back to the combined MBeanServerForwarder
       
   890          * that first implements
       
   891          * jmx.context//:type=JMXNamespace and then implements
       
   892          * jmx.context//foo=bar//.  The reason is that it is valid
       
   893          * to have jmx.context//foo=bar//jmx.context//baz=buh//, although
       
   894          * usually that will be combined into jmx.context//foo=bar;baz=buh//.
       
   895          *
       
   896          * Before forwarding to loopMBS, we must check for :type=JMXNamespace
       
   897          * so that jmx.context//foo=bar//:type=JMXNamespace will exist.  Its
       
   898          * existence is partial because it must remain "invisible": it should
       
   899          * not show up in queryNames or getMBeanCount even though it does
       
   900          * accept getAttribute and isRegistered and all other methods that
       
   901          * reference a single MBean.
       
   902          */
       
   903         private MBeanServer forwardMBS;
       
   904         private final MBeanServer loopMBS;
       
   905         private static final MBeanServer emptyMBS = new MBeanServerSupport() {
       
   906             @Override
       
   907             public DynamicMBean getDynamicMBeanFor(ObjectName name)
       
   908                     throws InstanceNotFoundException {
       
   909                 throw new InstanceNotFoundException(name.toString());
       
   910             }
       
   911 
       
   912             @Override
       
   913             protected Set<ObjectName> getNames() {
       
   914                 return Collections.emptySet();
       
   915             }
       
   916         };
       
   917 
       
   918         ContextInvocationHandler(MBeanServer forwardMBS, MBeanServer loopMBS) {
       
   919             this.forwardMBS = forwardMBS;
       
   920             DynamicMBean handlerMBean = new StandardMBean(
       
   921                     new JMXNamespace(loopMBS), JMXNamespaceMBean.class, false);
       
   922             MBeanServerForwarder handlerMBS = new SingleMBeanForwarder(
       
   923                     NAMESPACE_HANDLER_WITHOUT_NAMESPACE, handlerMBean, false);
       
   924             handlerMBS.setMBeanServer(loopMBS);
       
   925             this.loopMBS = handlerMBS;
       
   926         }
       
   927 
       
   928         public Object invoke(Object proxy, final Method method, final Object[] args)
       
   929         throws Throwable {
       
   930             String methodName = method.getName();
       
   931             Class<?>[] paramTypes = method.getParameterTypes();
       
   932 
       
   933             // If this is a method from MBeanServerForwarder, handle it here.
       
   934             // There are only two such methods: getMBeanServer() and
       
   935             // setMBeanServer(mbs).
       
   936             if (methodName.equals("getMBeanServer"))
       
   937                 return forwardMBS;
       
   938             else if (methodName.equals("setMBeanServer")) {
       
   939                 this.forwardMBS = (MBeanServer) args[0];
       
   940                 return null;
       
   941             }
       
   942 
       
   943             // It is a method from MBeanServer.
       
   944             // Find the first parameter whose declared type is ObjectName,
       
   945             // and see if it is in the context namespace.  If so we need to
       
   946             // trigger the logic for that namespace.  If not, we simply
       
   947             // forward to the next MBeanServer in the chain.  This logic
       
   948             // depends on the fact that if a method in the MBeanServer interface
       
   949             // has a "routing" ObjectName parameter, it is always the first
       
   950             // parameter of that type.  Conversely, if a method has an
       
   951             // ObjectName parameter, then it makes sense to "route" that
       
   952             // method.  Except for deserialize and instantiate, but if we
       
   953             // recognize a context namespace in those methods' ObjectName
       
   954             // parameters it is pretty harmless.
       
   955             int objectNameI = -1;
       
   956             for (int i = 0; i < paramTypes.length; i++) {
       
   957                 if (paramTypes[i] == ObjectName.class) {
       
   958                     objectNameI = i;
       
   959                     break;
       
   960                 }
       
   961             }
       
   962 
       
   963             if (objectNameI < 0)
       
   964                 return invoke(method, forwardMBS, args);
       
   965 
       
   966             ObjectName target = (ObjectName) args[objectNameI];
       
   967             if (target == null ||
       
   968                     !target.getDomain().startsWith(NAMESPACE_PLUS_SEP))
       
   969                 return invoke(method, forwardMBS, args);
       
   970 
       
   971             String domain = target.getDomain().substring(NAMESPACE_PLUS_SEP.length());
       
   972 
       
   973             // The method routes through the (simulated) context namespace.
       
   974             // Decode the context after it, e.g. jmx.context//jmx.locale=fr//...
       
   975             // If there is no context part, we can throw an exception,
       
   976             // because a forwarder has already handled the unique MBean
       
   977             // jmx.context//:type=JMXNamespace.
       
   978             int sep = domain.indexOf(NAMESPACE_SEPARATOR);
       
   979             if (sep < 0)
       
   980                 return invoke(method, emptyMBS, args);  // throw exception
       
   981             final String encodedContext = domain.substring(0, sep);
       
   982 
       
   983             if (method.getName().startsWith("query") &&
       
   984                     (encodedContext.contains("*") || encodedContext.contains("?"))) {
       
   985                 // Queries like jmx.context//*//d:k=v return
       
   986                 // an empty set, consistent with "real" namespaces.
       
   987                 return Collections.EMPTY_SET;
       
   988             }
       
   989 
       
   990             Map<String, String> ctx = new TreeMap<String, String>(getContext());
       
   991             ctx.putAll(stringToMap(encodedContext));
       
   992 
       
   993             return doWithContext(ctx, new Callable<Object>() {
       
   994                 public Object call() throws Exception {
       
   995                     // Create a proxy connection that will strip
       
   996                     // "jmx.context//" + encodedContext + "//" on input,
       
   997                     // and put it back on output.
       
   998                     //
       
   999                     // Note that using a transient ContextRoutingConnection
       
  1000                     // is possible only because it doesn't rewrite
       
  1001                     // notification sources - otherwise we would have to
       
  1002                     // keep the ContextRoutingConnection - just to preserve
       
  1003                     // the 'wrapping listeners'
       
  1004                     //
       
  1005                     String namespace = NAMESPACE_PLUS_SEP + encodedContext;
       
  1006                     final ContextRoutingConnection route =
       
  1007                               new ContextRoutingConnection(loopMBS, namespace);
       
  1008 
       
  1009                     if (LOG.isLoggable(Level.FINE))
       
  1010                         LOG.fine("context="+encodedContext);
       
  1011                     if (LOG.isLoggable(Level.FINER))
       
  1012                         LOG.finer(method.getName()+""+
       
  1013                             ((args==null)?"()":(""+Arrays.asList(args))));
       
  1014 
       
  1015                     return invoke(method, route, args);
       
  1016                 }
       
  1017             });
       
  1018         }
       
  1019 
       
  1020         private static Object invoke(Method method, Object target, Object[] args)
       
  1021                 throws Exception {
       
  1022             try {
       
  1023                 return method.invoke(target, args);
       
  1024             } catch (InvocationTargetException e) {
       
  1025                 Throwable cause = e.getCause();
       
  1026                 if (cause instanceof Error)
       
  1027                     throw (Error) cause;
       
  1028                 throw (Exception) cause;
       
  1029             }
       
  1030         }
       
  1031     }
       
  1032 
       
  1033     private static String mapToString(Map<String, String> map) {
       
  1034         StringBuilder sb = new StringBuilder();
       
  1035         for (Map.Entry<String, String> entry : map.entrySet()) {
       
  1036             String key = encode(entry.getKey());
       
  1037             String value = encode(entry.getValue());
       
  1038             if (sb.length() > 0)
       
  1039                 sb.append(";");
       
  1040             sb.append(key).append("=").append(value);
       
  1041         }
       
  1042         return sb.toString();
       
  1043     }
       
  1044 
       
  1045     private static Map<String, String> stringToMap(String encodedContext) {
       
  1046         Map<String, String> map = stringToMapOrNull(encodedContext);
       
  1047         if (map == null) {
       
  1048             throw new IllegalArgumentException(
       
  1049                     "Invalid encoded context: " + encodedContext);
       
  1050         }
       
  1051         return map;
       
  1052     }
       
  1053 
       
  1054     private static Map<String, String> stringToMapOrNull(String encodedContext) {
       
  1055         Map<String, String> map = new LinkedHashMap<String, String>();
       
  1056         StringTokenizer stok = new StringTokenizer(encodedContext, ";");
       
  1057         while (stok.hasMoreTokens()) {
       
  1058             String tok = stok.nextToken();
       
  1059             int eq = tok.indexOf('=');
       
  1060             if (eq < 0)
       
  1061                 return null;
       
  1062             String key = decode(tok.substring(0, eq));
       
  1063             if (key.equals(""))
       
  1064                 return null;
       
  1065             String value = decode(tok.substring(eq + 1));
       
  1066             map.put(key, value);
       
  1067         }
       
  1068         return map;
       
  1069     }
       
  1070 
       
  1071     private static String encode(String s) {
       
  1072         try {
       
  1073             s = URLEncoder.encode(s, "UTF-8");
       
  1074         } catch (UnsupportedEncodingException e) {
       
  1075             throw new RuntimeException(e);  // Should not happen
       
  1076         }
       
  1077         return s.replace("*", "%2A");
       
  1078         // The * character is left intact in URL encodings, but for us it
       
  1079         // is special (an ObjectName wildcard) so we must map it.
       
  1080         // We are assuming that URLDecoder will decode it the same way as any
       
  1081         // other hex escape.
       
  1082     }
       
  1083 
       
  1084     private static String decode(String s) {
       
  1085         try {
       
  1086             return URLDecoder.decode(s, "UTF-8");
       
  1087         } catch (UnsupportedEncodingException e) {
       
  1088             throw new RuntimeException(e);
       
  1089         }
       
  1090     }
       
  1091 }