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