1 
   2 /*
   3  * Copyright (c) 2003, 2012, Oracle and/or its affiliates. All rights reserved.
   4  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   5  *
   6  * This code is free software; you can redistribute it and/or modify it
   7  * under the terms of the GNU General Public License version 2 only, as
   8  * published by the Free Software Foundation.  Oracle designates this
   9  * particular file as subject to the "Classpath" exception as provided
  10  * by Oracle in the LICENSE file that accompanied this code.
  11  *
  12  * This code is distributed in the hope that it will be useful, but WITHOUT
  13  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  14  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  15  * version 2 for more details (a copy is included in the LICENSE file that
  16  * accompanied this code).
  17  *
  18  * You should have received a copy of the GNU General Public License version
  19  * 2 along with this work; if not, write to the Free Software Foundation,
  20  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  21  *
  22  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  23  * or visit www.oracle.com if you need additional information or have any
  24  * questions.
  25  */
  26 
  27 package com.sun.jmx.remote.util;
  28 
  29 import java.io.IOException;
  30 import java.io.ObjectOutputStream;
  31 import java.io.OutputStream;
  32 import java.util.Collection;
  33 import java.util.HashMap;
  34 import java.util.Hashtable;
  35 import java.util.Iterator;
  36 import java.util.Map;
  37 import java.util.SortedMap;
  38 import java.util.SortedSet;
  39 import java.util.StringTokenizer;
  40 import java.util.TreeMap;
  41 import java.util.TreeSet;
  42 
  43 import java.security.AccessController;
  44 
  45 import javax.management.ObjectName;
  46 import javax.management.MBeanServer;
  47 import javax.management.InstanceNotFoundException;
  48 import javax.management.remote.JMXConnectorFactory;
  49 import javax.management.remote.JMXConnectorServerFactory;
  50 import com.sun.jmx.mbeanserver.GetPropertyAction;
  51 import com.sun.jmx.remote.security.NotificationAccessController;
  52 import javax.management.remote.JMXConnector;
  53 import javax.management.remote.JMXConnectorServer;
  54 
  55 public class EnvHelp {
  56 
  57     /**
  58      * <p>Name of the attribute that specifies a default class loader
  59      * object.
  60      * The value associated with this attribute is a ClassLoader object</p>
  61      */
  62     private static final String DEFAULT_CLASS_LOADER =
  63         JMXConnectorFactory.DEFAULT_CLASS_LOADER;
  64 
  65     /**
  66      * <p>Name of the attribute that specifies a default class loader
  67      *    ObjectName.
  68      * The value associated with this attribute is an ObjectName object</p>
  69      */
  70     private static final String DEFAULT_CLASS_LOADER_NAME =
  71         JMXConnectorServerFactory.DEFAULT_CLASS_LOADER_NAME;
  72 
  73     /**
  74      * Get the Connector Server default class loader.
  75      * <p>
  76      * Returns:
  77      * <p>
  78      * <ul>
  79      * <li>
  80      *     The ClassLoader object found in <var>env</var> for
  81      *     <code>jmx.remote.default.class.loader</code>, if any.
  82      * </li>
  83      * <li>
  84      *     The ClassLoader pointed to by the ObjectName found in
  85      *     <var>env</var> for <code>jmx.remote.default.class.loader.name</code>,
  86      *     and registered in <var>mbs</var> if any.
  87      * </li>
  88      * <li>
  89      *     The current thread's context classloader otherwise.
  90      * </li>
  91      * </ul>
  92      *
  93      * @param env Environment attributes.
  94      * @param mbs The MBeanServer for which the connector server provides
  95      * remote access.
  96      *
  97      * @return the connector server's default class loader.
  98      *
  99      * @exception IllegalArgumentException if one of the following is true:
 100      * <ul>
 101      * <li>both
 102      *     <code>jmx.remote.default.class.loader</code> and
 103      *     <code>jmx.remote.default.class.loader.name</code> are specified,
 104      * </li>
 105      * <li>or
 106      *     <code>jmx.remote.default.class.loader</code> is not
 107      *     an instance of {@link ClassLoader},
 108      * </li>
 109      * <li>or
 110      *     <code>jmx.remote.default.class.loader.name</code> is not
 111      *     an instance of {@link ObjectName},
 112      * </li>
 113      * <li>or
 114      *     <code>jmx.remote.default.class.loader.name</code> is specified
 115      *     but <var>mbs</var> is null.
 116      * </li>
 117      * @exception InstanceNotFoundException if
 118      * <code>jmx.remote.default.class.loader.name</code> is specified
 119      * and the ClassLoader MBean is not found in <var>mbs</var>.
 120      */
 121     public static ClassLoader resolveServerClassLoader(Map<String, ?> env,
 122                                                        MBeanServer mbs)
 123         throws InstanceNotFoundException {
 124 
 125         if (env == null)
 126             return Thread.currentThread().getContextClassLoader();
 127 
 128         Object loader = env.get(DEFAULT_CLASS_LOADER);
 129         Object name   = env.get(DEFAULT_CLASS_LOADER_NAME);
 130 
 131         if (loader != null && name != null) {
 132             final String msg = "Only one of " +
 133                 DEFAULT_CLASS_LOADER + " or " +
 134                 DEFAULT_CLASS_LOADER_NAME +
 135                 " should be specified.";
 136             throw new IllegalArgumentException(msg);
 137         }
 138 
 139         if (loader == null && name == null)
 140             return Thread.currentThread().getContextClassLoader();
 141 
 142         if (loader != null) {
 143             if (loader instanceof ClassLoader) {
 144                 return (ClassLoader) loader;
 145             } else {
 146                 final String msg =
 147                     "ClassLoader object is not an instance of " +
 148                     ClassLoader.class.getName() + " : " +
 149                     loader.getClass().getName();
 150                 throw new IllegalArgumentException(msg);
 151             }
 152         }
 153 
 154         ObjectName on;
 155         if (name instanceof ObjectName) {
 156             on = (ObjectName) name;
 157         } else {
 158             final String msg =
 159                 "ClassLoader name is not an instance of " +
 160                 ObjectName.class.getName() + " : " +
 161                 name.getClass().getName();
 162             throw new IllegalArgumentException(msg);
 163         }
 164 
 165         if (mbs == null)
 166             throw new IllegalArgumentException("Null MBeanServer object");
 167 
 168         return mbs.getClassLoader(on);
 169     }
 170 
 171     /**
 172      * Get the Connector Client default class loader.
 173      * <p>
 174      * Returns:
 175      * <p>
 176      * <ul>
 177      * <li>
 178      *     The ClassLoader object found in <var>env</var> for
 179      *     <code>jmx.remote.default.class.loader</code>, if any.
 180      * </li>
 181      * <li>The <tt>Thread.currentThread().getContextClassLoader()</tt>
 182      *     otherwise.
 183      * </li>
 184      * </ul>
 185      * <p>
 186      * Usually a Connector Client will call
 187      * <pre>
 188      * ClassLoader dcl = EnvHelp.resolveClientClassLoader(env);
 189      * </pre>
 190      * in its <code>connect(Map env)</code> method.
 191      *
 192      * @return The connector client default class loader.
 193      *
 194      * @exception IllegalArgumentException if
 195      * <code>jmx.remote.default.class.loader</code> is specified
 196      * and is not an instance of {@link ClassLoader}.
 197      */
 198     public static ClassLoader resolveClientClassLoader(Map<String, ?> env) {
 199 
 200         if (env == null)
 201             return Thread.currentThread().getContextClassLoader();
 202 
 203         Object loader = env.get(DEFAULT_CLASS_LOADER);
 204 
 205         if (loader == null)
 206             return Thread.currentThread().getContextClassLoader();
 207 
 208         if (loader instanceof ClassLoader) {
 209             return (ClassLoader) loader;
 210         } else {
 211             final String msg =
 212                 "ClassLoader object is not an instance of " +
 213                 ClassLoader.class.getName() + " : " +
 214                 loader.getClass().getName();
 215             throw new IllegalArgumentException(msg);
 216         }
 217     }
 218 
 219     /**
 220      * Initialize the cause field of a {@code Throwable} object.
 221      *
 222      * @param throwable The {@code Throwable} on which the cause is set.
 223      * @param cause The cause to set on the supplied {@code Throwable}.
 224      * @return the {@code Throwable} with the cause field initialized.
 225      */
 226     public static <T extends Throwable> T initCause(T throwable,
 227                                                     Throwable cause) {
 228         throwable.initCause(cause);
 229         return throwable;
 230     }
 231 
 232     /**
 233      * Returns the cause field of a {@code Throwable} object.
 234      * The cause field can be got only if <var>t</var> has an
 235      * {@link Throwable#getCause()} method (JDK Version >= 1.4)
 236      * @param t {@code Throwable} on which the cause must be set.
 237      * @return the cause if getCause() succeeded and the got value is not
 238      * null, otherwise return the <var>t</var>.
 239      */
 240     public static Throwable getCause(Throwable t) {
 241         Throwable ret = t;
 242 
 243         try {
 244             java.lang.reflect.Method getCause =
 245                 t.getClass().getMethod("getCause", (Class<?>[]) null);
 246             ret = (Throwable)getCause.invoke(t, (Object[]) null);
 247 
 248         } catch (Exception e) {
 249             // OK.
 250             // it must be older than 1.4.
 251         }
 252         return (ret != null) ? ret: t;
 253     }
 254 
 255 
 256     /**
 257      * <p>Name of the attribute that specifies the size of a notification
 258      * buffer for a connector server. The default value is 1000.
 259      */
 260     public static final String BUFFER_SIZE_PROPERTY =
 261         "jmx.remote.x.notification.buffer.size";
 262 
 263 
 264     /**
 265      * Returns the size of a notification buffer for a connector server.
 266      * The default value is 1000.
 267      */
 268     public static int getNotifBufferSize(Map<String, ?> env) {
 269         int defaultQueueSize = 1000; // default value
 270 
 271         // keep it for the compability for the fix:
 272         // 6174229: Environment parameter should be notification.buffer.size
 273         // instead of buffer.size
 274         final String oldP = "jmx.remote.x.buffer.size";
 275 
 276         // the default value re-specified in the system
 277         try {
 278             GetPropertyAction act = new GetPropertyAction(BUFFER_SIZE_PROPERTY);
 279             String s = AccessController.doPrivileged(act);
 280             if (s != null) {
 281                 defaultQueueSize = Integer.parseInt(s);
 282             } else { // try the old one
 283                 act = new GetPropertyAction(oldP);
 284                 s = AccessController.doPrivileged(act);
 285                 if (s != null) {
 286                     defaultQueueSize = Integer.parseInt(s);
 287                 }
 288             }
 289         } catch (RuntimeException e) {
 290             logger.warning("getNotifBufferSize",
 291                            "Can't use System property "+
 292                            BUFFER_SIZE_PROPERTY+ ": " + e);
 293               logger.debug("getNotifBufferSize", e);
 294         }
 295 
 296         int queueSize = defaultQueueSize;
 297 
 298         try {
 299             if (env.containsKey(BUFFER_SIZE_PROPERTY)) {
 300                 queueSize = (int)EnvHelp.getIntegerAttribute(env,BUFFER_SIZE_PROPERTY,
 301                                             defaultQueueSize,0,
 302                                             Integer.MAX_VALUE);
 303             } else { // try the old one
 304                 queueSize = (int)EnvHelp.getIntegerAttribute(env,oldP,
 305                                             defaultQueueSize,0,
 306                                             Integer.MAX_VALUE);
 307             }
 308         } catch (RuntimeException e) {
 309             logger.warning("getNotifBufferSize",
 310                            "Can't determine queuesize (using default): "+
 311                            e);
 312             logger.debug("getNotifBufferSize", e);
 313         }
 314 
 315         return queueSize;
 316     }
 317 
 318     /**
 319      * <p>Name of the attribute that specifies the maximum number of
 320      * notifications that a client will fetch from its server.. The
 321      * value associated with this attribute should be an
 322      * <code>Integer</code> object.  The default value is 1000.</p>
 323      */
 324     public static final String MAX_FETCH_NOTIFS =
 325         "jmx.remote.x.notification.fetch.max";
 326 
 327     /**
 328      * Returns the maximum notification number which a client will
 329      * fetch every time.
 330      */
 331     public static int getMaxFetchNotifNumber(Map<String, ?> env) {
 332         return (int) getIntegerAttribute(env, MAX_FETCH_NOTIFS, 1000, 1,
 333                                          Integer.MAX_VALUE);
 334     }
 335 
 336     /**
 337      * <p>Name of the attribute that specifies the timeout for a
 338      * client to fetch notifications from its server. The value
 339      * associated with this attribute should be a <code>Long</code>
 340      * object.  The default value is 60000 milliseconds.</p>
 341      */
 342     public static final String FETCH_TIMEOUT =
 343         "jmx.remote.x.notification.fetch.timeout";
 344 
 345     /**
 346      * Returns the timeout for a client to fetch notifications.
 347      */
 348     public static long getFetchTimeout(Map<String, ?> env) {
 349         return getIntegerAttribute(env, FETCH_TIMEOUT, 60000L, 0,
 350                 Long.MAX_VALUE);
 351     }
 352 
 353     /**
 354      * <p>Name of the attribute that specifies an object that will check
 355      * accesses to add/removeNotificationListener and also attempts to
 356      * receive notifications.  The value associated with this attribute
 357      * should be a <code>NotificationAccessController</code> object.
 358      * The default value is null.</p>
 359      * This field is not public because of its com.sun dependency.
 360      */
 361     public static final String NOTIF_ACCESS_CONTROLLER =
 362             "com.sun.jmx.remote.notification.access.controller";
 363 
 364     public static NotificationAccessController getNotificationAccessController(
 365             Map<String, ?> env) {
 366         return (env == null) ? null :
 367             (NotificationAccessController) env.get(NOTIF_ACCESS_CONTROLLER);
 368     }
 369 
 370     /**
 371      * Get an integer-valued attribute with name <code>name</code>
 372      * from <code>env</code>.  If <code>env</code> is null, or does
 373      * not contain an entry for <code>name</code>, return
 374      * <code>defaultValue</code>.  The value may be a Number, or it
 375      * may be a String that is parsable as a long.  It must be at
 376      * least <code>minValue</code> and at most<code>maxValue</code>.
 377      *
 378      * @throws IllegalArgumentException if <code>env</code> contains
 379      * an entry for <code>name</code> but it does not meet the
 380      * constraints above.
 381      */
 382     public static long getIntegerAttribute(Map<String, ?> env, String name,
 383                                            long defaultValue, long minValue,
 384                                            long maxValue) {
 385         final Object o;
 386 
 387         if (env == null || (o = env.get(name)) == null)
 388             return defaultValue;
 389 
 390         final long result;
 391 
 392         if (o instanceof Number)
 393             result = ((Number) o).longValue();
 394         else if (o instanceof String) {
 395             result = Long.parseLong((String) o);
 396             /* May throw a NumberFormatException, which is an
 397                IllegalArgumentException.  */
 398         } else {
 399             final String msg =
 400                 "Attribute " + name + " value must be Integer or String: " + o;
 401             throw new IllegalArgumentException(msg);
 402         }
 403 
 404         if (result < minValue) {
 405             final String msg =
 406                 "Attribute " + name + " value must be at least " + minValue +
 407                 ": " + result;
 408             throw new IllegalArgumentException(msg);
 409         }
 410 
 411         if (result > maxValue) {
 412             final String msg =
 413                 "Attribute " + name + " value must be at most " + maxValue +
 414                 ": " + result;
 415             throw new IllegalArgumentException(msg);
 416         }
 417 
 418         return result;
 419     }
 420 
 421     public static final String DEFAULT_ORB="java.naming.corba.orb";
 422 
 423     /* Check that all attributes have a key that is a String.
 424        Could make further checks, e.g. appropriate types for attributes.  */
 425     public static void checkAttributes(Map<?, ?> attributes) {
 426         for (Object key : attributes.keySet()) {
 427             if (!(key instanceof String)) {
 428                 final String msg =
 429                     "Attributes contain key that is not a string: " + key;
 430                 throw new IllegalArgumentException(msg);
 431             }
 432         }
 433     }
 434 
 435     /* Return a writable map containing only those attributes that are
 436        serializable, and that are not hidden by
 437        jmx.remote.x.hidden.attributes or the default list of hidden
 438        attributes.  */
 439     public static <V> Map<String, V> filterAttributes(Map<String, V> attributes) {
 440         if (logger.traceOn()) {
 441             logger.trace("filterAttributes", "starts");
 442         }
 443 
 444         SortedMap<String, V> map = new TreeMap<String, V>(attributes);
 445         purgeUnserializable(map.values());
 446         hideAttributes(map);
 447         return map;
 448     }
 449 
 450     /**
 451      * Remove from the given Collection any element that is not a
 452      * serializable object.
 453      */
 454     private static void purgeUnserializable(Collection<?> objects) {
 455         logger.trace("purgeUnserializable", "starts");
 456         ObjectOutputStream oos = null;
 457         int i = 0;
 458         for (Iterator<?> it = objects.iterator(); it.hasNext(); i++) {
 459             Object v = it.next();
 460 
 461             if (v == null || v instanceof String) {
 462                 if (logger.traceOn()) {
 463                     logger.trace("purgeUnserializable",
 464                                  "Value trivially serializable: " + v);
 465                 }
 466                 continue;
 467             }
 468 
 469             try {
 470                 if (oos == null)
 471                     oos = new ObjectOutputStream(new SinkOutputStream());
 472                 oos.writeObject(v);
 473                 if (logger.traceOn()) {
 474                     logger.trace("purgeUnserializable",
 475                                  "Value serializable: " + v);
 476                 }
 477             } catch (IOException e) {
 478                 if (logger.traceOn()) {
 479                     logger.trace("purgeUnserializable",
 480                                  "Value not serializable: " + v + ": " +
 481                                  e);
 482                 }
 483                 it.remove();
 484                 oos = null; // ObjectOutputStream invalid after exception
 485             }
 486         }
 487     }
 488 
 489     /**
 490      * The value of this attribute, if present, is a string specifying
 491      * what other attributes should not appear in
 492      * JMXConnectorServer.getAttributes().  It is a space-separated
 493      * list of attribute patterns, where each pattern is either an
 494      * attribute name, or an attribute prefix followed by a "*"
 495      * character.  The "*" has no special significance anywhere except
 496      * at the end of a pattern.  By default, this list is added to the
 497      * list defined by {@link #DEFAULT_HIDDEN_ATTRIBUTES} (which
 498      * uses the same format).  If the value of this attribute begins
 499      * with an "=", then the remainder of the string defines the
 500      * complete list of attribute patterns.
 501      */
 502     public static final String HIDDEN_ATTRIBUTES =
 503         "jmx.remote.x.hidden.attributes";
 504 
 505     /**
 506      * Default list of attributes not to show.
 507      * @see #HIDDEN_ATTRIBUTES
 508      */
 509     /* This list is copied directly from the spec, plus
 510        java.naming.security.*.  Most of the attributes here would have
 511        been eliminated from the map anyway because they are typically
 512        not serializable.  But just in case they are, we list them here
 513        to conform to the spec.  */
 514     public static final String DEFAULT_HIDDEN_ATTRIBUTES =
 515         "java.naming.security.* " +
 516         "jmx.remote.authenticator " +
 517         "jmx.remote.context " +
 518         "jmx.remote.default.class.loader " +
 519         "jmx.remote.message.connection.server " +
 520         "jmx.remote.object.wrapping " +
 521         "jmx.remote.rmi.client.socket.factory " +
 522         "jmx.remote.rmi.server.socket.factory " +
 523         "jmx.remote.sasl.callback.handler " +
 524         "jmx.remote.tls.socket.factory " +
 525         "jmx.remote.x.access.file " +
 526         "jmx.remote.x.password.file ";
 527 
 528     private static final SortedSet<String> defaultHiddenStrings =
 529             new TreeSet<String>();
 530     private static final SortedSet<String> defaultHiddenPrefixes =
 531             new TreeSet<String>();
 532 
 533     private static void hideAttributes(SortedMap<String, ?> map) {
 534         if (map.isEmpty())
 535             return;
 536 
 537         final SortedSet<String> hiddenStrings;
 538         final SortedSet<String> hiddenPrefixes;
 539 
 540         String hide = (String) map.get(HIDDEN_ATTRIBUTES);
 541         if (hide != null) {
 542             if (hide.startsWith("="))
 543                 hide = hide.substring(1);
 544             else
 545                 hide += " " + DEFAULT_HIDDEN_ATTRIBUTES;
 546             hiddenStrings = new TreeSet<String>();
 547             hiddenPrefixes = new TreeSet<String>();
 548             parseHiddenAttributes(hide, hiddenStrings, hiddenPrefixes);
 549         } else {
 550             hide = DEFAULT_HIDDEN_ATTRIBUTES;
 551             synchronized (defaultHiddenStrings) {
 552                 if (defaultHiddenStrings.isEmpty()) {
 553                     parseHiddenAttributes(hide,
 554                                           defaultHiddenStrings,
 555                                           defaultHiddenPrefixes);
 556                 }
 557                 hiddenStrings = defaultHiddenStrings;
 558                 hiddenPrefixes = defaultHiddenPrefixes;
 559             }
 560         }
 561 
 562         /* Construct a string that is greater than any key in the map.
 563            Setting a string-to-match or a prefix-to-match to this string
 564            guarantees that we will never call next() on the corresponding
 565            iterator.  */
 566         String sentinelKey = map.lastKey() + "X";
 567         Iterator<String> keyIterator = map.keySet().iterator();
 568         Iterator<String> stringIterator = hiddenStrings.iterator();
 569         Iterator<String> prefixIterator = hiddenPrefixes.iterator();
 570 
 571         String nextString;
 572         if (stringIterator.hasNext())
 573             nextString = stringIterator.next();
 574         else
 575             nextString = sentinelKey;
 576         String nextPrefix;
 577         if (prefixIterator.hasNext())
 578             nextPrefix = prefixIterator.next();
 579         else
 580             nextPrefix = sentinelKey;
 581 
 582         /* Read each key in sorted order and, if it matches a string
 583            or prefix, remove it. */
 584     keys:
 585         while (keyIterator.hasNext()) {
 586             String key = keyIterator.next();
 587 
 588             /* Continue through string-match values until we find one
 589                that is either greater than the current key, or equal
 590                to it.  In the latter case, remove the key.  */
 591             int cmp = +1;
 592             while ((cmp = nextString.compareTo(key)) < 0) {
 593                 if (stringIterator.hasNext())
 594                     nextString = stringIterator.next();
 595                 else
 596                     nextString = sentinelKey;
 597             }
 598             if (cmp == 0) {
 599                 keyIterator.remove();
 600                 continue keys;
 601             }
 602 
 603             /* Continue through the prefix values until we find one
 604                that is either greater than the current key, or a
 605                prefix of it.  In the latter case, remove the key.  */
 606             while (nextPrefix.compareTo(key) <= 0) {
 607                 if (key.startsWith(nextPrefix)) {
 608                     keyIterator.remove();
 609                     continue keys;
 610                 }
 611                 if (prefixIterator.hasNext())
 612                     nextPrefix = prefixIterator.next();
 613                 else
 614                     nextPrefix = sentinelKey;
 615             }
 616         }
 617     }
 618 
 619     private static void parseHiddenAttributes(String hide,
 620                                               SortedSet<String> hiddenStrings,
 621                                               SortedSet<String> hiddenPrefixes) {
 622         final StringTokenizer tok = new StringTokenizer(hide);
 623         while (tok.hasMoreTokens()) {
 624             String s = tok.nextToken();
 625             if (s.endsWith("*"))
 626                 hiddenPrefixes.add(s.substring(0, s.length() - 1));
 627             else
 628                 hiddenStrings.add(s);
 629         }
 630     }
 631 
 632     /**
 633      * <p>Name of the attribute that specifies the timeout to keep a
 634      * server side connection after answering last client request.
 635      * The default value is 120000 milliseconds.</p>
 636      */
 637     public static final String SERVER_CONNECTION_TIMEOUT =
 638         "jmx.remote.x.server.connection.timeout";
 639 
 640     /**
 641      * Returns the server side connection timeout.
 642      */
 643     public static long getServerConnectionTimeout(Map<String, ?> env) {
 644         return getIntegerAttribute(env, SERVER_CONNECTION_TIMEOUT, 120000L,
 645                                    0, Long.MAX_VALUE);
 646     }
 647 
 648     /**
 649      * <p>Name of the attribute that specifies the period in
 650      * millisecond for a client to check its connection.  The default
 651      * value is 60000 milliseconds.</p>
 652      */
 653     public static final String CLIENT_CONNECTION_CHECK_PERIOD =
 654         "jmx.remote.x.client.connection.check.period";
 655 
 656     /**
 657      * Returns the client connection check period.
 658      */
 659     public static long getConnectionCheckPeriod(Map<String, ?> env) {
 660         return getIntegerAttribute(env, CLIENT_CONNECTION_CHECK_PERIOD, 60000L,
 661                                    0, Long.MAX_VALUE);
 662     }
 663 
 664     /**
 665      * Computes a boolean value from a string value retrieved from a
 666      * property in the given map.
 667      *
 668      * @param stringBoolean the string value that must be converted
 669      * into a boolean value.
 670      *
 671      * @return
 672      *   <ul>
 673      *   <li>{@code false} if {@code stringBoolean} is {@code null}</li>
 674      *   <li>{@code false} if
 675      *       {@code stringBoolean.equalsIgnoreCase("false")}
 676      *       is {@code true}</li>
 677      *   <li>{@code true} if
 678      *       {@code stringBoolean.equalsIgnoreCase("true")}
 679      *       is {@code true}</li>
 680      *   </ul>
 681      *
 682      * @throws IllegalArgumentException if
 683      * {@code ((String)env.get(prop)).equalsIgnoreCase("false")} and
 684      * {@code ((String)env.get(prop)).equalsIgnoreCase("true")} are
 685      * {@code false}.
 686      */
 687     public static boolean computeBooleanFromString(String stringBoolean) {
 688         // returns a default value of 'false' if no property is found...
 689         return computeBooleanFromString(stringBoolean,false);
 690     }
 691 
 692     /**
 693      * Computes a boolean value from a string value retrieved from a
 694      * property in the given map.
 695      *
 696      * @param stringBoolean the string value that must be converted
 697      * into a boolean value.
 698      * @param defaultValue a default value to return in case no property
 699      *        was defined.
 700      *
 701      * @return
 702      *   <ul>
 703      *   <li>{@code defaultValue} if {@code stringBoolean}
 704      *   is {@code null}</li>
 705      *   <li>{@code false} if
 706      *       {@code stringBoolean.equalsIgnoreCase("false")}
 707      *       is {@code true}</li>
 708      *   <li>{@code true} if
 709      *       {@code stringBoolean.equalsIgnoreCase("true")}
 710      *       is {@code true}</li>
 711      *   </ul>
 712      *
 713      * @throws IllegalArgumentException if
 714      * {@code ((String)env.get(prop)).equalsIgnoreCase("false")} and
 715      * {@code ((String)env.get(prop)).equalsIgnoreCase("true")} are
 716      * {@code false}.
 717      */
 718     public static boolean computeBooleanFromString( String stringBoolean, boolean defaultValue) {
 719         if (stringBoolean == null)
 720             return defaultValue;
 721         else if (stringBoolean.equalsIgnoreCase("true"))
 722             return true;
 723         else if (stringBoolean.equalsIgnoreCase("false"))
 724             return false;
 725         else
 726             throw new IllegalArgumentException(
 727                 "Property value must be \"true\" or \"false\" instead of \"" +
 728                 stringBoolean + "\"");
 729     }
 730 
 731     /**
 732      * Converts a map into a valid hash table, i.e.
 733      * it removes all the 'null' values from the map.
 734      */
 735     public static <K, V> Hashtable<K, V> mapToHashtable(Map<K, V> map) {
 736         HashMap<K, V> m = new HashMap<K, V>(map);
 737         if (m.containsKey(null)) m.remove(null);
 738         for (Iterator<?> i = m.values().iterator(); i.hasNext(); )
 739             if (i.next() == null) i.remove();
 740         return new Hashtable<K, V>(m);
 741     }
 742 
 743     /**
 744      * <p>Name of the attribute that specifies whether a connector server
 745      * should not prevent the VM from exiting
 746      */
 747     public static final String JMX_SERVER_DAEMON = "jmx.remote.x.daemon";
 748 
 749     /**
 750      * Returns true if {@value SERVER_DAEMON} is specified in the {@code env}
 751      * as a key and its value is a String and it is equal to true ignoring case.
 752      *
 753      * @param env
 754      * @return
 755      */
 756     public static boolean isServerDaemon(Map<String, ?> env) {
 757         return (env != null) &&
 758                 ("true".equalsIgnoreCase((String)env.get(JMX_SERVER_DAEMON)));
 759     }
 760 
 761     private static final class SinkOutputStream extends OutputStream {
 762         public void write(byte[] b, int off, int len) {}
 763         public void write(int b) {}
 764     }
 765 
 766     private static final ClassLogger logger =
 767         new ClassLogger("javax.management.remote.misc", "EnvHelp");
 768 }