1 /*
   2  * Copyright (c) 2002, 2008, 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 
  26 package javax.management.remote;
  27 
  28 import com.sun.jmx.mbeanserver.Util;
  29 import java.io.IOException;
  30 import java.net.MalformedURLException;
  31 import java.util.Collections;
  32 import java.util.HashMap;
  33 import java.util.Map;
  34 import java.util.Iterator;
  35 import java.util.ServiceLoader;
  36 import java.util.StringTokenizer;
  37 import java.security.AccessController;
  38 import java.security.PrivilegedAction;
  39 
  40 import com.sun.jmx.remote.util.ClassLogger;
  41 import com.sun.jmx.remote.util.EnvHelp;
  42 import sun.reflect.misc.ReflectUtil;
  43 
  44 
  45 /**
  46  * <p>Factory to create JMX API connector clients.  There
  47  * are no instances of this class.</p>
  48  *
  49  * <p>Connections are usually made using the {@link
  50  * #connect(JMXServiceURL) connect} method of this class.  More
  51  * advanced applications can separate the creation of the connector
  52  * client, using {@link #newJMXConnector(JMXServiceURL, Map)
  53  * newJMXConnector} and the establishment of the connection itself, using
  54  * {@link JMXConnector#connect(Map)}.</p>
  55  *
  56  * <p>Each client is created by an instance of {@link
  57  * JMXConnectorProvider}.  This instance is found as follows.  Suppose
  58  * the given {@link JMXServiceURL} looks like
  59  * <code>"service:jmx:<em>protocol</em>:<em>remainder</em>"</code>.
  60  * Then the factory will attempt to find the appropriate {@link
  61  * JMXConnectorProvider} for <code><em>protocol</em></code>.  Each
  62  * occurrence of the character <code>+</code> or <code>-</code> in
  63  * <code><em>protocol</em></code> is replaced by <code>.</code> or
  64  * <code>_</code>, respectively.</p>
  65  *
  66  * <p>A <em>provider package list</em> is searched for as follows:</p>
  67  *
  68  * <ol>
  69  *
  70  * <li>If the <code>environment</code> parameter to {@link
  71  * #newJMXConnector(JMXServiceURL, Map) newJMXConnector} contains the
  72  * key <code>jmx.remote.protocol.provider.pkgs</code> then the
  73  * associated value is the provider package list.
  74  *
  75  * <li>Otherwise, if the system property
  76  * <code>jmx.remote.protocol.provider.pkgs</code> exists, then its value
  77  * is the provider package list.
  78  *
  79  * <li>Otherwise, there is no provider package list.
  80  *
  81  * </ol>
  82  *
  83  * <p>The provider package list is a string that is interpreted as a
  84  * list of non-empty Java package names separated by vertical bars
  85  * (<code>|</code>).  If the string is empty, then so is the provider
  86  * package list.  If the provider package list is not a String, or if
  87  * it contains an element that is an empty string, a {@link
  88  * JMXProviderException} is thrown.</p>
  89  *
  90  * <p>If the provider package list exists and is not empty, then for
  91  * each element <code><em>pkg</em></code> of the list, the factory
  92  * will attempt to load the class
  93  *
  94  * <blockquote>
  95  * <code><em>pkg</em>.<em>protocol</em>.ClientProvider</code>
  96  * </blockquote>
  97 
  98  * <p>If the <code>environment</code> parameter to {@link
  99  * #newJMXConnector(JMXServiceURL, Map) newJMXConnector} contains the
 100  * key <code>jmx.remote.protocol.provider.class.loader</code> then the
 101  * associated value is the class loader to use to load the provider.
 102  * If the associated value is not an instance of {@link
 103  * java.lang.ClassLoader}, an {@link
 104  * java.lang.IllegalArgumentException} is thrown.</p>
 105  *
 106  * <p>If the <code>jmx.remote.protocol.provider.class.loader</code>
 107  * key is not present in the <code>environment</code> parameter, the
 108  * calling thread's context class loader is used.</p>
 109  *
 110  * <p>If the attempt to load this class produces a {@link
 111  * ClassNotFoundException}, the search for a handler continues with
 112  * the next element of the list.</p>
 113  *
 114  * <p>Otherwise, a problem with the provider found is signalled by a
 115  * {@link JMXProviderException} whose {@link
 116  * JMXProviderException#getCause() <em>cause</em>} indicates the underlying
 117  * exception, as follows:</p>
 118  *
 119  * <ul>
 120  *
 121  * <li>if the attempt to load the class produces an exception other
 122  * than <code>ClassNotFoundException</code>, that is the
 123  * <em>cause</em>;
 124  *
 125  * <li>if {@link Class#newInstance()} for the class produces an
 126  * exception, that is the <em>cause</em>.
 127  *
 128  * </ul>
 129  *
 130  * <p>If no provider is found by the above steps, including the
 131  * default case where there is no provider package list, then the
 132  * implementation will use its own provider for
 133  * <code><em>protocol</em></code>, or it will throw a
 134  * <code>MalformedURLException</code> if there is none.  An
 135  * implementation may choose to find providers by other means.  For
 136  * example, it may support the <a
 137  * href="{@docRoot}/../technotes/guides/jar/jar.html#Service Provider">
 138  * JAR conventions for service providers</a>, where the service
 139  * interface is <code>JMXConnectorProvider</code>.</p>
 140  *
 141  * <p>Every implementation must support the RMI connector protocol with
 142  * the default RMI transport, specified with string <code>rmi</code>.
 143  * An implementation may optionally support the RMI connector protocol
 144  * with the RMI/IIOP transport, specified with the string
 145  * <code>iiop</code>.</p>
 146  *
 147  * <p>Once a provider is found, the result of the
 148  * <code>newJMXConnector</code> method is the result of calling {@link
 149  * JMXConnectorProvider#newJMXConnector(JMXServiceURL,Map) newJMXConnector}
 150  * on the provider.</p>
 151  *
 152  * <p>The <code>Map</code> parameter passed to the
 153  * <code>JMXConnectorProvider</code> is a new read-only
 154  * <code>Map</code> that contains all the entries that were in the
 155  * <code>environment</code> parameter to {@link
 156  * #newJMXConnector(JMXServiceURL,Map)
 157  * JMXConnectorFactory.newJMXConnector}, if there was one.
 158  * Additionally, if the
 159  * <code>jmx.remote.protocol.provider.class.loader</code> key is not
 160  * present in the <code>environment</code> parameter, it is added to
 161  * the new read-only <code>Map</code>.  The associated value is the
 162  * calling thread's context class loader.</p>
 163  *
 164  * @since 1.5
 165  */
 166 public class JMXConnectorFactory {
 167 
 168     /**
 169      * <p>Name of the attribute that specifies the default class
 170      * loader. This class loader is used to deserialize return values and
 171      * exceptions from remote <code>MBeanServerConnection</code>
 172      * calls.  The value associated with this attribute is an instance
 173      * of {@link ClassLoader}.</p>
 174      */
 175     public static final String DEFAULT_CLASS_LOADER =
 176         "jmx.remote.default.class.loader";
 177 
 178     /**
 179      * <p>Name of the attribute that specifies the provider packages
 180      * that are consulted when looking for the handler for a protocol.
 181      * The value associated with this attribute is a string with
 182      * package names separated by vertical bars (<code>|</code>).</p>
 183      */
 184     public static final String PROTOCOL_PROVIDER_PACKAGES =
 185         "jmx.remote.protocol.provider.pkgs";
 186 
 187     /**
 188      * <p>Name of the attribute that specifies the class
 189      * loader for loading protocol providers.
 190      * The value associated with this attribute is an instance
 191      * of {@link ClassLoader}.</p>
 192      */
 193     public static final String PROTOCOL_PROVIDER_CLASS_LOADER =
 194         "jmx.remote.protocol.provider.class.loader";
 195 
 196     private static final String PROTOCOL_PROVIDER_DEFAULT_PACKAGE =
 197         "com.sun.jmx.remote.protocol";
 198 
 199     private static final ClassLogger logger =
 200         new ClassLogger("javax.management.remote.misc", "JMXConnectorFactory");
 201 
 202     /** There are no instances of this class.  */
 203     private JMXConnectorFactory() {
 204     }
 205 
 206     /**
 207      * <p>Creates a connection to the connector server at the given
 208      * address.</p>
 209      *
 210      * <p>This method is equivalent to {@link
 211      * #connect(JMXServiceURL,Map) connect(serviceURL, null)}.</p>
 212      *
 213      * @param serviceURL the address of the connector server to
 214      * connect to.
 215      *
 216      * @return a <code>JMXConnector</code> whose {@link
 217      * JMXConnector#connect connect} method has been called.
 218      *
 219      * @exception NullPointerException if <code>serviceURL</code> is null.
 220      *
 221      * @exception IOException if the connector client or the
 222      * connection cannot be made because of a communication problem.
 223      *
 224      * @exception SecurityException if the connection cannot be made
 225      * for security reasons.
 226      */
 227     public static JMXConnector connect(JMXServiceURL serviceURL)
 228             throws IOException {
 229         return connect(serviceURL, null);
 230     }
 231 
 232     /**
 233      * <p>Creates a connection to the connector server at the given
 234      * address.</p>
 235      *
 236      * <p>This method is equivalent to:</p>
 237      *
 238      * <pre>
 239      * JMXConnector conn = JMXConnectorFactory.newJMXConnector(serviceURL,
 240      *                                                         environment);
 241      * conn.connect(environment);
 242      * </pre>
 243      *
 244      * @param serviceURL the address of the connector server to connect to.
 245      *
 246      * @param environment a set of attributes to determine how the
 247      * connection is made.  This parameter can be null.  Keys in this
 248      * map must be Strings.  The appropriate type of each associated
 249      * value depends on the attribute.  The contents of
 250      * <code>environment</code> are not changed by this call.
 251      *
 252      * @return a <code>JMXConnector</code> representing the newly-made
 253      * connection.  Each successful call to this method produces a
 254      * different object.
 255      *
 256      * @exception NullPointerException if <code>serviceURL</code> is null.
 257      *
 258      * @exception IOException if the connector client or the
 259      * connection cannot be made because of a communication problem.
 260      *
 261      * @exception SecurityException if the connection cannot be made
 262      * for security reasons.
 263      */
 264     public static JMXConnector connect(JMXServiceURL serviceURL,
 265                                        Map<String,?> environment)
 266             throws IOException {
 267         if (serviceURL == null)
 268             throw new NullPointerException("Null JMXServiceURL");
 269         JMXConnector conn = newJMXConnector(serviceURL, environment);
 270         conn.connect(environment);
 271         return conn;
 272     }
 273 
 274     private static <K,V> Map<K,V> newHashMap() {
 275         return new HashMap<K,V>();
 276     }
 277 
 278     private static <K> Map<K,Object> newHashMap(Map<K,?> map) {
 279         return new HashMap<K,Object>(map);
 280     }
 281 
 282     /**
 283      * <p>Creates a connector client for the connector server at the
 284      * given address.  The resultant client is not connected until its
 285      * {@link JMXConnector#connect(Map) connect} method is called.</p>
 286      *
 287      * @param serviceURL the address of the connector server to connect to.
 288      *
 289      * @param environment a set of attributes to determine how the
 290      * connection is made.  This parameter can be null.  Keys in this
 291      * map must be Strings.  The appropriate type of each associated
 292      * value depends on the attribute.  The contents of
 293      * <code>environment</code> are not changed by this call.
 294      *
 295      * @return a <code>JMXConnector</code> representing the new
 296      * connector client.  Each successful call to this method produces
 297      * a different object.
 298      *
 299      * @exception NullPointerException if <code>serviceURL</code> is null.
 300      *
 301      * @exception IOException if the connector client cannot be made
 302      * because of a communication problem.
 303      *
 304      * @exception MalformedURLException if there is no provider for the
 305      * protocol in <code>serviceURL</code>.
 306      *
 307      * @exception JMXProviderException if there is a provider for the
 308      * protocol in <code>serviceURL</code> but it cannot be used for
 309      * some reason.
 310      */
 311     public static JMXConnector newJMXConnector(JMXServiceURL serviceURL,
 312                                                Map<String,?> environment)
 313             throws IOException {
 314 
 315         final Map<String,Object> envcopy;
 316         if (environment == null)
 317             envcopy = newHashMap();
 318         else {
 319             EnvHelp.checkAttributes(environment);
 320             envcopy = newHashMap(environment);
 321         }
 322 
 323         final ClassLoader loader = resolveClassLoader(envcopy);
 324         final Class<JMXConnectorProvider> targetInterface =
 325                 JMXConnectorProvider.class;
 326         final String protocol = serviceURL.getProtocol();
 327         final String providerClassName = "ClientProvider";
 328         final JMXServiceURL providerURL = serviceURL;
 329 
 330         JMXConnectorProvider provider = getProvider(providerURL, envcopy,
 331                                                providerClassName,
 332                                                targetInterface,
 333                                                loader);
 334 
 335         IOException exception = null;
 336         if (provider == null) {
 337             // Loader is null when context class loader is set to null
 338             // and no loader has been provided in map.
 339             // com.sun.jmx.remote.util.Service class extracted from j2se
 340             // provider search algorithm doesn't handle well null classloader.
 341             if (loader != null) {
 342                 try {
 343                     JMXConnector connection =
 344                         getConnectorAsService(loader, providerURL, envcopy);
 345                     if (connection != null)
 346                         return connection;
 347                 } catch (JMXProviderException e) {
 348                     throw e;
 349                 } catch (IOException e) {
 350                     exception = e;
 351                 }
 352             }
 353             provider = getProvider(protocol, PROTOCOL_PROVIDER_DEFAULT_PACKAGE,
 354                             JMXConnectorFactory.class.getClassLoader(),
 355                             providerClassName, targetInterface);
 356         }
 357 
 358         if (provider == null) {
 359             MalformedURLException e =
 360                 new MalformedURLException("Unsupported protocol: " + protocol);
 361             if (exception == null) {
 362                 throw e;
 363             } else {
 364                 throw EnvHelp.initCause(e, exception);
 365             }
 366         }
 367 
 368         final Map<String,Object> fixedenv =
 369                 Collections.unmodifiableMap(envcopy);
 370 
 371         return provider.newJMXConnector(serviceURL, fixedenv);
 372     }
 373 
 374     private static String resolvePkgs(Map<String, ?> env)
 375             throws JMXProviderException {
 376 
 377         Object pkgsObject = null;
 378 
 379         if (env != null)
 380             pkgsObject = env.get(PROTOCOL_PROVIDER_PACKAGES);
 381 
 382         if (pkgsObject == null)
 383             pkgsObject =
 384                 AccessController.doPrivileged(new PrivilegedAction<String>() {
 385                     public String run() {
 386                         return System.getProperty(PROTOCOL_PROVIDER_PACKAGES);
 387                     }
 388                 });
 389 
 390         if (pkgsObject == null)
 391             return null;
 392 
 393         if (!(pkgsObject instanceof String)) {
 394             final String msg = "Value of " + PROTOCOL_PROVIDER_PACKAGES +
 395                 " parameter is not a String: " +
 396                 pkgsObject.getClass().getName();
 397             throw new JMXProviderException(msg);
 398         }
 399 
 400         final String pkgs = (String) pkgsObject;
 401         if (pkgs.trim().equals(""))
 402             return null;
 403 
 404         // pkgs may not contain an empty element
 405         if (pkgs.startsWith("|") || pkgs.endsWith("|") ||
 406             pkgs.indexOf("||") >= 0) {
 407             final String msg = "Value of " + PROTOCOL_PROVIDER_PACKAGES +
 408                 " contains an empty element: " + pkgs;
 409             throw new JMXProviderException(msg);
 410         }
 411 
 412         return pkgs;
 413     }
 414 
 415     static <T> T getProvider(JMXServiceURL serviceURL,
 416                              final Map<String, Object> environment,
 417                              String providerClassName,
 418                              Class<T> targetInterface,
 419                              final ClassLoader loader)
 420             throws IOException {
 421 
 422         final String protocol = serviceURL.getProtocol();
 423 
 424         final String pkgs = resolvePkgs(environment);
 425 
 426         T instance = null;
 427 
 428         if (pkgs != null) {
 429             instance =
 430                 getProvider(protocol, pkgs, loader, providerClassName,
 431                             targetInterface);
 432 
 433             if (instance != null) {
 434                 boolean needsWrap = (loader != instance.getClass().getClassLoader());
 435                 environment.put(PROTOCOL_PROVIDER_CLASS_LOADER, needsWrap ? wrap(loader) : loader);
 436             }
 437         }
 438 
 439         return instance;
 440     }
 441 
 442     static <T> Iterator<T> getProviderIterator(final Class<T> providerClass,
 443                                                final ClassLoader loader) {
 444        ServiceLoader<T> serviceLoader =
 445                 ServiceLoader.load(providerClass, loader);
 446        return serviceLoader.iterator();
 447     }
 448 
 449     private static ClassLoader wrap(final ClassLoader parent) {
 450         return parent != null ? AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
 451             @Override
 452             public ClassLoader run() {
 453                 return new ClassLoader(parent) {
 454                     @Override
 455                     protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
 456                         ReflectUtil.checkPackageAccess(name);
 457                         return super.loadClass(name, resolve);
 458                     }
 459                 };
 460             }
 461         }) : null;
 462     }
 463 
 464     private static JMXConnector getConnectorAsService(ClassLoader loader,
 465                                                       JMXServiceURL url,
 466                                                       Map<String, ?> map)
 467         throws IOException {
 468 
 469         Iterator<JMXConnectorProvider> providers =
 470                 getProviderIterator(JMXConnectorProvider.class, loader);
 471         JMXConnector connection;
 472         IOException exception = null;
 473         while (providers.hasNext()) {
 474             JMXConnectorProvider provider = providers.next();
 475             try {
 476                 connection = provider.newJMXConnector(url, map);
 477                 return connection;
 478             } catch (JMXProviderException e) {
 479                 throw e;
 480             } catch (Exception e) {
 481                 if (logger.traceOn())
 482                     logger.trace("getConnectorAsService",
 483                                  "URL[" + url +
 484                                  "] Service provider exception: " + e);
 485                 if (!(e instanceof MalformedURLException)) {
 486                     if (exception == null) {
 487                         if (e instanceof IOException) {
 488                             exception = (IOException) e;
 489                         } else {
 490                             exception = EnvHelp.initCause(
 491                                 new IOException(e.getMessage()), e);
 492                         }
 493                     }
 494                 }
 495                 continue;
 496             }
 497         }
 498         if (exception == null)
 499             return null;
 500         else
 501             throw exception;
 502     }
 503 
 504     static <T> T getProvider(String protocol,
 505                               String pkgs,
 506                               ClassLoader loader,
 507                               String providerClassName,
 508                               Class<T> targetInterface)
 509             throws IOException {
 510 
 511         StringTokenizer tokenizer = new StringTokenizer(pkgs, "|");
 512 
 513         while (tokenizer.hasMoreTokens()) {
 514             String pkg = tokenizer.nextToken();
 515             String className = (pkg + "." + protocol2package(protocol) +
 516                                 "." + providerClassName);
 517             Class<?> providerClass;
 518             try {
 519                 providerClass = Class.forName(className, true, loader);
 520             } catch (ClassNotFoundException e) {
 521                 //Add trace.
 522                 continue;
 523             }
 524 
 525             if (!targetInterface.isAssignableFrom(providerClass)) {
 526                 final String msg =
 527                     "Provider class does not implement " +
 528                     targetInterface.getName() + ": " +
 529                     providerClass.getName();
 530                 throw new JMXProviderException(msg);
 531             }
 532 
 533             // We have just proved that this cast is correct
 534             Class<? extends T> providerClassT = Util.cast(providerClass);
 535             try {
 536                 return providerClassT.newInstance();
 537             } catch (Exception e) {
 538                 final String msg =
 539                     "Exception when instantiating provider [" + className +
 540                     "]";
 541                 throw new JMXProviderException(msg, e);
 542             }
 543         }
 544 
 545         return null;
 546     }
 547 
 548     static ClassLoader resolveClassLoader(Map<String, ?> environment) {
 549         ClassLoader loader = null;
 550 
 551         if (environment != null) {
 552             try {
 553                 loader = (ClassLoader)
 554                     environment.get(PROTOCOL_PROVIDER_CLASS_LOADER);
 555             } catch (ClassCastException e) {
 556                 final String msg =
 557                     "The ClassLoader supplied in the environment map using " +
 558                     "the " + PROTOCOL_PROVIDER_CLASS_LOADER +
 559                     " attribute is not an instance of java.lang.ClassLoader";
 560                 throw new IllegalArgumentException(msg);
 561             }
 562         }
 563 
 564         if (loader == null) {
 565             loader = Thread.currentThread().getContextClassLoader();
 566         }
 567 
 568         return loader;
 569     }
 570 
 571     private static String protocol2package(String protocol) {
 572         return protocol.replace('+', '.').replace('-', '_');
 573     }
 574 }