1 /*
   2  * Copyright 2007-2008 Sun Microsystems, Inc.  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.  Sun designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
  22  * CA 95054 USA or visit www.sun.com if you need additional information or
  23  * have any questions.
  24  */
  25 
  26 package sun.module.repository;
  27 
  28 import java.io.BufferedInputStream;
  29 import java.io.File;
  30 import java.io.FileInputStream;
  31 import java.io.IOException;
  32 import java.io.PrintWriter;
  33 import java.io.StringWriter;
  34 import java.lang.reflect.Constructor;
  35 import java.lang.reflect.InvocationTargetException;
  36 import java.module.Modules;
  37 import java.module.Repository;
  38 import java.net.MalformedURLException;
  39 import java.net.URI;
  40 import java.net.URL;
  41 import java.net.URLClassLoader;
  42 import java.net.URISyntaxException;
  43 import java.security.AccessController;
  44 import java.security.PrivilegedAction;
  45 import java.util.HashMap;
  46 import java.util.LinkedHashMap;
  47 import java.util.Map;
  48 import java.util.Properties;
  49 import sun.module.JamUtils;
  50 import sun.module.config.ModuleSystemConfig;
  51 import sun.security.util.PropertyExpander;
  52 
  53 /**
  54  * Establishes the configuration of a set of repositories in a running JVM.  A
  55  * configuration specifies a list of repositories, one of which is a child of
  56  * the bootstrap repository and the others are successive children.
  57  * <p>
  58  * Repositories can be configured automatically via configuration files, or by
  59  * setting a system property to the name of a configuration file.
  60  * <p>
  61  * The initial call to {@code getApplicationRepository} causes repositories to be
  62  * configured via a configuration file, with the system repository set to the
  63  * configured repository that is most distant (in the parent-child distance of
  64  * repository instances).
  65  * @since 1.7
  66  */
  67 public final class RepositoryConfig
  68 {
  69 
  70     static {
  71         // Setup "repository.system.home" system property if it doesn't exist
  72         AccessController.doPrivileged(new PrivilegedAction<Void>() {
  73             public Void run() {
  74                 if (System.getProperty("repository.system.home") == null) {
  75                     String platform = System.getProperty("os.platform");
  76                     String repositorySystemHome = null;
  77                     if (platform.equalsIgnoreCase("windows")) {
  78                         repositorySystemHome = "C:\\Windows\\Sun\\Java";
  79                     } else if (platform.equalsIgnoreCase("solaris")) {
  80                         repositorySystemHome = "/usr/java/packages";
  81                     } else if (platform.equalsIgnoreCase("linux")) {
  82                         repositorySystemHome = "/usr/jdk/packages";
  83                     }
  84                     if (repositorySystemHome != null) {
  85                         System.setProperty("repository.system.home", repositorySystemHome);
  86                     }
  87                 }
  88                 return null;
  89             }
  90         });
  91     }
  92 
  93     /** Application's repository. */
  94     private static Repository applicationRepository = Repository.getBootstrapRepository();
  95 
  96     /**
  97      * True if setApplicationRepository ever completes normally: this allows for
  98      * it being changed at most once from its default.
  99      */
 100     private static boolean applicationRepositoryWasSet = false;
 101 
 102     /** True once configRepositories has completed. */
 103     private static boolean configDone = false;
 104 
 105     /** True once the repositories are all initialized. */
 106     private static boolean initialized = false;
 107 
 108     /** System repository in the configuration. */
 109     private static Repository systemRepository;
 110 
 111     /** Attribute which designates the parent of a repository. */
 112     private static final String parentAttr = "parent";
 113 
 114     /**Attribute which designates the source location of a repository. */
 115     private static final String sourceAttr = "source";
 116 
 117     /**
 118      * Optional attribute which designates the class name of a repository.
 119      * Note: only one of "classname" or "factoryname" can be used for
 120      * configuring a particular repository.
 121      */
 122     private static final String classAttr = "classname";
 123 
 124     /**
 125      * Optional attribute which designates the class name of a factory that
 126      * can create a repository. Note: only one of "classname" or "factoryname"
 127      * can be used for configuring a particular repository.
 128      */
 129     private static final String factoryAttr = "factoryname";
 130 
 131     /**
 132      * Value of the parentAttr which specifies the repository that is to be
 133      * created as the imediate child of the bootstrap repository.
 134      */
 135     private static final String bootstrapValue = "bootstrap";
 136 
 137     /** Cache of RepositoryFactory instances. */
 138     private static final Map<String, RepositoryFactory> factories =
 139         new HashMap<String, RepositoryFactory>();
 140 
 141     /**
 142      * Sets the application repository.
 143      * @param r {@code Repository} that will be the application repository
 144      * @throws IllegalArgumentException if the application repository has already
 145      * been set via this method.
 146      */
 147     public static void setApplicationRepository(Repository r) throws IllegalArgumentException {
 148         if (applicationRepositoryWasSet) {
 149             throw new IllegalArgumentException("Application repository is already set.");
 150         } else {
 151             applicationRepository = r;
 152             applicationRepositoryWasSet = true;
 153         }
 154     }
 155 
 156     /**
 157      * Returns the current application repository, or if one has not been set (for
 158      * example, by the ModuleLauncher), creates it first.
 159      *
 160      * @param initializeAll initialize all repositories
 161      * @return the application repository
 162      */
 163     public static synchronized Repository getApplicationRepository(boolean initializeAll) {
 164         if (!configDone) {
 165             applicationRepository = configRepositories();
 166         }
 167         // XXX: Will revisit the bootstrapping issue
 168         //
 169         // During the VM startup, we can only initialize the repositories
 170         // provided by rt.jar (loaded by bootstrap class loader).
 171         // This method may be called when initializing the application class
 172         // loader.  Loading and initialize the OSGi and user-defined repositories
 173         // (classes not loaded by bootstrap class loader) may result
 174         // in a deadlock if loaded by the application class loader.
 175         //
 176         if (initializeAll && !initialized) {
 177             initialized = true;
 178             for (Repository r = applicationRepository; r != null; r = r.getParent()) {
 179                 try {
 180                     if (!r.isActive()) {
 181                         r.initialize();
 182                     }
 183                 } catch (IOException e) {
 184                     throw new AssertionError(e);
 185                 }
 186             }
 187         }
 188         return applicationRepository;
 189     }
 190 
 191     public static synchronized Repository getApplicationRepository() {
 192         return getApplicationRepository(true);
 193     }
 194 
 195     /**
 196      * Returns the repository specified by the repository configuration which
 197      * has the largest number of repositories between itself and the bootstrap
 198      * repository.
 199      * @return the repository configured to be farthest from the bootstrap
 200      * repository.
 201      */
 202     public static synchronized Repository getSystemRepository() {
 203         getApplicationRepository(); // Force initialization
 204         return systemRepository;
 205     }
 206 
 207     /**
 208      * Creates repositories as described in the repository.properties file.
 209      * The format of  the property file contents is described in {@link
 210      * #configRepositories(Properties configProps)}.
 211      * @return The repository in the configuration that is furthest in the
 212      * list from the bootstrap repository
 213      * @throws IllegalStateException if configuration has already been done
 214      * @throws RuntimeException if repositories cannot be configured
 215      */
 216     private static Repository configRepositories() throws RuntimeException {
 217         if (configDone) {
 218             throw new IllegalStateException("Repositories have already been configured.");
 219         }
 220 
 221         String location = ModuleSystemConfig.getRepositoryPropertiesFileName();
 222         Properties rp = new Properties();
 223         try {
 224             File f = new File(PropertyExpander.expand(location));
 225             if (f.exists() && f.canRead()) {
 226                 BufferedInputStream is = null;
 227                 try {
 228                     is = new BufferedInputStream(new FileInputStream(f));
 229                     rp.load(is);
 230                 } finally {
 231                     JamUtils.close(is);
 232                 }
 233             } else {
 234                 throw new RuntimeException(
 235                     "Cannot load repository properties from file " + f.getAbsolutePath());
 236             }
 237         } catch (PropertyExpander.ExpandException ex) {
 238             throw new RuntimeException(ex);
 239         } catch (IOException ex) {
 240             throw new RuntimeException(ex);
 241         }
 242 
 243         return configRepositories(rp);
 244     }
 245 
 246     /**
 247      * Creates repositories as described in the given properties.  Each
 248      * property has the form:
 249      * <pre>
 250      * repositoryName . attribute = value
 251      * </pre>
 252      * For each named repository, there can be several attribute/value pairs.
 253      * One and only one named repository must have the attribute
 254      * "parent" with the value "bootstrap": this indicates which repository
 255      * is to be created as the immediate child of the bootstrap repository.
 256      * Each other named repository must have a parent, and these must describe
 257      * a list of repositories.
 258      * <p>
 259      * Each repository must provide a attribute "source" which
 260      * indicates the source location of the repository.
 261      * <p>
 262      * Each repository can optionally provide a "classname" attribute, with a
 263      * corresponding value that indicates the name of the Class of the
 264      * repository, or a "factoryname"  attribute, which indicates the name of
 265      * a Class that can create a Repository.
 266      * <p>
 267      * If classname is given, then an instance of that named class will be
 268      * created. The repository class must have a constructor which has a
 269      * signature like this:
 270      * <pre>
 271      * FooRepository(Repository parent, String name, URL source, Map&lt;String, String&gt; config) throws IOException;
 272      * </pre>
 273      * <p>
 274      * If factoryname is given, an instance of that will be created (but only
 275      * once per JVM) and its {@code create} method used to create a repository
 276      * instance.  See {@link RepositoryFactory}.
 277      * <p>
 278      * If neither classname nor factoryname is given, an appropriate
 279      * repository will be created based on the source.  If source
 280      * is a file-based URL or a filename, a LocalRepository will be created.
 281      * If it is a non-file-based URL, a URLRepository will be created.
 282      * <p>
 283      * Other attributes are allowed, and are particular to the class of
 284      * repository that is created.  They are passed to the repository's
 285      * constructor in it's {@code Map<String, String> config} parameter.
 286      * @param configProps Properties that configure a list of repositories
 287      * @return The repository in the configuration that is furthest in the
 288      * list from the bootstrap repository.
 289      * @throws IllegalStateException if repositories have already been
 290      * configured.
 291      */
 292     private static Repository configRepositories(Properties configProps) {
 293         if (configDone) {
 294             throw new IllegalStateException("Repositories have already been configured.");
 295         }
 296 
 297         if (!configProps.isEmpty()) {
 298             LinkedHashMap<String, Map<String, String>> orderedConfig =
 299                 getConfigFromProps(configProps);
 300             systemRepository = createRepositories(orderedConfig);
 301             configDone = true;
 302         }
 303         return systemRepository;
 304     }
 305 
 306     /**
 307      * Examine the given properties to create a Map from repository name to
 308      * (repository property, repository value).  The returned Map is a {@code
 309      * LinkedHashMap} ordered such that the bootstrap repository is first.
 310      */
 311     private static LinkedHashMap<String, Map<String, String>> getConfigFromProps(
 312             Properties configProps) {
 313         Map<String, Map<String, String>> config
 314             = new HashMap<String, Map<String, String>>();
 315         boolean foundBootstrap = false;
 316 
 317         for (String name : configProps.stringPropertyNames()) {
 318             int dotPos = name.indexOf('.');
 319             String repoName = name.substring(0, dotPos);
 320             String repoKey = name.substring(dotPos +1);
 321             String repoValue = configProps.getProperty(name);
 322             try {
 323                 repoValue = PropertyExpander.expand(repoValue);
 324             } catch (PropertyExpander.ExpandException ex) {
 325                 throw new IllegalArgumentException(
 326                     "Invalid property value in repository configuration: '"
 327                     + repoValue + "'");
 328             }
 329             Map<String, String> repoConfig = config.get(repoName);
 330             if (repoConfig == null) {
 331                 repoConfig = new HashMap<String, String>();
 332                 config.put(repoName, repoConfig);
 333             }
 334             repoConfig.put(repoKey, repoValue);
 335             if (repoKey.equals(parentAttr) && repoValue.equals(bootstrapValue)) {
 336                 foundBootstrap = true;
 337             }
 338         }
 339 
 340         if (!foundBootstrap) {
 341             throw new IllegalArgumentException(
 342                 "RepositoryConfig: No repository specified as child of bootstrap repository");
 343         }
 344 
 345         /*
 346          * Sort config into orderedConfig, with bootstrap repository first,
 347          * then its child, then its grandchild, and so on.
 348          */
 349         LinkedHashMap<String, Map<String, String>> orderedConfig =
 350             new LinkedHashMap<String, Map<String, String>>();
 351         int size = config.size();
 352         String parentName = bootstrapValue;
 353         while (orderedConfig.size() < size) {
 354             boolean repoFound = false;
 355             for (String repoName : config.keySet()) {
 356                 Map<String, String> repoConfig = config.get(repoName);
 357                 String repoParent = repoConfig.get(parentAttr);
 358 
 359                 if (repoParent == null) {
 360                     throw new RuntimeException(
 361                         "Invalid repository configuration: no parent specified by repository '"
 362                         + repoName + "'");
 363                 }
 364 
 365                 if (!repoParent.equals(bootstrapValue) && config.get(repoParent) == null) {
 366                     throw new RuntimeException(
 367                         "Invalid repository configuration: missing parent specified for repository '"
 368                         + repoName + "'");
 369                 }
 370 
 371                 if (parentName.equals(repoParent)) {
 372                     orderedConfig.put(repoName, repoConfig);
 373                     parentName = repoName;
 374                     repoFound = true;
 375                     break;
 376                 }
 377             }
 378             if (!repoFound) {
 379                 throw new RuntimeException(
 380                     "Invalid repository configuration: no child repository found for parent repository '"
 381                     + parentName + "'");
 382             }
 383         }
 384         return orderedConfig;
 385     }
 386 
 387     /**
 388      * RepositoryCreator provides for creating instances of Repository
 389      * subclasses.  Each of its subclasses provides a different way of
 390      * specifying how to create a Repository.
 391      */
 392     private abstract static class RepositoryCreator {
 393         protected abstract Repository create(Repository parent, String repoName,
 394                                    String sourceName, Map<String, String> config)
 395                 throws IOException, InstantiationException, NoSuchMethodException,
 396                 ClassNotFoundException, IllegalAccessException,
 397                 InvocationTargetException, URISyntaxException;
 398     }
 399 
 400     /**
 401      * Creates a Local or URL repository, depending on whether the
 402      * source is a file or other URL .
 403      */
 404     private static class DefaultCreator extends RepositoryCreator {
 405         static final RepositoryCreator instance = new DefaultCreator();
 406         private DefaultCreator() { }
 407 
 408         protected Repository create(Repository parent, String repoName,
 409                           String sourceName, Map<String, String> config)
 410         throws IOException, InstantiationException, NoSuchMethodException,
 411                 ClassNotFoundException, IllegalAccessException,
 412                 InvocationTargetException {
 413             Repository rc = null;
 414             try {
 415                 URL u = new URL(sourceName);
 416                 if ("file".equals(u.getProtocol())) {
 417                     File f = new File(u.getFile()).getCanonicalFile();
 418                     rc = Modules.newLocalRepository(repoName, f, config, parent);
 419                 } else {
 420                     rc = Modules.newURLRepository(repoName, u, config, parent);
 421                 }
 422             } catch (MalformedURLException ex) {
 423                 File f = new File(sourceName).getCanonicalFile();
 424                 rc = Modules.newLocalRepository(repoName, f, config, parent);
 425             }
 426             return rc;
 427         }
 428     }
 429 
 430     /**
 431      * Creates a repository given the name of its class.
 432      */
 433     private static class ClassBasedCreator extends RepositoryCreator {
 434         static final ClassBasedCreator instance = new ClassBasedCreator();
 435         private ClassBasedCreator() { }
 436 
 437         /** Name of Repository subclass to create. */
 438         private String className;
 439 
 440         void setClassName(String name) {
 441             className = name;
 442         }
 443 
 444         protected Repository create(Repository parent, String repoName,
 445                           String sourceName, Map<String, String> config)
 446         throws IOException, InstantiationException, NoSuchMethodException,
 447                 ClassNotFoundException, IllegalAccessException,
 448                 InvocationTargetException, URISyntaxException {
 449             URI u = sourceName == null ? null : new URI(sourceName);
 450             Class<?> clazz = Class.forName(className);
 451             Constructor ctor = clazz.getDeclaredConstructor(
 452                 String.class, URI.class, Map.class,
 453                 Repository.class);
 454             return (Repository) ctor.newInstance(
 455                 repoName, u, config, parent);
 456         }
 457     }
 458 
 459     /**
 460      * Creates a repository given the name of a factory that can create it.
 461      */
 462     private static class FactoryBasedCreator extends RepositoryCreator {
 463         static final FactoryBasedCreator instance = new FactoryBasedCreator();
 464         private FactoryBasedCreator() { }
 465 
 466         /** Name of RepositoryFactory class to create Repository subclass. */
 467         private String factoryName;
 468 
 469         void setFactoryName(String name) {
 470             factoryName = name;
 471         }
 472 
 473         protected synchronized Repository create(Repository parent, String repoName,
 474                           String sourceName, Map<String, String> config)
 475         throws IOException, InstantiationException, NoSuchMethodException,
 476                 ClassNotFoundException, IllegalAccessException,
 477                 InvocationTargetException, URISyntaxException {
 478             URL u = sourceName == null ? null : new URL(sourceName);
 479             RepositoryFactory rf  = factories.get(factoryName);
 480             if (rf == null) {
 481                 Class<?> clazz = Class.forName(factoryName);
 482                 Constructor ctor = clazz.getDeclaredConstructor();
 483                 rf = (RepositoryFactory) ctor.newInstance();
 484                 factories.put(factoryName, rf);
 485             }
 486             return rf.create(parent, repoName, u, config);
 487         }
 488     }
 489 
 490     /**
 491      * Creates an OSGi repository
 492      */
 493     private static class OSGiRepositoryCreator extends RepositoryCreator {
 494         static final OSGiRepositoryCreator instance = new OSGiRepositoryCreator();
 495         private OSGiRepositoryCreator() { }
 496 
 497         /** Name of Repository subclass to create. */
 498         private String className = "sun.module.osgi.OSGiRepository";
 499         private final String repoJar = "file:///${java.home}/lib/osgi-repo.jar";
 500         private final String containerAttr = "container";
 501 
 502         void setClassName(String name) {
 503             className = name;
 504         }
 505         URLClassLoader getURLClassLoader(File container) throws IOException {
 506             URL[] urls = new URL[2];
 507             try {
 508                 urls[0] = new URL(PropertyExpander.expand(repoJar));
 509                 urls[1] = container.toURI().toURL();
 510             } catch (PropertyExpander.ExpandException ex) {
 511                 // XXX: should not reach here
 512                 ex.printStackTrace();
 513             }
 514             return URLClassLoader.newInstance(urls);
 515         }
 516 
 517         protected Repository create(Repository parent, String repoName,
 518                           String sourceName, Map<String, String> config)
 519         throws IOException, InstantiationException, NoSuchMethodException,
 520                 ClassNotFoundException, IllegalAccessException,
 521                 InvocationTargetException, URISyntaxException {
 522             URI u = sourceName == null ? null : new URI(sourceName);
 523             String containerPath = config.get(containerAttr);
 524             File container;
 525             try {
 526                 container = new File(new URI(containerPath));
 527             } catch (URISyntaxException e) {
 528                 container = new File(containerPath);
 529             }
 530             if (!container.exists()) {
 531                 throw new IOException("OSGi container \"" + container +
 532                     "\" does not exist");
 533             }
 534 
 535             Class<?> clazz = Class.forName(className, true, getURLClassLoader(container));
 536             Constructor ctor = clazz.getDeclaredConstructor(
 537                 String.class, URI.class, Repository.class, Map.class);
 538             return (Repository) ctor.newInstance(repoName, u, parent, config);
 539         }
 540     }
 541 
 542     /**
 543      * Creates repositories based on the given {@code LinkedHashMap}, whose
 544      * keys are presumed to be ordered such that the first entry describes the
 545      * repository to create as a child of the bootstrap repository, the next
 546      * to create as a child of that, and so on.
 547      * @return The repository in the configuration that is furthest in the
 548      * list from the bootstrap repository.
 549      */
 550     private synchronized static Repository createRepositories(
 551             LinkedHashMap<String, Map<String, String>> orderedConfig) {
 552 
 553         Repository parentRepo = Repository.getBootstrapRepository();
 554         for (String repoName : orderedConfig.keySet()) {
 555             Map<String, String> repoConfig = orderedConfig.get(repoName);
 556 
 557             String sourceName = repoConfig.get(sourceAttr);
 558 
 559             sourceName = sourceName.replace('\\', '/');
 560 
 561             Repository repo = null;
 562 
 563             String clazzName = repoConfig.get(classAttr);
 564             String factoryName = repoConfig.get(factoryAttr);
 565             if (clazzName != null && factoryName != null) {
 566                 throw new IllegalArgumentException(
 567                     "Cannot specify both classname and factoryname for '" + repoName);
 568             }
 569 
 570             RepositoryCreator creator = DefaultCreator.instance;
 571             if (repoName.equals("osgi")) {
 572                 creator = OSGiRepositoryCreator.instance;
 573             } else {
 574                 if (clazzName != null || factoryName != null) {
 575                     if (clazzName != null) {
 576                         ClassBasedCreator.instance.setClassName(clazzName);
 577                         creator = ClassBasedCreator.instance;
 578                     } else {
 579                         FactoryBasedCreator.instance.setFactoryName(factoryName);
 580                         creator = FactoryBasedCreator.instance;
 581                     }
 582                 }
 583             }
 584 
 585             try {
 586                 repo = creator.create(parentRepo, repoName, sourceName, repoConfig);
 587             } catch (Error ex) {
 588                 throw ex;
 589             } catch (RuntimeException ex) {
 590                 throw ex;
 591             } catch (InvocationTargetException ex) {
 592                 String msg = ex.getMessage();
 593                 Throwable t = ex.getCause();
 594                 if (t != null) {
 595                     String s = t.getMessage();
 596                     if (s != null) {
 597                         msg += "\nCause: " + t.getMessage();
 598                     }
 599                     StringWriter sw = new StringWriter();
 600                     PrintWriter pw = new PrintWriter(sw);
 601                     t.printStackTrace(pw);
 602                     pw.close();
 603                     msg += "\n" + sw.toString();
 604                 }
 605                 throw new IllegalArgumentException(
 606                     "Cannot create repository named '" + repoName + "' at location '" + sourceName
 607                     + "': " + msg);
 608             } catch (Exception ex) {
 609                 throw new IllegalArgumentException(
 610                     "Cannot create repository named '" + repoName + "' at location '" + sourceName
 611                     + "': " + ex, ex);
 612             }
 613 
 614             /*
 615              * If a repository was created, make it the parent for the next
 616              * one created (if any).  If a repository was *not* created, then
 617              * don't change the current parent.
 618              */
 619             if (repo != null) {
 620                 parentRepo = repo;
 621             }
 622         }
 623         return parentRepo;
 624     }
 625 }