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      * Name of the attribute that specifies a default class loader
  59      * object.
  60      * The value associated with this attribute is a ClassLoader object.
  61      */
  62     private static final String DEFAULT_CLASS_LOADER =
  63         JMXConnectorFactory.DEFAULT_CLASS_LOADER;
  64 
  65     /**
  66      * Name of the attribute that specifies a default class loader
  67      * ObjectName.
  68      * The value associated with this attribute is an ObjectName object.
  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      * <ul>
  78      * <li>
  79      *     The ClassLoader object found in <var>env</var> for
  80      *     <code>jmx.remote.default.class.loader</code>, if any.
  81      * </li>
  82      * <li>
  83      *     The ClassLoader pointed to by the ObjectName found in
  84      *     <var>env</var> for <code>jmx.remote.default.class.loader.name</code>,
  85      *     and registered in <var>mbs</var> if any.
  86      * </li>
  87      * <li>
  88      *     The current thread's context classloader otherwise.
  89      * </li>
  90      * </ul>
  91      *
  92      * @param env Environment attributes.
  93      * @param mbs The MBeanServer for which the connector server provides
  94      * remote access.
  95      *
  96      * @return the connector server's default class loader.
  97      *
  98      * @exception IllegalArgumentException if one of the following is true:
  99      * <ul>
 100      * <li>both
 101      *     <code>jmx.remote.default.class.loader</code> and
 102      *     <code>jmx.remote.default.class.loader.name</code> are specified,
 103      * </li>
 104      * <li>or
 105      *     <code>jmx.remote.default.class.loader</code> is not
 106      *     an instance of {@link ClassLoader},
 107      * </li>
 108      * <li>or
 109      *     <code>jmx.remote.default.class.loader.name</code> is not
 110      *     an instance of {@link ObjectName},
 111      * </li>
 112      * <li>or
 113      *     <code>jmx.remote.default.class.loader.name</code> is specified
 114      *     but <var>mbs</var> is null.
 115      * </li>
 116      * </ul>
 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      * <ul>
 176      * <li>
 177      *     The ClassLoader object found in <var>env</var> for
 178      *     <code>jmx.remote.default.class.loader</code>, if any.
 179      * </li>
 180      * <li>The {@code Thread.currentThread().getContextClassLoader()}
 181      *     otherwise.
 182      * </li>
 183      * </ul>
 184      * <p>
 185      * Usually a Connector Client will call
 186      * <pre>
 187      * ClassLoader dcl = EnvHelp.resolveClientClassLoader(env);
 188      * </pre>
 189      * in its <code>connect(Map env)</code> method.
 190      *
 191      * @return The connector client default class loader.
 192      *
 193      * @exception IllegalArgumentException if
 194      * <code>jmx.remote.default.class.loader</code> is specified
 195      * and is not an instance of {@link ClassLoader}.
 196      */
 197     public static ClassLoader resolveClientClassLoader(Map<String, ?> env) {
 198 
 199         if (env == null)
 200             return Thread.currentThread().getContextClassLoader();
 201 
 202         Object loader = env.get(DEFAULT_CLASS_LOADER);
 203 
 204         if (loader == null)
 205             return Thread.currentThread().getContextClassLoader();
 206 
 207         if (loader instanceof ClassLoader) {
 208             return (ClassLoader) loader;
 209         } else {
 210             final String msg =
 211                 "ClassLoader object is not an instance of " +
 212                 ClassLoader.class.getName() + " : " +
 213                 loader.getClass().getName();
 214             throw new IllegalArgumentException(msg);
 215         }
 216     }
 217 
 218     /**
 219      * Initialize the cause field of a {@code Throwable} object.
 220      *
 221      * @param throwable The {@code Throwable} on which the cause is set.
 222      * @param cause The cause to set on the supplied {@code Throwable}.
 223      * @return the {@code Throwable} with the cause field initialized.
 224      */
 225     public static <T extends Throwable> T initCause(T throwable,
 226                                                     Throwable cause) {
 227         throwable.initCause(cause);
 228         return throwable;
 229     }
 230 
 231     /**
 232      * Returns the cause field of a {@code Throwable} object.
 233      * The cause field can be got only if <var>t</var> has an
 234      * {@link Throwable#getCause()} method (JDK Version {@literal >=} 1.4)
 235      * @param t {@code Throwable} on which the cause must be set.
 236      * @return the cause if getCause() succeeded and the got value is not
 237      * null, otherwise return the <var>t</var>.
 238      */
 239     public static Throwable getCause(Throwable t) {
 240         Throwable ret = t;
 241 
 242         try {
 243             java.lang.reflect.Method getCause =
 244                 t.getClass().getMethod("getCause", (Class<?>[]) null);
 245             ret = (Throwable)getCause.invoke(t, (Object[]) null);
 246 
 247         } catch (Exception e) {
 248             // OK.
 249             // it must be older than 1.4.
 250         }
 251         return (ret != null) ? ret: t;
 252     }
 253 
 254 
 255     /**
 256      * Name of the attribute that specifies the size of a notification
 257      * buffer for a connector server. The default value is 1000.
 258      */
 259     public static final String BUFFER_SIZE_PROPERTY =
 260         "jmx.remote.x.notification.buffer.size";
 261 
 262 
 263     /**
 264      * Returns the size of a notification buffer for a connector server.
 265      * The default value is 1000.
 266      */
 267     public static int getNotifBufferSize(Map<String, ?> env) {
 268         int defaultQueueSize = 1000; // default value
 269 
 270         // keep it for the compability for the fix:
 271         // 6174229: Environment parameter should be notification.buffer.size
 272         // instead of buffer.size
 273         final String oldP = "jmx.remote.x.buffer.size";
 274 
 275         // the default value re-specified in the system
 276         try {
 277             GetPropertyAction act = new GetPropertyAction(BUFFER_SIZE_PROPERTY);
 278             String s = AccessController.doPrivileged(act);
 279             if (s != null) {
 280                 defaultQueueSize = Integer.parseInt(s);
 281             } else { // try the old one
 282                 act = new GetPropertyAction(oldP);
 283                 s = AccessController.doPrivileged(act);
 284                 if (s != null) {
 285                     defaultQueueSize = Integer.parseInt(s);
 286                 }
 287             }
 288         } catch (RuntimeException e) {
 289             logger.warning("getNotifBufferSize",
 290                            "Can't use System property "+
 291                            BUFFER_SIZE_PROPERTY+ ": " + e);
 292               logger.debug("getNotifBufferSize", e);
 293         }
 294 
 295         int queueSize = defaultQueueSize;
 296 
 297         try {
 298             if (env.containsKey(BUFFER_SIZE_PROPERTY)) {
 299                 queueSize = (int)EnvHelp.getIntegerAttribute(env,BUFFER_SIZE_PROPERTY,
 300                                             defaultQueueSize,0,
 301                                             Integer.MAX_VALUE);
 302             } else { // try the old one
 303                 queueSize = (int)EnvHelp.getIntegerAttribute(env,oldP,
 304                                             defaultQueueSize,0,
 305                                             Integer.MAX_VALUE);
 306             }
 307         } catch (RuntimeException e) {
 308             logger.warning("getNotifBufferSize",
 309                            "Can't determine queuesize (using default): "+
 310                            e);
 311             logger.debug("getNotifBufferSize", e);
 312         }
 313 
 314         return queueSize;
 315     }
 316 
 317     /**
 318      * Name of the attribute that specifies the maximum number of
 319      * notifications that a client will fetch from its server. The
 320      * value associated with this attribute should be an
 321      * {@code Integer} object.  The default value is 1000.
 322      */
 323     public static final String MAX_FETCH_NOTIFS =
 324         "jmx.remote.x.notification.fetch.max";
 325 
 326     /**
 327      * Returns the maximum notification number which a client will
 328      * fetch every time.
 329      */
 330     public static int getMaxFetchNotifNumber(Map<String, ?> env) {
 331         return (int) getIntegerAttribute(env, MAX_FETCH_NOTIFS, 1000, 1,
 332                                          Integer.MAX_VALUE);
 333     }
 334 
 335     /**
 336      * Name of the attribute that specifies the timeout for a
 337      * client to fetch notifications from its server. The value
 338      * associated with this attribute should be a <code>Long</code>
 339      * object.  The default value is 60000 milliseconds.
 340      */
 341     public static final String FETCH_TIMEOUT =
 342         "jmx.remote.x.notification.fetch.timeout";
 343 
 344     /**
 345      * Returns the timeout for a client to fetch notifications.
 346      */
 347     public static long getFetchTimeout(Map<String, ?> env) {
 348         return getIntegerAttribute(env, FETCH_TIMEOUT, 60000L, 0,
 349                 Long.MAX_VALUE);
 350     }
 351 
 352     /**
 353      * Name of the attribute that specifies an object that will check
 354      * accesses to add/removeNotificationListener and also attempts to
 355      * receive notifications.  The value associated with this attribute
 356      * should be a <code>NotificationAccessController</code> object.
 357      * The default value is null.
 358      * <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      * 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.
 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      * 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.
 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      * 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 JMX_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 }