1. /*
  2. * @(#)EnvHelp.java 1.33 04/02/13
  3. *
  4. * Copyright 2004 Sun Microsystems, Inc. All rights reserved.
  5. * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
  6. */
  7. package com.sun.jmx.remote.util;
  8. import java.io.IOException;
  9. import java.io.ObjectOutputStream;
  10. import java.io.OutputStream;
  11. import java.util.Collection;
  12. import java.util.HashMap;
  13. import java.util.Hashtable;
  14. import java.util.Iterator;
  15. import java.util.Map;
  16. import java.util.SortedMap;
  17. import java.util.SortedSet;
  18. import java.util.StringTokenizer;
  19. import java.util.TreeMap;
  20. import java.util.TreeSet;
  21. import javax.management.ObjectName;
  22. import javax.management.MBeanServer;
  23. import javax.management.InstanceNotFoundException;
  24. import javax.management.remote.JMXConnectorFactory;
  25. import javax.management.remote.JMXConnectorServerFactory;
  26. public class EnvHelp {
  27. /**
  28. * <p>Name of the attribute that specifies a default class loader
  29. * object.
  30. * The value associated with this attribute is a ClassLoader object</p>
  31. */
  32. private static final String DEFAULT_CLASS_LOADER =
  33. JMXConnectorFactory.DEFAULT_CLASS_LOADER;
  34. /**
  35. * <p>Name of the attribute that specifies a default class loader
  36. * ObjectName.
  37. * The value associated with this attribute is an ObjectName object</p>
  38. */
  39. private static final String DEFAULT_CLASS_LOADER_NAME =
  40. JMXConnectorServerFactory.DEFAULT_CLASS_LOADER_NAME;
  41. /**
  42. * Get the Connector Server default class loader.
  43. * <p>
  44. * Returns:
  45. * <p>
  46. * <ul>
  47. * <li>
  48. * The ClassLoader object found in <var>env</var> for
  49. * <tt>jmx.remote.default.class.loader</tt>, if any.
  50. * </li>
  51. * <li>
  52. * The ClassLoader pointed to by the ObjectName found in
  53. * <var>env</var> for <tt>jmx.remote.default.class.loader.name</tt>,
  54. * and registered in <var>mbs</var> if any.
  55. * </li>
  56. * <li>
  57. * The current thread's context classloader otherwise.
  58. * </li>
  59. * </ul>
  60. *
  61. * @param env Environment attributes.
  62. * @param mbs The MBeanServer for which the connector server provides
  63. * remote access.
  64. *
  65. * @return the connector server's default class loader.
  66. *
  67. * @exception IllegalArgumentException if one of the following is true:
  68. * <ul>
  69. * <li>both
  70. * <tt>jmx.remote.default.class.loader</tt> and
  71. * <tt>jmx.remote.default.class.loader.name</tt> are specified,
  72. * </li>
  73. * <li>or
  74. * <tt>jmx.remote.default.class.loader</tt> is not
  75. * an instance of {@link ClassLoader},
  76. * </li>
  77. * <li>or
  78. * <tt>jmx.remote.default.class.loader.name</tt> is not
  79. * an instance of {@link ObjectName},
  80. * </li>
  81. * <li>or
  82. * <tt>jmx.remote.default.class.loader.name</tt> is specified
  83. * but <var>mbs</var> is null.
  84. * </li>
  85. * @exception InstanceNotFoundException if
  86. * <tt>jmx.remote.default.class.loader.name</tt> is specified
  87. * and the ClassLoader MBean is not found in <var>mbs</var>.
  88. */
  89. public static ClassLoader resolveServerClassLoader(Map env,
  90. MBeanServer mbs)
  91. throws InstanceNotFoundException {
  92. if (env == null)
  93. return Thread.currentThread().getContextClassLoader();
  94. Object loader = env.get(DEFAULT_CLASS_LOADER);
  95. Object name = env.get(DEFAULT_CLASS_LOADER_NAME);
  96. if (loader != null && name != null) {
  97. final String msg = "Only one of " +
  98. DEFAULT_CLASS_LOADER + " or " +
  99. DEFAULT_CLASS_LOADER_NAME +
  100. " should be specified.";
  101. throw new IllegalArgumentException(msg);
  102. }
  103. if (loader == null && name == null)
  104. return Thread.currentThread().getContextClassLoader();
  105. if (loader != null) {
  106. if (loader instanceof ClassLoader) {
  107. return (ClassLoader) loader;
  108. } else {
  109. final String msg =
  110. "ClassLoader object is not an instance of " +
  111. ClassLoader.class.getName() + " : " +
  112. loader.getClass().getName();
  113. throw new IllegalArgumentException(msg);
  114. }
  115. }
  116. ObjectName on;
  117. if (name instanceof ObjectName) {
  118. on = (ObjectName) name;
  119. } else {
  120. final String msg =
  121. "ClassLoader name is not an instance of " +
  122. ObjectName.class.getName() + " : " +
  123. name.getClass().getName();
  124. throw new IllegalArgumentException(msg);
  125. }
  126. if (mbs == null)
  127. throw new IllegalArgumentException("Null MBeanServer object");
  128. return mbs.getClassLoader(on);
  129. }
  130. /**
  131. * Get the Connector Client default class loader.
  132. * <p>
  133. * Returns:
  134. * <p>
  135. * <ul>
  136. * <li>
  137. * The ClassLoader object found in <var>env</var> for
  138. * <tt>jmx.remote.default.class.loader</tt>, if any.
  139. * </li>
  140. * <li>The <tt>Thread.currentThread().getContextClassLoader()</tt>
  141. * otherwise.
  142. * </li>
  143. * </ul>
  144. * <p>
  145. * Usually a Connector Client will call
  146. * <pre>
  147. * ClassLoader dcl = EnvHelp.resolveClientClassLoader(env);
  148. * </pre>
  149. * in its <tt>connect(Map env)</tt> method.
  150. *
  151. * @return The connector client default class loader.
  152. *
  153. * @exception IllegalArgumentException if
  154. * <tt>jmx.remote.default.class.loader</tt> is specified
  155. * and is not an instance of {@link ClassLoader}.
  156. */
  157. public static ClassLoader resolveClientClassLoader(Map env) {
  158. if (env == null)
  159. return Thread.currentThread().getContextClassLoader();
  160. Object loader = env.get(DEFAULT_CLASS_LOADER);
  161. if (loader == null)
  162. return Thread.currentThread().getContextClassLoader();
  163. if (loader instanceof ClassLoader) {
  164. return (ClassLoader) loader;
  165. } else {
  166. final String msg =
  167. "ClassLoader object is not an instance of " +
  168. ClassLoader.class.getName() + " : " +
  169. loader.getClass().getName();
  170. throw new IllegalArgumentException(msg);
  171. }
  172. }
  173. /**
  174. * Init the cause field of a Throwable object.
  175. * The cause field is set only if <var>t</var> has an
  176. * {@link Throwable#initCause(Throwable)} method (JDK Version >= 1.4)
  177. * @param t Throwable on which the cause must be set.
  178. * @param cause The cause to set on <var>t</var>.
  179. * @return <var>t</var> with or without the cause field set.
  180. */
  181. public static Throwable initCause(Throwable t, Throwable cause) {
  182. /* Make a best effort to set the cause, but if we don't
  183. succeed, too bad, you don't get that useful debugging
  184. information. We jump through hoops here so that we can
  185. work on platforms prior to J2SE 1.4 where the
  186. Throwable.initCause method was introduced. If we change
  187. the public interface of JMRuntimeException in a future
  188. version we can add getCause() so we don't need to do this. */
  189. try {
  190. java.lang.reflect.Method initCause =
  191. t.getClass().getMethod("initCause",
  192. new Class[] {Throwable.class});
  193. initCause.invoke(t, new Object[] {cause});
  194. } catch (Exception e) {
  195. // OK.
  196. // too bad, no debugging info
  197. }
  198. return t;
  199. }
  200. /**
  201. * Returns the cause field of a Throwable object.
  202. * The cause field can be got only if <var>t</var> has an
  203. * {@link Throwable#getCause()} method (JDK Version >= 1.4)
  204. * @param t Throwable on which the cause must be set.
  205. * @return the cause if getCause() succeeded and the got value is not
  206. * null, otherwise return the <var>t</var>.
  207. */
  208. public static Throwable getCause(Throwable t) {
  209. Throwable ret = t;
  210. try {
  211. java.lang.reflect.Method getCause =
  212. t.getClass().getMethod("getCause", (Class[]) null);
  213. ret = (Throwable)getCause.invoke(t, (Object[]) null);
  214. } catch (Exception e) {
  215. // OK.
  216. // it must be older than 1.4.
  217. }
  218. return (ret != null) ? ret: t;
  219. }
  220. /**
  221. * <p>Name of the attribute that specifies the maximum number of
  222. * notifications that a client will fetch from its server.. The
  223. * value associated with this attribute should be an
  224. * <code>Integer</code> object. The default value is 1000.</p>
  225. */
  226. public static final String MAX_FETCH_NOTIFS =
  227. "jmx.remote.x.notification.fetch.max";
  228. /**
  229. * Returns the maximum notification number which a client will
  230. * fetch every time.
  231. */
  232. public static int getMaxFetchNotifNumber(Map env) {
  233. return (int) getIntegerAttribute(env, MAX_FETCH_NOTIFS, 1000, 1,
  234. Integer.MAX_VALUE);
  235. }
  236. /**
  237. * <p>Name of the attribute that specifies the timeout for a
  238. * client to fetch notifications from its server. The value
  239. * associated with this attribute should be a <code>Long</code>
  240. * object. The default value is 60000 milleseconds.</p>
  241. */
  242. public static final String FETCH_TIMEOUT =
  243. "jmx.remote.x.notification.fetch.timeout";
  244. /**
  245. * Returns the timeout for a client to fetch notifications.
  246. */
  247. public static long getFetchTimeout(Map env) {
  248. return getIntegerAttribute(env, FETCH_TIMEOUT, 60000L, 0,
  249. Long.MAX_VALUE);
  250. }
  251. /**
  252. * Get an integer-valued attribute with name <code>name</code>
  253. * from <code>env</code>. If <code>env</code> is null, or does
  254. * not contain an entry for <code>name</code>, return
  255. * <code>defaultValue</code>. The value may be a Number, or it
  256. * may be a String that is parsable as a long. It must be at
  257. * least <code>minValue</code> and at most<code>maxValue</code>.
  258. *
  259. * @throws IllegalArgumentException if <code>env</code> contains
  260. * an entry for <code>name</code> but it does not meet the
  261. * constraints above.
  262. */
  263. public static long getIntegerAttribute(Map env, String name,
  264. long defaultValue, long minValue,
  265. long maxValue) {
  266. final Object o;
  267. if (env == null || (o = env.get(name)) == null)
  268. return defaultValue;
  269. final long result;
  270. if (o instanceof Number)
  271. result = ((Number) o).longValue();
  272. else if (o instanceof String) {
  273. result = Long.parseLong((String) o);
  274. /* May throw a NumberFormatException, which is an
  275. IllegalArgumentException. */
  276. } else {
  277. final String msg =
  278. "Attribute " + name + " value must be Integer or String: " + o;
  279. throw new IllegalArgumentException(msg);
  280. }
  281. if (result < minValue) {
  282. final String msg =
  283. "Attribute " + name + " value must be at least " + minValue +
  284. ": " + result;
  285. throw new IllegalArgumentException(msg);
  286. }
  287. if (result > maxValue) {
  288. final String msg =
  289. "Attribute " + name + " value must be at most " + maxValue +
  290. ": " + result;
  291. throw new IllegalArgumentException(msg);
  292. }
  293. return result;
  294. }
  295. public static final String DEFAULT_ORB="java.naming.corba.orb";
  296. /* Check that all attributes have a key that is a String.
  297. Could make further checks, e.g. appropriate types for attributes. */
  298. public static void checkAttributes(Map attributes) {
  299. for (Iterator it = attributes.keySet().iterator(); it.hasNext(); ) {
  300. Object key = it.next();
  301. if (!(key instanceof String)) {
  302. final String msg =
  303. "Attributes contain key that is not a string: " + key;
  304. throw new IllegalArgumentException(msg);
  305. }
  306. }
  307. }
  308. /* Return a writable map containing only those attributes that are
  309. serializable, and that are not hidden by
  310. jmx.remote.x.hidden.attributes or the default list of hidden
  311. attributes. */
  312. public static Map filterAttributes(Map attributes) {
  313. if (logger.traceOn()) {
  314. logger.trace("filterAttributes", "starts");
  315. }
  316. SortedMap map = new TreeMap(attributes);
  317. purgeUnserializable(map.values());
  318. hideAttributes(map);
  319. return map;
  320. }
  321. /**
  322. * Remove from the given Collection any element that is not a
  323. * serializable object.
  324. */
  325. private static void purgeUnserializable(Collection objects) {
  326. logger.trace("purgeUnserializable", "starts");
  327. ObjectOutputStream oos = null;
  328. int i = 0;
  329. for (Iterator it = objects.iterator(); it.hasNext(); i++) {
  330. Object v = it.next();
  331. if (v == null || v instanceof String) {
  332. if (logger.traceOn()) {
  333. logger.trace("purgeUnserializable",
  334. "Value trivially serializable: " + v);
  335. }
  336. continue;
  337. }
  338. try {
  339. if (oos == null)
  340. oos = new ObjectOutputStream(new SinkOutputStream());
  341. oos.writeObject(v);
  342. if (logger.traceOn()) {
  343. logger.trace("purgeUnserializable",
  344. "Value serializable: " + v);
  345. }
  346. } catch (IOException e) {
  347. if (logger.traceOn()) {
  348. logger.trace("purgeUnserializable",
  349. "Value not serializable: " + v + ": " +
  350. e);
  351. }
  352. it.remove();
  353. oos = null; // ObjectOutputStream invalid after exception
  354. }
  355. }
  356. }
  357. /**
  358. The value of this attribute, if present, is a string specifying
  359. what other attributes should not appear in
  360. JMXConnectorServer.getAttributes(). It is a space-separated
  361. list of attribute patterns, where each pattern is either an
  362. attribute name, or an attribute prefix followed by a "*"
  363. character. The "*" has no special significance anywhere except
  364. at the end of a pattern. By default, this list is added to the
  365. list defined by {@link #DEFAULT_HIDDEN_ATTRIBUTES} (which
  366. uses the same format). If the value of this attribute begins
  367. with an "=", then the remainder of the string defines the
  368. complete list of attribute patterns.
  369. */
  370. public static final String HIDDEN_ATTRIBUTES =
  371. "jmx.remote.x.hidden.attributes";
  372. /**
  373. Default list of attributes not to show.
  374. @see #HIDDEN_ATTRIBUTES
  375. */
  376. /* This list is copied directly from the spec, plus
  377. java.naming.security.*. Most of the attributes here would have
  378. been eliminated from the map anyway because they are typically
  379. not serializable. But just in case they are, we list them here
  380. to conform to the spec. */
  381. public static final String DEFAULT_HIDDEN_ATTRIBUTES =
  382. "java.naming.security.* " +
  383. "jmx.remote.authenticator " +
  384. "jmx.remote.context " +
  385. "jmx.remote.default.class.loader " +
  386. "jmx.remote.message.connection.server " +
  387. "jmx.remote.object.wrapping " +
  388. "jmx.remote.rmi.client.socket.factory " +
  389. "jmx.remote.rmi.server.socket.factory " +
  390. "jmx.remote.sasl.callback.handler " +
  391. "jmx.remote.tls.socket.factory " +
  392. "jmx.remote.x.access.file " +
  393. "jmx.remote.x.password.file ";
  394. private static final SortedSet defaultHiddenStrings = new TreeSet();
  395. private static final SortedSet defaultHiddenPrefixes = new TreeSet();
  396. private static void hideAttributes(SortedMap map) {
  397. if (map.isEmpty())
  398. return;
  399. final SortedSet hiddenStrings;
  400. final SortedSet hiddenPrefixes;
  401. String hide = (String) map.get(HIDDEN_ATTRIBUTES);
  402. if (hide != null) {
  403. if (hide.startsWith("="))
  404. hide = hide.substring(1);
  405. else
  406. hide += " " + DEFAULT_HIDDEN_ATTRIBUTES;
  407. hiddenStrings = new TreeSet();
  408. hiddenPrefixes = new TreeSet();
  409. parseHiddenAttributes(hide, hiddenStrings, hiddenPrefixes);
  410. } else {
  411. hide = DEFAULT_HIDDEN_ATTRIBUTES;
  412. synchronized (defaultHiddenStrings) {
  413. if (defaultHiddenStrings.isEmpty()) {
  414. parseHiddenAttributes(hide,
  415. defaultHiddenStrings,
  416. defaultHiddenPrefixes);
  417. }
  418. hiddenStrings = defaultHiddenStrings;
  419. hiddenPrefixes = defaultHiddenPrefixes;
  420. }
  421. }
  422. /* Construct a string that is greater than any key in the map.
  423. Setting a string-to-match or a prefix-to-match to this string
  424. guarantees that we will never call next() on the corresponding
  425. iterator. */
  426. String sentinelKey = map.lastKey() + "X";
  427. Iterator keyIterator = map.keySet().iterator();
  428. Iterator stringIterator = hiddenStrings.iterator();
  429. Iterator prefixIterator = hiddenPrefixes.iterator();
  430. String nextString;
  431. if (stringIterator.hasNext())
  432. nextString = (String) stringIterator.next();
  433. else
  434. nextString = sentinelKey;
  435. String nextPrefix;
  436. if (prefixIterator.hasNext())
  437. nextPrefix = (String) prefixIterator.next();
  438. else
  439. nextPrefix = sentinelKey;
  440. /* Read each key in sorted order and, if it matches a string
  441. or prefix, remove it. */
  442. keys:
  443. while (keyIterator.hasNext()) {
  444. String key = (String) keyIterator.next();
  445. /* Continue through string-match values until we find one
  446. that is either greater than the current key, or equal
  447. to it. In the latter case, remove the key. */
  448. int cmp = +1;
  449. while ((cmp = nextString.compareTo(key)) < 0) {
  450. if (stringIterator.hasNext())
  451. nextString = (String) stringIterator.next();
  452. else
  453. nextString = sentinelKey;
  454. }
  455. if (cmp == 0) {
  456. keyIterator.remove();
  457. continue keys;
  458. }
  459. /* Continue through the prefix values until we find one
  460. that is either greater than the current key, or a
  461. prefix of it. In the latter case, remove the key. */
  462. while (nextPrefix.compareTo(key) <= 0) {
  463. if (key.startsWith(nextPrefix)) {
  464. keyIterator.remove();
  465. continue keys;
  466. }
  467. if (prefixIterator.hasNext())
  468. nextPrefix = (String) prefixIterator.next();
  469. else
  470. nextPrefix = sentinelKey;
  471. }
  472. }
  473. }
  474. private static void parseHiddenAttributes(String hide,
  475. SortedSet hiddenStrings,
  476. SortedSet hiddenPrefixes) {
  477. final StringTokenizer tok = new StringTokenizer(hide);
  478. while (tok.hasMoreTokens()) {
  479. String s = tok.nextToken();
  480. if (s.endsWith("*"))
  481. hiddenPrefixes.add(s.substring(0, s.length() - 1));
  482. else
  483. hiddenStrings.add(s);
  484. }
  485. }
  486. /**
  487. * <p>Name of the attribute that specifies the timeout to keep a
  488. * server side connection after answering last client request.
  489. * The default value is 120000 milliseconds.</p>
  490. */
  491. public static final String SERVER_CONNECTION_TIMEOUT =
  492. "jmx.remote.x.server.connection.timeout";
  493. /**
  494. * Returns the server side connection timeout.
  495. */
  496. public static long getServerConnectionTimeout(Map env) {
  497. return getIntegerAttribute(env, SERVER_CONNECTION_TIMEOUT, 120000L,
  498. 0, Long.MAX_VALUE);
  499. }
  500. /**
  501. * <p>Name of the attribute that specifies the period in
  502. * millisecond for a client to check its connection. The default
  503. * value is 60000 milliseconds.</p>
  504. */
  505. public static final String CLIENT_CONNECTION_CHECK_PERIOD =
  506. "jmx.remote.x.client.connection.check.period";
  507. /**
  508. * Returns the client connection check oeriod.
  509. */
  510. public static long getConnectionCheckPeriod(Map env) {
  511. return getIntegerAttribute(env, CLIENT_CONNECTION_CHECK_PERIOD, 60000L,
  512. 0, Long.MAX_VALUE);
  513. }
  514. /**
  515. * Converts a map into a valid hash table, i.e.
  516. * it removes all the 'null' values from the map.
  517. */
  518. public static Hashtable mapToHashtable(Map map) {
  519. HashMap m = new HashMap(map);
  520. if (m.containsKey(null)) m.remove(null);
  521. for (Iterator i = m.values().iterator(); i.hasNext(); )
  522. if (i.next() == null) i.remove();
  523. return new Hashtable(m);
  524. }
  525. private static final class SinkOutputStream extends OutputStream {
  526. public void write(byte[] b, int off, int len) {}
  527. public void write(int b) {}
  528. }
  529. private static final ClassLogger logger =
  530. new ClassLogger("javax.management.remote.misc", "EnvHelp");
  531. }