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 } |
|