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