1 /*
   2  * Copyright (c) 2009, 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 
  26 package org.openjdk.jigsaw;
  27 
  28 import java.lang.module.*;
  29 import java.io.*;
  30 import java.net.URI;
  31 import java.nio.channels.FileChannel;
  32 import java.nio.file.*;
  33 import java.nio.file.attribute.BasicFileAttributes;
  34 import java.security.*;
  35 import java.security.cert.*;
  36 import java.util.*;
  37 import java.util.jar.*;
  38 import java.util.zip.*;
  39 
  40 import static java.nio.file.StandardCopyOption.*;
  41 import static java.nio.file.StandardOpenOption.*;
  42 
  43 /**
  44  * A simple module library which stores data directly in the filesystem
  45  *
  46  * @see Library
  47  */
  48 
  49 // ## TODO: Move remaining parent-searching logic upward into Library class
  50 
  51 // On-disk library layout
  52 //
  53 //   $LIB/%jigsaw-library
  54 //        com.foo.bar/1.2.3/info (= module-info.class)
  55 //                          index (list of defined classes)
  56 //                          config (resolved configuration, if a root)
  57 //                          classes/com/foo/bar/...
  58 //                          resources/com/foo/bar/...
  59 //                          lib/libbar.so
  60 //                          bin/bar
  61 //                          signer (signer's certchain & timestamp)
  62 //
  63 // ## Issue: Concurrent access to the module library
  64 // ## e.g. a module is being removed while a running application
  65 // ## is depending on it
  66 
  67 public final class SimpleLibrary
  68     extends Library
  69 {
  70 
  71     private static abstract class MetaData {
  72 
  73         protected final int maxMajorVersion;
  74         protected final int maxMinorVersion;
  75         protected int majorVersion;
  76         protected int minorVersion;
  77         private final FileConstants.Type type;
  78         private final File file;
  79 
  80         protected MetaData(int maxMajor, int maxMinor,
  81                            FileConstants.Type t, File f)
  82         {
  83             maxMajorVersion = majorVersion = maxMajor;
  84             maxMinorVersion = minorVersion = maxMinor;
  85             type = t;
  86             file = f;
  87         }
  88 
  89         protected abstract void storeRest(DataOutputStream out)
  90             throws IOException;
  91 
  92         void store() throws IOException {
  93             try (OutputStream fos = new FileOutputStream(file);
  94                  BufferedOutputStream bos = new BufferedOutputStream(fos);
  95                  DataOutputStream out = new DataOutputStream(bos)) {
  96                 out.writeInt(FileConstants.MAGIC);
  97                 out.writeShort(type.value());
  98                 out.writeShort(majorVersion);
  99                 out.writeShort(minorVersion);
 100                 storeRest(out);
 101             }
 102         }
 103 
 104         protected abstract void loadRest(DataInputStream in)
 105             throws IOException;
 106 
 107         protected void load() throws IOException {
 108             try (InputStream fis = new FileInputStream(file);
 109                  BufferedInputStream bis = new BufferedInputStream(fis);
 110                  DataInputStream in = new DataInputStream(bis)) {
 111                 if (in.readInt() != FileConstants.MAGIC)
 112                     throw new IOException(file + ": Invalid magic number");
 113                 if (in.readShort() != type.value())
 114                     throw new IOException(file + ": Invalid file type");
 115                 int maj = in.readShort();
 116                 int min = in.readShort();
 117                 if (   maj > maxMajorVersion
 118                     || (maj == maxMajorVersion && min > maxMinorVersion)) {
 119                     throw new IOException(file
 120                                           + ": Futuristic version number");
 121                 }
 122                 majorVersion = maj;
 123                 minorVersion = min;
 124                 loadRest(in);
 125             } catch (EOFException x) {
 126                 throw new IOException(file + ": Invalid library metadata", x);
 127             }
 128         }
 129     }
 130 
 131     /**
 132      * Defines the storage options that SimpleLibrary supports.
 133      */
 134     public static enum StorageOption {
 135         DEFLATED,
 136     }
 137 
 138     private static final class Header
 139         extends MetaData
 140     {
 141         private static final String FILE
 142             = FileConstants.META_PREFIX + "jigsaw-library";
 143 
 144         private static final int MAJOR_VERSION = 0;
 145         private static final int MINOR_VERSION = 1;
 146 
 147         private static final int DEFLATED = 1 << 0;
 148 
 149         private File parent;
 150         // location of native libs for this library (may be outside the library)
 151         // null:default, to use a per-module 'lib' directory
 152         private File natlibs;
 153         // location of native cmds for this library (may be outside the library)
 154         // null:default, to use a per-module 'bin' directory
 155         private File natcmds;
 156         // location of config files for this library (may be outside the library)
 157         // null:default, to use a per-module 'etc' directory
 158         private File configs;
 159         private Set<StorageOption> opts;
 160 
 161         public File parent()  { return parent;  }
 162         public File natlibs() { return natlibs; }
 163         public File natcmds() { return natcmds; }
 164         public File configs() { return configs; }
 165         public boolean isDeflated() {
 166             return opts.contains(StorageOption.DEFLATED);
 167         }
 168 
 169         private Header(File root) {
 170             super(MAJOR_VERSION, MINOR_VERSION,
 171                   FileConstants.Type.LIBRARY_HEADER,
 172                   new File(root, FILE));
 173         }
 174 
 175         private Header(File root, File parent, File natlibs, File natcmds,
 176                        File configs, Set<StorageOption> opts) {
 177             this(root);
 178             this.parent = parent;
 179             this.natlibs = natlibs;
 180             this.natcmds = natcmds;
 181             this.configs = configs;
 182             this.opts = new HashSet<>(opts);
 183         }
 184 
 185         private void storePath(File p, DataOutputStream out) throws IOException {
 186             if (p != null) {
 187                 out.writeByte(1);
 188                 out.writeUTF(Files.convertSeparator(p.toString()));
 189             } else {
 190                 out.write(0);
 191             }
 192         }
 193 
 194         protected void storeRest(DataOutputStream out) throws IOException {
 195             int flags = 0;
 196             if (isDeflated())
 197                 flags |= DEFLATED;
 198             out.writeShort(flags);
 199 
 200             storePath(parent, out);
 201             storePath(natlibs, out);
 202             storePath(natcmds, out);
 203             storePath(configs, out);
 204         }
 205 
 206         private File loadPath(DataInputStream in) throws IOException {
 207             if (in.readByte() != 0)
 208                 return new File(Files.platformSeparator(in.readUTF()));
 209             return null;
 210         }
 211 
 212         protected void loadRest(DataInputStream in) throws IOException {
 213             opts = new HashSet<StorageOption>();
 214             int flags = in.readShort();
 215             if ((flags & DEFLATED) == DEFLATED)
 216                 opts.add(StorageOption.DEFLATED);
 217             parent = loadPath(in);
 218             natlibs = loadPath(in);
 219             natcmds = loadPath(in);
 220             configs = loadPath(in);
 221         }
 222 
 223         private static Header load(File f) throws IOException {
 224             Header h = new Header(f);
 225             h.load();
 226             return h;
 227         }
 228     }
 229 
 230     private final File root;
 231     private final File canonicalRoot;
 232     private final File parentPath;
 233     private final File natlibs;
 234     private final File natcmds;
 235     private final File configs;
 236     private final SimpleLibrary parent;
 237     private final Header hd;
 238     private final ModuleDictionary moduleDictionary;
 239     private final File lockf;
 240     private final File trash;
 241 
 242     public String name() { return root.toString(); }
 243     public File root() { return canonicalRoot; }
 244     public int majorVersion() { return hd.majorVersion; }
 245     public int minorVersion() { return hd.minorVersion; }
 246     public SimpleLibrary parent() { return parent; }
 247     public File natlibs() { return natlibs; }
 248     public File natcmds() { return natcmds; }
 249     public File configs() { return configs; }
 250     public boolean isDeflated() { return hd.isDeflated(); }
 251 
 252     private URI location = null;
 253     public URI location() {
 254         if (location == null)
 255             location = root().toURI();
 256         return location;
 257     }
 258 
 259     @Override
 260     public String toString() {
 261         return (this.getClass().getName()
 262                 + "[" + canonicalRoot
 263                 + ", v" + hd.majorVersion + "." + hd.minorVersion + "]");
 264     }
 265 
 266     private static File resolveAndEnsurePath(File path) throws IOException {
 267         if (path == null) { return null; }
 268 
 269         File p = path.getCanonicalFile();
 270         if (!p.exists()) {
 271             Files.mkdirs(p, p.toString());
 272         } else {
 273             Files.ensureIsDirectory(p);
 274             Files.ensureWriteable(p);
 275         }
 276         return p;
 277     }
 278 
 279     private File relativize(File path) throws IOException {
 280         if (path == null) { return null; }
 281         // Return the path relative to the canonical root
 282         return (canonicalRoot.toPath().relativize(path.toPath().toRealPath())).toFile();
 283     }
 284 
 285     // Opens an existing library
 286     private SimpleLibrary(File path) throws IOException {
 287         root = path;
 288         canonicalRoot = root.getCanonicalFile();
 289         Files.ensureIsDirectory(root);
 290         hd = Header.load(root);
 291 
 292         parentPath = hd.parent();
 293         parent = parentPath != null ? open(parentPath) : null;
 294 
 295         natlibs = hd.natlibs() == null ? null :
 296             new File(canonicalRoot, hd.natlibs().toString()).getCanonicalFile();
 297         natcmds = hd.natcmds() == null ? null :
 298             new File(canonicalRoot, hd.natcmds().toString()).getCanonicalFile();
 299         configs = hd.configs() == null ? null :
 300             new File(canonicalRoot, hd.configs().toString()).getCanonicalFile();
 301 
 302         lockf = new File(root, FileConstants.META_PREFIX + "lock");
 303         trash = new File(root, TRASH);
 304         moduleDictionary = new ModuleDictionary(root);
 305     }
 306 
 307     // Creates a new library
 308     private SimpleLibrary(File path, File parentPath, File natlibs, File natcmds,
 309                           File configs, Set<StorageOption> opts)
 310         throws IOException
 311     {
 312         root = path;
 313         canonicalRoot = root.getCanonicalFile();
 314         if (root.exists()) {
 315             Files.ensureIsDirectory(root);
 316             if (root.list().length != 0)
 317                 throw new IOException(root + ": Already Exists");
 318             Files.ensureWriteable(root);
 319         } else
 320             Files.mkdirs(root, root.toString());
 321 
 322         this.parent = parentPath != null ? open(parentPath) : null;
 323         this.parentPath = parentPath != null ? this.parent.root() : null;
 324 
 325         this.natlibs = resolveAndEnsurePath(natlibs);
 326         this.natcmds = resolveAndEnsurePath(natcmds);
 327         this.configs = resolveAndEnsurePath(configs);
 328 
 329         hd = new Header(canonicalRoot, this.parentPath, relativize(this.natlibs),
 330                         relativize(this.natcmds), relativize(this.configs), opts);
 331         hd.store();
 332 
 333         lockf = new File(root, FileConstants.META_PREFIX + "lock");
 334         lockf.createNewFile();
 335         trash = new File(root, TRASH);
 336         Files.mkdirs(trash, "module library trash");
 337         moduleDictionary = new ModuleDictionary(canonicalRoot);
 338         moduleDictionary.store();
 339     }
 340 
 341     public static SimpleLibrary create(File path, File parent, File natlibs,
 342                                        File natcmds, File configs,
 343                                        Set<StorageOption> opts)
 344         throws IOException
 345     {
 346         return new SimpleLibrary(path, parent, natlibs, natcmds, configs, opts);
 347     }
 348 
 349     public static SimpleLibrary create(File path, File parent, Set<StorageOption> opts)
 350         throws IOException
 351     {
 352         return new SimpleLibrary(path, parent, null, null, null, opts);
 353     }
 354 
 355     public static SimpleLibrary create(File path, File parent)
 356         throws IOException
 357     {
 358         return SimpleLibrary.create(path, parent, Collections.<StorageOption>emptySet());
 359     }
 360 
 361     public static SimpleLibrary create(File path, Set<StorageOption> opts)
 362         throws IOException
 363     {
 364         // ## Should default parent to $JAVA_HOME/lib/modules
 365         return SimpleLibrary.create(path, null, opts);
 366     }
 367 
 368     public static SimpleLibrary open(File path)
 369         throws IOException
 370     {
 371         return new SimpleLibrary(path);
 372     }
 373 
 374     private static final JigsawModuleSystem jms
 375         = JigsawModuleSystem.instance();
 376 
 377     private static final class Index
 378         extends MetaData
 379     {
 380 
 381         private static String FILE = "index";
 382 
 383         private static int MAJOR_VERSION = 0;
 384         private static int MINOR_VERSION = 1;
 385 
 386         private Set<String> publicClasses;
 387         public Set<String> publicClasses() { return publicClasses; }
 388 
 389         private Set<String> otherClasses;
 390         public Set<String> otherClasses() { return otherClasses; }
 391 
 392         private Index(File root) {
 393             super(MAJOR_VERSION, MINOR_VERSION,
 394                   FileConstants.Type.LIBRARY_MODULE_INDEX,
 395                   new File(root, FILE));
 396             // Unsorted on input, because we don't need it sorted
 397             publicClasses = new HashSet<String>();
 398             otherClasses = new HashSet<String>();
 399         }
 400 
 401         private void storeSet(Set<String> cnset, DataOutputStream out)
 402             throws IOException
 403         {
 404             // Sorted on output, because we can afford it
 405             List<String> cns = new ArrayList<String>(cnset);
 406             Collections.sort(cns);
 407             out.writeInt(cns.size());
 408             for (String cn : cns)
 409                 out.writeUTF(cn);
 410         }
 411 
 412         protected void storeRest(DataOutputStream out)
 413             throws IOException
 414         {
 415             storeSet(publicClasses, out);
 416             storeSet(otherClasses, out);
 417         }
 418 
 419         private void loadSet(DataInputStream in, Set<String> cnset)
 420             throws IOException
 421         {
 422             int n = in.readInt();
 423             for (int i = 0; i < n; i++)
 424                 cnset.add(in.readUTF());
 425         }
 426 
 427         protected void loadRest(DataInputStream in)
 428             throws IOException
 429         {
 430             loadSet(in, publicClasses);
 431             loadSet(in, otherClasses);
 432         }
 433 
 434         private static Index load(File f)
 435             throws IOException
 436         {
 437             Index ix = new Index(f);
 438             ix.load();
 439             return ix;
 440         }
 441 
 442     }
 443 
 444     private static final class StoredConfiguration
 445         extends MetaData
 446     {
 447 
 448         private static String FILE = "config";
 449 
 450         private static int MAJOR_VERSION = 0;
 451         private static int MINOR_VERSION = 1;
 452 
 453         private Configuration<Context> cf;
 454 
 455         private static void delete(File root) {
 456             new File(root, FILE).delete();
 457         }
 458 
 459         private StoredConfiguration(File root, Configuration<Context> conf)
 460         {
 461             super(MAJOR_VERSION, MINOR_VERSION,
 462                   FileConstants.Type.LIBRARY_MODULE_CONFIG,
 463                   new File(root, FILE));
 464             cf = conf;
 465         }
 466 
 467         protected void storeRest(DataOutputStream out)
 468             throws IOException
 469         {
 470             // Roots
 471             out.writeInt(cf.roots().size());
 472             for (ModuleId mid : cf.roots()) {
 473                 out.writeUTF(mid.toString());
 474             }
 475             // Contexts
 476             List<Context> contexts = new ArrayList<>(cf.contexts());
 477             out.writeInt(contexts.size());
 478             for (Context cx : contexts) {
 479                 out.writeUTF(cx.name());
 480             }
 481             for (Context cx : contexts) {
 482                 // Module ids, and their libraries
 483                 out.writeInt(cx.modules().size());
 484                 for (ModuleId mid : cx.modules()) {
 485                     out.writeUTF(mid.toString());
 486                     File lp = cx.findLibraryPathForModule(mid);
 487                     if (lp == null)
 488                         out.writeUTF("");
 489                     else
 490                         out.writeUTF(lp.toString());
 491 
 492                     // Module views
 493                     out.writeInt(cx.views(mid).size());
 494                     for (ModuleId id : cx.views(mid)) {
 495                         out.writeUTF(id.toString());
 496                     }
 497                 }
 498 
 499                 // Local class map
 500                 out.writeInt(cx.localClasses().size());
 501                 for (Map.Entry<String,ModuleId> me
 502                          : cx.moduleForLocalClassMap().entrySet()) {
 503                     out.writeUTF(me.getKey());
 504                     out.writeUTF(me.getValue().toString());
 505                 }
 506 
 507                 // Remote package map
 508                 out.writeInt(cx.contextForRemotePackageMap().size());
 509                 for (Map.Entry<String,String> me
 510                          : cx.contextForRemotePackageMap().entrySet()) {
 511                     out.writeUTF(me.getKey());
 512                     out.writeUTF(me.getValue());
 513                 }
 514 
 515                 // Suppliers
 516                 out.writeInt(cx.remoteContexts().size());
 517                 for (String cxn : cx.remoteContexts()) {
 518                     out.writeUTF(cxn);
 519                 }
 520 
 521                 // Local service implementations
 522                 Map<String,Set<String>> services = cx.services();
 523                 out.writeInt(services.size());
 524                 for (Map.Entry<String,Set<String>> me: services.entrySet()) {
 525                     out.writeUTF(me.getKey());
 526                     Set<String> values = me.getValue();
 527                     out.writeInt(values.size());
 528                     for (String value: values) {
 529                         out.writeUTF(value);
 530                     }
 531                 }
 532 
 533                 // Remote service suppliers
 534                 Map<String,Set<String>> serviceSuppliers = cx.serviceSuppliers();
 535                 out.writeInt(serviceSuppliers.size());
 536                 for (Map.Entry<String,Set<String>> entry: serviceSuppliers.entrySet()) {
 537                     out.writeUTF(entry.getKey());
 538                     Set<String> remotes = entry.getValue();
 539                     out.writeInt(remotes.size());
 540                     for (String rcxn: remotes) {
 541                         out.writeUTF(rcxn);
 542                     }
 543                 }
 544 
 545             }
 546         }
 547 
 548         protected void loadRest(DataInputStream in)
 549             throws IOException
 550         {
 551             // Roots
 552             int nRoots = in.readInt();
 553             List<ModuleId> roots = new ArrayList<>();
 554             for (int i = 0; i < nRoots; i++) {
 555                 String root = in.readUTF();
 556                 ModuleId rmid = jms.parseModuleId(root);
 557                 roots.add(rmid);
 558             }
 559             cf = new Configuration<Context>(roots);
 560             // Contexts
 561             int nContexts = in.readInt();
 562             List<String> contexts = new ArrayList<>(nContexts);
 563             for (int i = 0; i < nContexts; i++) {
 564                 contexts.add(in.readUTF());
 565             }
 566             for (String cxn : contexts) {
 567                 Context cx = new Context();
 568                 // Module ids
 569                 int nModules = in.readInt();
 570                 for (int j = 0; j < nModules; j++) {
 571                     ModuleId mid = jms.parseModuleId(in.readUTF());
 572                     String lps = in.readUTF();
 573                     if (lps.length() > 0)
 574                         cx.putLibraryPathForModule(mid, new File(lps));
 575                     // Module Views
 576                     int nViews = in.readInt();
 577                     Set<ModuleId> views = new HashSet<>();
 578                     for (int k = 0; k < nViews; k++) {
 579                         ModuleId id = jms.parseModuleId(in.readUTF());
 580                         views.add(id);
 581                         cf.put(id.name(), cx);
 582                     }
 583                     cx.add(mid, views);
 584                 }
 585                 cx.freeze();
 586                 assert cx.name().equals(cxn);
 587                 cf.add(cx);
 588                 // Local class map
 589                 int nClasses = in.readInt();
 590                 for (int j = 0; j < nClasses; j++)
 591                     cx.putModuleForLocalClass(in.readUTF(),
 592                                               jms.parseModuleId(in.readUTF()));
 593                 // Remote package map
 594                 int nPackages = in.readInt();
 595                 for (int j = 0; j < nPackages; j++)
 596                     cx.putContextForRemotePackage(in.readUTF(), in.readUTF());
 597 
 598                 // Suppliers
 599                 int nSuppliers = in.readInt();
 600                 for (int j = 0; j < nSuppliers; j++)
 601                     cx.addSupplier(in.readUTF());
 602 
 603                 // Local service implementations
 604                 int nServices = in.readInt();
 605                 for (int j = 0; j < nServices; j++) {
 606                     String sn = in.readUTF();
 607                     int nImpl = in.readInt();
 608                     for (int k = 0; k < nImpl; k++) {
 609                         String cn = in.readUTF();
 610                         cx.putService(sn, cn);
 611                     }
 612                 }
 613 
 614                 // Remote service suppliers
 615                 int nRemoteServices = in.readInt();
 616                 for (int j = 0; j < nRemoteServices; j++) {
 617                     String sn = in.readUTF();
 618                     int nRemotes = in.readInt();
 619                     for (int k = 0; k < nRemotes; k++) {
 620                         String rcxn = in.readUTF();
 621                         cx.addServiceSupplier(sn, rcxn);
 622                     }
 623                 }
 624             }
 625 
 626         }
 627 
 628         private static StoredConfiguration load(File f)
 629             throws IOException
 630         {
 631             StoredConfiguration sp = new StoredConfiguration(f, null);
 632             sp.load();
 633             return sp;
 634         }
 635 
 636     }
 637 
 638     private static final class Signers
 639         extends MetaData {
 640 
 641         private static final String FILE = "signer";
 642         private static final int MAJOR_VERSION = 0;
 643         private static final int MINOR_VERSION = 1;
 644         private static final String ENCODING = "PkiPath";
 645 
 646         private CertificateFactory cf;
 647         private Set<CodeSigner> signers;
 648         private Set<CodeSigner> signers() { return signers; }
 649 
 650         private Signers(File root, Set<CodeSigner> signers) {
 651             super(MAJOR_VERSION, MINOR_VERSION,
 652                   FileConstants.Type.LIBRARY_MODULE_SIGNER,
 653                   new File(root, FILE));
 654             this.signers = signers;
 655         }
 656 
 657         @Override
 658         protected void storeRest(DataOutputStream out)
 659             throws IOException
 660         {
 661             out.writeInt(signers.size());
 662             for (CodeSigner signer : signers) {
 663                 try {
 664                     CertPath signerCertPath = signer.getSignerCertPath();
 665                     out.write(signerCertPath.getEncoded(ENCODING));
 666                     Timestamp ts = signer.getTimestamp();
 667                     if (ts != null) {
 668                         out.writeByte(1);
 669                         out.writeLong(ts.getTimestamp().getTime());
 670                         out.write(ts.getSignerCertPath().getEncoded(ENCODING));
 671                     } else {
 672                         out.writeByte(0);
 673                     }
 674                 } catch (CertificateEncodingException cee) {
 675                     throw new IOException(cee);
 676                 }
 677             }
 678         }
 679 
 680         @Override
 681         protected void loadRest(DataInputStream in)
 682             throws IOException
 683         {
 684             int size = in.readInt();
 685             for (int i = 0; i < size; i++) {
 686                 try {
 687                     if (cf == null)
 688                         cf = CertificateFactory.getInstance("X.509");
 689                     CertPath signerCertPath = cf.generateCertPath(in, ENCODING);
 690                     int b = in.readByte();
 691                     if (b != 0) {
 692                         Date timestamp = new Date(in.readLong());
 693                         CertPath tsaCertPath = cf.generateCertPath(in, ENCODING);
 694                         Timestamp ts = new Timestamp(timestamp, tsaCertPath);
 695                         signers.add(new CodeSigner(signerCertPath, ts));
 696                     } else {
 697                         signers.add(new CodeSigner(signerCertPath, null));
 698                     }
 699                 } catch (CertificateException ce) {
 700                     throw new IOException(ce);
 701                 }
 702             }
 703         }
 704 
 705         private static Signers load(File f)
 706             throws IOException
 707         {
 708             Signers signers = new Signers(f, new HashSet<CodeSigner>());
 709             signers.load();
 710             return signers;
 711         }
 712     }
 713 
 714     protected void gatherLocalModuleIds(String moduleName,
 715                                         Set<ModuleId> mids)
 716         throws IOException
 717     {
 718         moduleDictionary.gatherLocalModuleIds(moduleName, mids);
 719     }
 720 
 721     protected void gatherLocalDeclaringModuleIds(Set<ModuleId> mids)
 722         throws IOException
 723     {
 724         mids.addAll(moduleDictionary.modules());
 725     }
 726 
 727     private void checkModuleId(ModuleId mid) {
 728         Version v = mid.version();
 729         if (v == null)
 730             return;
 731         if (!(v instanceof JigsawVersion))
 732             throw new IllegalArgumentException(mid + ": Not a Jigsaw module id");
 733     }
 734 
 735     private static File moduleDir(File root, ModuleId mid) {
 736         Version v = mid.version();
 737         String vs = (v != null) ? v.toString() : "default";
 738         return new File(new File(root, mid.name()), vs);
 739     }
 740 
 741     private static void checkModuleDir(File md)
 742         throws IOException
 743     {
 744         if (!md.isDirectory())
 745             throw new IOException(md + ": Not a directory");
 746         if (!md.canRead())
 747             throw new IOException(md + ": Not readable");
 748     }
 749 
 750     private File preinstallModuleDir(File dst, ModuleInfo mi) throws IOException {
 751         File md = moduleDir(dst, mi.id());
 752         if (md.exists()) {
 753             Files.deleteTree(md);
 754         }
 755         if (!md.mkdirs()) {
 756             throw new IOException(md + ": Cannot create");
 757         }
 758         return md;
 759     }
 760 
 761     public byte[] readLocalModuleInfoBytes(ModuleId mid)
 762         throws IOException
 763     {
 764         File md = moduleDictionary.findDeclaringModuleDir(mid);
 765         if (md == null)
 766             return null;
 767         return Files.load(new File(md, "info"));
 768     }
 769 
 770     @Override
 771     public CodeSigner[] readLocalCodeSigners(ModuleId mid)
 772         throws IOException
 773     {
 774         File md = moduleDictionary.findDeclaringModuleDir(mid);
 775         if (md == null)
 776             return null;
 777 
 778         File f = new File(md, "signer");
 779         // ## concurrency issues : what is the expected behavior if file is
 780         // ## removed by another thread/process here?
 781         if (!f.exists())
 782             return null;
 783         return Signers.load(md).signers().toArray(new CodeSigner[0]);
 784     }
 785 
 786     // ## Close all zip files when we close this library
 787     private Map<ModuleId, Object> contentForModule = new HashMap<>();
 788     private Object NONE = new Object();
 789 
 790     private Object findContent(ModuleId mid)
 791         throws IOException
 792     {
 793         ModuleId dmid = moduleDictionary.getDeclaringModule(mid);
 794         Object o = contentForModule.get(dmid);
 795         if (o == NONE)
 796             return null;
 797         if (o != null)
 798             return o;
 799         File md = moduleDictionary.findDeclaringModuleDir(dmid);
 800         if (md == null) {
 801             contentForModule.put(mid, NONE);
 802             return null;
 803         }
 804         File cf = new File(md, "classes");
 805         if (cf.isFile()) {
 806             ZipFile zf = new ZipFile(cf);
 807             contentForModule.put(mid, zf);
 808             return zf;
 809         }
 810         if (cf.isDirectory()) {
 811             contentForModule.put(mid, cf);
 812             return cf;
 813         }
 814         contentForModule.put(mid, NONE);
 815         return null;
 816     }
 817 
 818     private byte[] loadContent(ZipFile zf, String path)
 819         throws IOException
 820     {
 821         ZipEntry ze = zf.getEntry(path);
 822         if (ze == null)
 823             return null;
 824         return Files.load(zf.getInputStream(ze), (int)ze.getSize());
 825     }
 826 
 827     private byte[] loadContent(ModuleId mid, String path)
 828         throws IOException
 829     {
 830         Object o = findContent(mid);
 831         if (o == null)
 832             return null;
 833         if (o instanceof ZipFile) {
 834             ZipFile zf = (ZipFile)o;
 835             ZipEntry ze = zf.getEntry(path);
 836             if (ze == null)
 837                 return null;
 838             return Files.load(zf.getInputStream(ze), (int)ze.getSize());
 839         }
 840         if (o instanceof File) {
 841             File f = new File((File)o, path);
 842             if (!f.exists())
 843                 return null;
 844             return Files.load(f);
 845         }
 846         assert false;
 847         return null;
 848     }
 849 
 850     private URI locateContent(ModuleId mid, String path)
 851         throws IOException
 852     {
 853         Object o = findContent(mid);
 854         if (o == null)
 855             return null;
 856         if (o instanceof ZipFile) {
 857             ZipFile zf = (ZipFile)o;
 858             ZipEntry ze = zf.getEntry(path);
 859             if (ze == null)
 860                 return null;
 861             return URI.create("jar:"
 862                               + new File(zf.getName()).toURI().toString()
 863                               + "!/" + path);
 864         }
 865         if (o instanceof File) {
 866             File f = new File((File)o, path);
 867             if (!f.exists())
 868                 return null;
 869             return f.toURI();
 870         }
 871         return null;
 872     }
 873 
 874     public byte[] readLocalClass(ModuleId mid, String className)
 875         throws IOException
 876     {
 877         return loadContent(mid, className.replace('.', '/') + ".class");
 878     }
 879 
 880     public List<String> listLocalClasses(ModuleId mid, boolean all)
 881         throws IOException
 882     {
 883         File md = moduleDictionary.findDeclaringModuleDir(mid);
 884         if (md == null)
 885             return null;
 886         Index ix = Index.load(md);
 887         int os = all ? ix.otherClasses().size() : 0;
 888         ArrayList<String> cns
 889             = new ArrayList<String>(ix.publicClasses().size() + os);
 890         cns.addAll(ix.publicClasses());
 891         if (all)
 892             cns.addAll(ix.otherClasses());
 893         return cns;
 894     }
 895 
 896     public Configuration<Context> readConfiguration(ModuleId mid)
 897         throws IOException
 898     {
 899         File md = moduleDictionary.findDeclaringModuleDir(mid);
 900         if (md == null) {
 901             if (parent != null) {
 902                 return parent.readConfiguration(mid);
 903             }
 904             return null;
 905         }
 906         StoredConfiguration scf = StoredConfiguration.load(md);
 907         return scf.cf;
 908     }
 909 
 910     private boolean addToIndex(ClassInfo ci, Index ix)
 911         throws IOException
 912     {
 913         if (ci.isModuleInfo())
 914             return false;
 915         if (ci.moduleName() != null) {
 916             // ## From early Jigsaw development; can probably delete now
 917             throw new IOException("Old-style class file with"
 918                                   + " module attribute");
 919         }
 920         if (ci.isPublic())
 921             ix.publicClasses().add(ci.name());
 922         else
 923             ix.otherClasses().add(ci.name());
 924         return true;
 925     }
 926 
 927     private void reIndex(ModuleId mid)
 928         throws IOException
 929     {
 930 
 931         File md = moduleDictionary.findDeclaringModuleDir(mid);
 932         if (md == null)
 933             throw new IllegalArgumentException(mid + ": No such module");
 934         File cd = new File(md, "classes");
 935         final Index ix = new Index(md);
 936 
 937         if (cd.isDirectory()) {
 938             Files.walkTree(cd, new Files.Visitor<File>() {
 939                 public void accept(File f) throws IOException {
 940                     if (f.getPath().endsWith(".class"))
 941                         addToIndex(ClassInfo.read(f), ix);
 942                 }
 943             });
 944         } else if (cd.isFile()) {
 945             try (FileInputStream fis = new FileInputStream(cd);
 946                  ZipInputStream zis = new ZipInputStream(fis))
 947             {
 948                 ZipEntry ze;
 949                 while ((ze = zis.getNextEntry()) != null) {
 950                     if (!ze.getName().endsWith(".class"))
 951                         continue;
 952                     addToIndex(ClassInfo.read(Files.nonClosingStream(zis),
 953                                               ze.getSize(),
 954                                               mid + ":" + ze.getName()),
 955                                ix);
 956                 }
 957             }
 958         }
 959 
 960         ix.store();
 961     }
 962 
 963     /**
 964      * Strip the debug attributes from the classes in a given module
 965      * directory.
 966      */
 967     private void strip(File md) throws IOException {
 968         File classes = new File(md, "classes");
 969         if (classes.isFile()) {
 970             File pf = new File(md, "classes.pack");
 971             try (JarFile jf = new JarFile(classes);
 972                 FileOutputStream out = new FileOutputStream(pf))
 973             {
 974                 Pack200.Packer packer = Pack200.newPacker();
 975                 Map<String,String> p = packer.properties();
 976                 p.put("com.sun.java.util.jar.pack.strip.debug", Pack200.Packer.TRUE);
 977                 packer.pack(jf, out);
 978             }
 979 
 980             try (OutputStream out = new FileOutputStream(classes);
 981                  JarOutputStream jos = new JarOutputStream(out))
 982             {
 983                 Pack200.Unpacker unpacker = Pack200.newUnpacker();
 984                 unpacker.unpack(pf, jos);
 985             } finally {
 986                 pf.delete();
 987            }
 988         }
 989     }
 990 
 991     private List<Path> listFiles(Path dir) throws IOException {
 992         final List<Path> files = new ArrayList<>();
 993         java.nio.file.Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
 994             @Override
 995             public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
 996                 throws IOException
 997             {
 998                 if (!file.endsWith("module-info.class"))
 999                     files.add(file);
1000 
1001                 return FileVisitResult.CONTINUE;
1002             }
1003         });
1004         return files;
1005     }
1006 
1007     private ModuleId installWhileLocked(Manifest mf, File dst, boolean strip)
1008         throws IOException
1009     {
1010         if (mf.classes().size() > 1)
1011             throw new IllegalArgumentException("Multiple module-class"
1012                                                + " directories"
1013                                                + " not yet supported");
1014         if (mf.classes().size() < 1)
1015             throw new IllegalArgumentException("At least one module-class"
1016                                                + " directory required");
1017         File classes = mf.classes().get(0);
1018         final String mn = mf.module();
1019 
1020         File mif = new File(classes, "module-info.class");
1021         File src = null;
1022         if (mif.exists()) {
1023             src = classes;
1024         } else {
1025             src = new File(classes, mn);
1026             mif = new File(src, "module-info.class");
1027         }
1028         byte[] bs =  Files.load(mif);
1029         ModuleInfo mi = jms.parseModuleInfo(bs);
1030         if (!mi.id().name().equals(mn)) {
1031             // ## Need a more appropriate throwable here
1032             throw new Error(mif + " is for module " + mi.id().name()
1033                             + ", not " + mn);
1034         }
1035         String m = mi.id().name();
1036         JigsawVersion v = (JigsawVersion)mi.id().version();
1037         String vs = (v == null) ? "default" : v.toString();
1038 
1039         try {
1040             File mdst;
1041             if (dst.equals(root)) {
1042                 mdst = moduleDictionary.add(mi);
1043             } else {
1044                 mdst = preinstallModuleDir(dst, mi);
1045             }
1046             Files.store(bs, new File(mdst, "info"));
1047             File cldst = new File(mdst, "classes");
1048 
1049             // Delete the config file, if one exists
1050             StoredConfiguration.delete(mdst);
1051 
1052             if (false) {
1053 
1054                 // ## Retained for now in case we later want to add an option
1055                 // ## to install into a tree rather than a zip file
1056 
1057                 // Copy class files and build index
1058                 final Index ix = new Index(mdst);
1059                 Files.copyTree(src, cldst, new Files.Filter<File>() {
1060 
1061                     public boolean accept(File f) throws IOException {
1062                         if (f.isDirectory())
1063                             return true;
1064                         if (f.getName().endsWith(".class")) {
1065                             return addToIndex(ClassInfo.read(f), ix);
1066                         } else {
1067                             return true;
1068                         }
1069                     }
1070                 });
1071                 ix.store();
1072             } else {
1073                 // Copy class/resource files and build index
1074                 Index ix = new Index(mdst);
1075                 Path srcPath = src.toPath();
1076                 List<Path> files = listFiles(srcPath);
1077 
1078                 if (!files.isEmpty()) {
1079                     try (FileOutputStream fos = new FileOutputStream(new File(mdst, "classes"));
1080                          JarOutputStream jos = new JarOutputStream(new BufferedOutputStream(fos)))
1081                     {
1082                         boolean deflate = isDeflated();
1083                         for (Path path : files) {
1084                             File file = path.toFile();
1085                             String jp = Files.convertSeparator(srcPath.relativize(path).toString());
1086                             try (OutputStream out = Files.newOutputStream(jos, deflate, jp)) {
1087                                 java.nio.file.Files.copy(path, out);
1088                             }
1089                             if (file.getName().endsWith(".class"))
1090                                 addToIndex(ClassInfo.read(file), ix);
1091                         }
1092                     }
1093                 }
1094                 ix.store();
1095                 if (strip) {
1096                     strip(mdst);
1097                 }
1098             }
1099         } catch (ConfigurationException x) {
1100             // module already exists
1101             throw new IOException(x);
1102         } catch (IOException x) {
1103             try {
1104                 moduleDictionary.remove(mi);
1105             } catch (IOException y) {
1106                 x.addSuppressed(y);
1107             }
1108             throw x;
1109         }
1110         return mi.id();
1111     }
1112 
1113     public void installFromManifests(Collection<Manifest> mfs, boolean strip)
1114         throws ConfigurationException, IOException
1115     {
1116         boolean complete = false;
1117         List<ModuleId> mids = new ArrayList<>();
1118         FileChannel fc = FileChannel.open(lockf.toPath(), WRITE);
1119         try {
1120             fc.lock();
1121             moduleDictionary.load();
1122             for (Manifest mf : mfs) {
1123                 mids.add(installWhileLocked(mf, root, strip));
1124             }
1125             configureWhileModuleDirectoryLocked(null);
1126             complete = true;
1127         } catch (ConfigurationException | IOException x) {
1128             try {
1129                 for (ModuleId mid : mids) {
1130                     ModuleInfo mi = readLocalModuleInfo(mid);
1131                     if (mi != null) {
1132                         moduleDictionary.remove(mi);
1133                     }
1134                 }
1135             } catch (IOException y) {
1136                 x.addSuppressed(y);
1137             }
1138             throw x;
1139         } finally {
1140             if (complete) {
1141                 moduleDictionary.store();
1142             }
1143             fc.close();
1144         }
1145     }
1146 
1147     @Override
1148     public void installFromManifests(Collection<Manifest> mfs)
1149         throws ConfigurationException, IOException
1150     {
1151         installFromManifests(mfs, false);
1152     }
1153 
1154     private ModuleId installWhileLocked(InputStream is, boolean verifySignature,
1155                                         boolean strip)
1156         throws ConfigurationException, IOException, SignatureException
1157     {
1158         BufferedInputStream bin = new BufferedInputStream(is);
1159         DataInputStream in = new DataInputStream(bin);
1160         ModuleInfo mi = null;
1161         try (ModuleFile.Reader mr = new ModuleFile.Reader(in)) {
1162             byte[] mib = mr.readStart();
1163             ModuleInfo moduleInfo= jms.parseModuleInfo(mib);
1164             File md = moduleDictionary.add(moduleInfo);
1165             mi = moduleInfo;
1166             if (verifySignature && mr.hasSignature()) {
1167                 // Verify the module signature
1168                 SignedModule sm = new SignedModule(mr);
1169                 Set<CodeSigner> signers = sm.verifySignature();
1170 
1171                 // Validate the signers
1172                 try {
1173                     SignedModule.validateSigners(signers);
1174                 } catch (CertificateException x) {
1175                     throw new SignatureException(x);
1176                 }
1177 
1178                 // ## TODO: Check policy and determine if signer is trusted
1179                 // ## and what permissions should be granted.
1180                 // ## If there is no policy entry, show signers and prompt
1181                 // ## user to accept before proceeding.
1182 
1183                 // Verify the module header hash and the module info hash
1184                 sm.verifyHashesStart();
1185 
1186                 // Read the rest of the hashes
1187                 mr.readRest(md, isDeflated(), natlibs(), natcmds(), configs());
1188 
1189                 // Verify the rest of the hashes
1190                 sm.verifyHashesRest();
1191 
1192                 // Store signer info
1193                 new Signers(md, signers).store();
1194             } else {
1195                 mr.readRest(md, isDeflated(), natlibs(), natcmds(), configs());
1196             }
1197 
1198             if (strip)
1199                 strip(md);
1200             reIndex(mi.id());         // ## Could do this while reading module file
1201 
1202             return mi.id();
1203 
1204         } catch (ConfigurationException | IOException | SignatureException x) {
1205             if (mi != null) {
1206                 try {
1207                     moduleDictionary.remove(mi);
1208                 } catch (IOException y) {
1209                     x.addSuppressed(y);
1210                 }
1211             }
1212             throw x;
1213         }
1214     }
1215 
1216     private ModuleId installFromJarFile(File mf, boolean verifySignature, boolean strip)
1217         throws ConfigurationException, IOException, SignatureException
1218     {
1219         ModuleInfo mi = null;
1220         try (JarFile jf = new JarFile(mf, verifySignature)) {
1221             ModuleInfo moduleInfo = jf.getModuleInfo();
1222             if (moduleInfo == null)
1223                 throw new ConfigurationException(mf + ": not a modular JAR file");
1224 
1225             File md = moduleDictionary.add(moduleInfo);
1226             mi = moduleInfo;
1227             ModuleId mid = mi.id();
1228 
1229             boolean signed = false;
1230 
1231             // copy the jar file to the module library
1232             File classesDir = new File(md, "classes");
1233             try (FileOutputStream fos = new FileOutputStream(classesDir);
1234                  BufferedOutputStream bos = new BufferedOutputStream(fos);
1235                  JarOutputStream jos = new JarOutputStream(bos)) {
1236                 jos.setLevel(0);
1237 
1238                 Enumeration<JarEntry> entries = jf.entries();
1239                 while (entries.hasMoreElements()) {
1240                     JarEntry je = entries.nextElement();
1241                     try (InputStream is = jf.getInputStream(je)) {
1242                         if (je.getName().equals(JarFile.MODULEINFO_NAME)) {
1243                             java.nio.file.Files.copy(is, md.toPath().resolve("info"));
1244                         } else {
1245                             writeJarEntry(is, je, jos);
1246                         }
1247                     }
1248                     if (!signed) {
1249                         String name = je.getName().toUpperCase(Locale.ENGLISH);
1250                         signed = name.startsWith("META-INF/")
1251                                  && name.endsWith(".SF");
1252                     }
1253                 }
1254             }
1255 
1256             try {
1257                 if (verifySignature && signed) {
1258                     // validate the code signers
1259                     Set<CodeSigner> signers = getSigners(jf);
1260                     SignedModule.validateSigners(signers);
1261                     // store the signers
1262                     new Signers(md, signers).store();
1263                 }
1264             } catch (CertificateException ce) {
1265                 throw new SignatureException(ce);
1266             }
1267 
1268             if (strip)
1269                 strip(md);
1270             reIndex(mid);
1271 
1272             return mid;
1273         } catch (ConfigurationException | IOException | SignatureException x) {
1274             if (mi != null) {
1275                 try {
1276                     moduleDictionary.remove(mi);
1277                 } catch (IOException y) {
1278                     x.addSuppressed(y);
1279                 }
1280             }
1281             throw x;
1282         }
1283     }
1284 
1285     /**
1286      * Returns the set of signers of the specified jar file. Each signer
1287      * must have signed all relevant entries.
1288      */
1289     private static Set<CodeSigner> getSigners(JarFile jf)
1290         throws SignatureException
1291     {
1292         Set<CodeSigner> signers = new HashSet<>();
1293         Enumeration<JarEntry> entries = jf.entries();
1294         while (entries.hasMoreElements()) {
1295             JarEntry je = entries.nextElement();
1296             String name = je.getName().toUpperCase(Locale.ENGLISH);
1297             if (name.endsWith("/") || isSigningRelated(name))
1298                 continue;
1299 
1300             // A signed modular jar can be signed by multiple signers.
1301             // However, all entries must be signed by each of these signers.
1302             // Signers that only sign a subset of entries are ignored.
1303             CodeSigner[] jeSigners = je.getCodeSigners();
1304             if (jeSigners == null || jeSigners.length == 0)
1305                 throw new SignatureException("Found unsigned entry in "
1306                                              + "signed modular JAR");
1307 
1308             Set<CodeSigner> jeSignerSet =
1309                 new HashSet<>(Arrays.asList(jeSigners));
1310             if (signers.isEmpty())
1311                 signers.addAll(jeSignerSet);
1312             else if (signers.retainAll(jeSignerSet) && signers.isEmpty())
1313                 throw new SignatureException("No signers in common in "
1314                                              + "signed modular JAR");
1315         }
1316         return signers;
1317     }
1318 
1319     // true if file is part of the signature mechanism itself
1320     private static boolean isSigningRelated(String name) {
1321         if (!name.startsWith("META-INF/")) {
1322             return false;
1323         }
1324         name = name.substring(9);
1325         if (name.indexOf('/') != -1) {
1326             return false;
1327         }
1328         if (name.endsWith(".DSA") ||
1329             name.endsWith(".RSA") ||
1330             name.endsWith(".SF")  ||
1331             name.endsWith(".EC")  ||
1332             name.startsWith("SIG-") ||
1333             name.equals("MANIFEST.MF")) {
1334             return true;
1335         }
1336         return false;
1337     }
1338 
1339     private void writeJarEntry(InputStream is, JarEntry je, JarOutputStream jos)
1340         throws IOException, SignatureException
1341     {
1342         JarEntry entry = new JarEntry(je.getName());
1343         entry.setMethod(isDeflated() ? ZipEntry.DEFLATED : ZipEntry.STORED);
1344         entry.setTime(je.getTime());
1345         try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
1346             int size = 0;
1347             byte[] bs = new byte[1024];
1348             int cc = 0;
1349             // This will throw a SecurityException if a signature is invalid.
1350             while ((cc = is.read(bs)) > 0) {
1351                 baos.write(bs, 0, cc);
1352                 size += cc;
1353             }
1354             if (!isDeflated()) {
1355                 entry.setSize(size);
1356                 entry.setCrc(je.getCrc());
1357                 entry.setCompressedSize(size);
1358             }
1359             jos.putNextEntry(entry);
1360             if (baos.size() > 0)
1361                 baos.writeTo(jos);
1362             jos.closeEntry();
1363         } catch (SecurityException se) {
1364             throw new SignatureException(se);
1365         }
1366     }
1367 
1368     private ModuleId installWhileLocked(File mf, boolean verifySignature, boolean strip)
1369         throws ConfigurationException, IOException, SignatureException
1370     {
1371         if (mf.getName().endsWith(".jar"))
1372             return installFromJarFile(mf, verifySignature, strip);
1373         else {
1374             // Assume jmod file
1375             try (FileInputStream in = new FileInputStream(mf)) {
1376                 return installWhileLocked(in, verifySignature, strip);
1377             }
1378         }
1379     }
1380 
1381     public void install(Collection<File> mfs, boolean verifySignature, boolean strip)
1382         throws ConfigurationException, IOException, SignatureException
1383     {
1384         List<ModuleId> mids = new ArrayList<>();
1385         boolean complete = false;
1386         FileChannel fc = FileChannel.open(lockf.toPath(), WRITE);
1387         try {
1388             fc.lock();
1389             moduleDictionary.load();
1390             for (File mf : mfs)
1391                 mids.add(installWhileLocked(mf, verifySignature, strip));
1392             configureWhileModuleDirectoryLocked(mids);
1393             complete = true;
1394         } catch (ConfigurationException | IOException | SignatureException x) {
1395             try {
1396                 for (ModuleId mid : mids) {
1397                     ModuleInfo mi = readLocalModuleInfo(mid);
1398                     if (mi != null) {
1399                         moduleDictionary.remove(mi);
1400                     }
1401                 }
1402             } catch (IOException y) {
1403                 x.addSuppressed(y);
1404             }
1405             throw x;
1406         } finally {
1407             if (complete) {
1408                 moduleDictionary.store();
1409             }
1410             fc.close();
1411         }
1412     }
1413 
1414     @Override
1415     public void install(Collection<File> mfs, boolean verifySignature)
1416         throws ConfigurationException, IOException, SignatureException
1417     {
1418         install(mfs, verifySignature, false);
1419     }
1420 
1421     // Public entry point, since the Resolver itself is package-private
1422     //
1423     public Resolution resolve(Collection<ModuleIdQuery> midqs)
1424         throws ConfigurationException, IOException
1425     {
1426         try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) {
1427             fc.lock();
1428             return Resolver.run(this, midqs);
1429         }
1430     }
1431 
1432     public void install(Resolution res, boolean verifySignature, boolean strip)
1433         throws ConfigurationException, IOException, SignatureException
1434     {
1435         boolean complete = false;
1436         FileChannel fc = FileChannel.open(lockf.toPath(), WRITE);
1437         try {
1438             fc.lock();
1439             moduleDictionary.load();
1440 
1441             // ## Handle case of installing multiple root modules
1442             assert res.rootQueries.size() == 1;
1443             ModuleIdQuery midq = res.rootQueries.iterator().next();
1444             ModuleInfo root = null;
1445             for (String mn : res.moduleViewForName.keySet()) {
1446                 ModuleView mv = res.moduleViewForName.get(mn);
1447                 if (midq.matches(mv.id())) {
1448                     root = mv.moduleInfo();
1449                     break;
1450                 }
1451             }
1452             assert root != null;
1453 
1454             // Download
1455             //
1456             for (ModuleId mid : res.modulesNeeded()) {
1457                 URI u = res.locationForName.get(mid.name());
1458                 assert u != null;
1459                 RemoteRepository rr = repositoryList().firstRepository();
1460                 assert rr != null;
1461                 installWhileLocked(rr.fetch(mid), verifySignature, strip);
1462                 res.locationForName.put(mid.name(), location());
1463                 // ## If something goes wrong, delete all our modules
1464             }
1465 
1466             // Configure
1467             //
1468             configureWhileModuleDirectoryLocked(res.modulesNeeded());
1469             complete = true;
1470         } catch (ConfigurationException | IOException | SignatureException x) {
1471             try {
1472                 for (ModuleId mid : res.modulesNeeded()) {
1473                     ModuleInfo mi = readLocalModuleInfo(mid);
1474                     if (mi != null) {
1475                         moduleDictionary.remove(mi);
1476                     }
1477                 }
1478             } catch (IOException y) {
1479                 x.addSuppressed(y);
1480             }
1481             throw x;
1482         } finally {
1483             if (complete) {
1484                 moduleDictionary.store();
1485             }
1486             fc.close();
1487         }
1488     }
1489 
1490     @Override
1491     public void install(Resolution res, boolean verifySignature)
1492         throws ConfigurationException, IOException, SignatureException
1493     {
1494         install(res, verifySignature, false);
1495     }
1496 
1497     @Override
1498     public void removeForcibly(List<ModuleId> mids)
1499         throws IOException
1500     {
1501         try {
1502             remove(mids, true, false);
1503         } catch (ConfigurationException x) {
1504             throw new Error("should not be thrown when forcibly removing", x);
1505         }
1506     }
1507 
1508     @Override
1509     public void remove(List<ModuleId> mids, boolean dry)
1510         throws ConfigurationException, IOException
1511     {
1512         remove(mids, false,  dry);
1513     }
1514 
1515     private void remove(List<ModuleId> mids, boolean force, boolean dry)
1516         throws ConfigurationException, IOException
1517     {
1518         IOException ioe = null;
1519 
1520         try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) {
1521             fc.lock();
1522             for (ModuleId mid : mids) {
1523                 // ## Should we support alias and/or non-default view names??
1524                 if (moduleDictionary.findDeclaringModuleDir(mid) == null)
1525                     throw new IllegalArgumentException(mid + ": No such module");
1526             }
1527             if (!force)
1528                 ensureNotInConfiguration(mids);
1529             if (dry)
1530                 return;
1531 
1532             // The library may be altered after this point, so the modules
1533             // dictionary needs to be refreshed
1534             List<IOException> excs = removeWhileLocked(mids);
1535             try {
1536                 moduleDictionary.refresh();
1537                 moduleDictionary.store();
1538             } catch (IOException x) {
1539                 excs.add(x);
1540             }
1541             if (!excs.isEmpty()) {
1542                 ioe = excs.remove(0);
1543                 for (IOException x : excs)
1544                     ioe.addSuppressed(x);
1545             }
1546         } finally {
1547             if (ioe != null)
1548                 throw ioe;
1549         }
1550     }
1551 
1552     private void ensureNotInConfiguration(List<ModuleId> mids)
1553         throws ConfigurationException, IOException
1554     {
1555         // ## We do not know if a root module in a child library depends on one
1556         // ## of the 'to be removed' modules. We would break it's configuration.
1557 
1558         // check each root configuration for reference to a module in mids
1559         for (ModuleId rootid : libraryRoots()) {
1560             // skip any root modules being removed
1561             if (mids.contains(rootid))
1562                 continue;
1563 
1564             Configuration<Context> cf = readConfiguration(rootid);
1565             for (Context cx : cf.contexts()) {
1566                 for (ModuleId mid : cx.modules()) {
1567                     if (mids.contains(mid))
1568                         throw new ConfigurationException(mid +
1569                                 ": being used by " + rootid);
1570                 }
1571             }  
1572         }
1573     }
1574 
1575     private static final String TRASH = ".trash";
1576     // lazy initialization of Random
1577     private static class LazyInitialization {
1578         static final Random random = new Random();
1579     }
1580     private static Path moduleTrashDir(File trash, ModuleId mid)
1581         throws IOException
1582     {
1583         String mn = mid.name();
1584         Version version = mid.version();
1585         String v = (version != null) ? version.toString() : "default";
1586         for (;;) {
1587             long n = LazyInitialization.random.nextLong();
1588             n = (n == Long.MIN_VALUE) ? 0 : Math.abs(n);
1589             String modTrashName = mn + '_' + v + '_' + Long.toString(n);
1590             File mtd = new File(trash, modTrashName);
1591             if (!mtd.exists())
1592                 return mtd.toPath();
1593         }
1594     }
1595 
1596     private List<IOException> removeWhileLocked(List<ModuleId> mids) {
1597         List<IOException> excs = new ArrayList<>();
1598         // First move the modules to the .trash dir
1599         for (ModuleId mid : mids) {
1600             try {
1601                 File md = moduleDir(root, mid);
1602                 java.nio.file.Files.move(md.toPath(),
1603                                          moduleTrashDir(trash, mid),
1604                                          ATOMIC_MOVE);
1605                 File p = md.getParentFile();
1606                 if (p.list().length == 0)
1607                     java.nio.file.Files.delete(p.toPath());
1608             } catch (IOException x) {
1609                 excs.add(x);
1610             }
1611         }
1612         for (String tm : trash.list())
1613             excs.addAll(ModuleFile.Reader.remove(new File(trash, tm)));
1614 
1615         return excs;
1616     }
1617 
1618     /**
1619      * <p> Pre-install one or more modules to an arbitrary destination
1620      * directory. </p>
1621      *
1622      * <p> A pre-installed module has the same format as within the library
1623      * itself, except that there is never a configuration file. </p>
1624      *
1625      * <p> This method is provided for use by the module-packaging tool. </p>
1626      *
1627      * @param   mfs
1628      *          The manifest describing the contents of the modules to be
1629      *          pre-installed
1630      *
1631      * @param   dst
1632      *          The destination directory, with one subdirectory per module
1633      *          name, each of which contains one subdirectory per version
1634      */
1635     public void preInstall(Collection<Manifest> mfs, File dst)
1636         throws IOException
1637     {
1638         Files.mkdirs(dst, "module destination");
1639         try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) {
1640             fc.lock();
1641             for (Manifest mf : mfs) {
1642                 installWhileLocked(mf, dst, false);
1643             }
1644             // no update to the module directory
1645         }
1646     }
1647 
1648     public void preInstall(Manifest mf, File dst)
1649         throws IOException
1650     {
1651         preInstall(Collections.singleton(mf), dst);
1652     }
1653 
1654     /**
1655      * Refresh the module library.
1656      */
1657     public void refresh() throws IOException {
1658         try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) {
1659             fc.lock();
1660             moduleDictionary.refresh();
1661             moduleDictionary.store();
1662         }
1663     }
1664 
1665     /**
1666      * <p> Update the configurations of any root modules affected by the
1667      * copying of the named modules, in pre-installed format, into this
1668      * library. </p>
1669      *
1670      * @param   mids
1671      *          The module ids of the new or updated modules, or
1672      *          {@code null} if the configuration of every root module
1673      *          should be (re)computed
1674      */
1675     public void configure(Collection<ModuleId> mids)
1676         throws ConfigurationException, IOException
1677     {
1678         try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) {
1679             fc.lock();
1680             configureWhileModuleDirectoryLocked(mids);
1681         }
1682     }
1683 
1684     private void configureWhileModuleDirectoryLocked(Collection<ModuleId> mids)
1685         throws ConfigurationException, IOException
1686     {
1687         // ## mids not used yet
1688         for (ModuleId mid : libraryRoots()) {
1689             // ## We could be a lot more clever about this!
1690             Configuration<Context> cf
1691                 = Configurator.configure(this, mid.toQuery());
1692             File md = moduleDictionary.findDeclaringModuleDir(mid);
1693             new StoredConfiguration(md, cf).store();
1694         }
1695     }
1696 
1697     private List<ModuleId> libraryRoots()
1698         throws IOException
1699     {
1700         List<ModuleId> roots = new ArrayList<>();
1701         for (ModuleId mid : listLocalDeclaringModuleIds()) {
1702             // each module can have multiple entry points, but
1703             // only one configuration for each module.
1704             ModuleInfo mi = readModuleInfo(mid);
1705             for (ModuleView mv : mi.views()) {
1706                 if (mv.mainClass() != null) {
1707                     roots.add(mid);
1708                     break;
1709                 }
1710             }
1711         }
1712         return roots;
1713     }
1714 
1715     public URI findLocalResource(ModuleId mid, String name)
1716         throws IOException
1717     {
1718         return locateContent(mid, name);
1719     }
1720 
1721     public File findLocalNativeLibrary(ModuleId mid, String name)
1722         throws IOException
1723     {
1724         File f = natlibs();
1725         if (f == null) {
1726             f = moduleDictionary.findDeclaringModuleDir(mid);
1727             if (f == null)
1728                 return null;
1729             f = new File(f, "lib");
1730         }
1731         f = new File(f, name);
1732         if (!f.exists())
1733             return null;
1734         return f;
1735     }
1736 
1737     public File classPath(ModuleId mid)
1738         throws IOException
1739     {
1740         File md = moduleDictionary.findDeclaringModuleDir(mid);
1741         if (md == null) {
1742             if (parent != null)
1743                 return parent.classPath(mid);
1744             return null;
1745         }
1746         // ## Check for other formats here
1747         return new File(md, "classes");
1748     }
1749 
1750     /**
1751      * <p> Re-index the classes of the named previously-installed modules, and
1752      * then update the configurations of any affected root modules. </p>
1753      *
1754      * <p> This method is intended for use during development, when a build
1755      * process may update a previously-installed module in place, adding or
1756      * removing classes. </p>
1757      *
1758      * @param   mids
1759      *          The module ids of the new or updated modules, or
1760      *          {@code null} if the configuration of every root module
1761      *          should be (re)computed
1762      */
1763     public void reIndex(List<ModuleId> mids)
1764         throws ConfigurationException, IOException
1765     {
1766         for (ModuleId mid : mids)
1767             reIndex(mid);
1768         configure(mids);
1769     }
1770 
1771     private static final class ModuleDictionary
1772     {
1773         private static final String FILE
1774             = FileConstants.META_PREFIX + "mids";
1775 
1776         private final File root;
1777         private final File file;
1778         private Map<String,Set<ModuleId>> moduleIdsForName;
1779         private Map<ModuleId,ModuleId> providingModuleIds;
1780         private Set<ModuleId> modules;
1781         private long lastUpdated;
1782 
1783         ModuleDictionary(File root) {
1784             this.root = root;
1785             this.file = new File(root, FileConstants.META_PREFIX + "mids");
1786             this.providingModuleIds = new LinkedHashMap<>();
1787             this.moduleIdsForName = new LinkedHashMap<>();
1788             this.modules = new HashSet<>();
1789             this.lastUpdated = -1;
1790         }
1791 
1792         private static FileHeader fileHeader() {
1793             return (new FileHeader()
1794                     .type(FileConstants.Type.LIBRARY_MODULE_IDS)
1795                     .majorVersion(Header.MAJOR_VERSION)
1796                     .minorVersion(Header.MINOR_VERSION));
1797         }
1798 
1799         void load() throws IOException {
1800             if (lastUpdated == file.lastModified())
1801                 return;
1802 
1803             providingModuleIds = new LinkedHashMap<>();
1804             moduleIdsForName = new LinkedHashMap<>();
1805             modules = new HashSet<>();
1806             lastUpdated = file.lastModified();
1807 
1808             try (FileInputStream fin = new FileInputStream(file);
1809                  DataInputStream in = new DataInputStream(new BufferedInputStream(fin)))
1810             {
1811                 FileHeader fh = fileHeader();
1812                 fh.read(in);
1813                 int nMids = in.readInt();
1814                 for (int j = 0; j < nMids; j++) {
1815                     ModuleId mid = jms.parseModuleId(in.readUTF());
1816                     ModuleId pmid = jms.parseModuleId(in.readUTF());
1817                     providingModuleIds.put(mid, pmid);
1818                     addModuleId(mid);
1819                     addModuleId(pmid);
1820                     if (mid.equals(pmid))
1821                         modules.add(mid);
1822                 }
1823             }
1824         }
1825 
1826         void store() throws IOException {
1827             File newfn = new File(root, "mids.new");
1828             FileOutputStream fout = new FileOutputStream(newfn);
1829             DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fout));
1830             try {
1831                 try {
1832                     fileHeader().write(out);
1833                     out.writeInt(providingModuleIds.size());
1834                     for (Map.Entry<ModuleId, ModuleId> e : providingModuleIds.entrySet()) {
1835                         out.writeUTF(e.getKey().toString());
1836                         out.writeUTF(e.getValue().toString());
1837                     }
1838                 } finally {
1839                     out.close();
1840                 }
1841             } catch (IOException x) {
1842                 newfn.delete();
1843                 throw x;
1844             }
1845             java.nio.file.Files.move(newfn.toPath(), file.toPath(), ATOMIC_MOVE);
1846         }
1847 
1848         void gatherLocalModuleIds(String moduleName, Set<ModuleId> mids)
1849                 throws IOException
1850         {
1851             if (lastUpdated != file.lastModified())
1852                 load();
1853 
1854             if (moduleName == null) {
1855                 mids.addAll(providingModuleIds.keySet());
1856             } else {
1857                 Set<ModuleId> res = moduleIdsForName.get(moduleName);
1858                 if (res != null)
1859                     mids.addAll(res);
1860             }
1861         }
1862 
1863         ModuleId getDeclaringModule(ModuleId mid) throws IOException {
1864             if (lastUpdated != file.lastModified())
1865                 load();
1866 
1867             ModuleId pmid = providingModuleIds.get(mid);
1868             if (pmid != null && !pmid.equals(providingModuleIds.get(pmid))) {
1869                 // mid is an alias
1870                 pmid = providingModuleIds.get(pmid);
1871             }
1872             return pmid;
1873         }
1874 
1875         File findDeclaringModuleDir(ModuleId mid)
1876                 throws IOException
1877         {
1878             ModuleId dmid = getDeclaringModule(mid);
1879             if (dmid == null)
1880                 return null;
1881 
1882             File md = moduleDir(root, dmid);
1883             assert md.exists();
1884             checkModuleDir(md);
1885             return md;
1886         }
1887 
1888         Set<ModuleId> modules() throws IOException {
1889             if (lastUpdated != file.lastModified())
1890                 load();
1891             return modules;
1892         }
1893 
1894         void addModuleId(ModuleId mid) {
1895             Set<ModuleId> mids = moduleIdsForName.get(mid.name());
1896             if (mids == null) {
1897                 mids = new HashSet<>();
1898                 moduleIdsForName.put(mid.name(), mids);
1899             }
1900             mids.add(mid);
1901         }
1902 
1903         File add(ModuleInfo mi)
1904                 throws ConfigurationException, IOException
1905         {
1906             File md = ensureNewModule(mi);
1907             addToDirectory(mi);
1908             return md;
1909         }
1910 
1911         private void addToDirectory(ModuleInfo mi) {
1912             modules.add(mi.id());
1913             for (ModuleView view : mi.views()) {
1914                 providingModuleIds.put(view.id(), mi.id());
1915                 addModuleId(view.id());
1916                 for (ModuleId alias : view.aliases()) {
1917                     providingModuleIds.put(alias, view.id());
1918                     addModuleId(alias);
1919                 }
1920             }
1921         }
1922 
1923         void remove(ModuleInfo mi) throws IOException {
1924             modules.remove(mi.id());
1925             for (ModuleView view : mi.views()) {
1926                 providingModuleIds.remove(view.id());
1927                 Set<ModuleId> mids = moduleIdsForName.get(view.id().name());
1928                 if (mids != null)
1929                     mids.remove(view.id());
1930                 for (ModuleId alias : view.aliases()) {
1931                     providingModuleIds.remove(alias);
1932                     mids = moduleIdsForName.get(alias.name());
1933                     if (mids != null)
1934                         mids.remove(view.id());
1935                 }
1936             }
1937             File md = moduleDir(root, mi.id());
1938             delete(md);
1939         }
1940 
1941         private void delete(File md) throws IOException {
1942             if (!md.exists())
1943                 return;
1944 
1945             checkModuleDir(md);
1946             ModuleFile.Reader.remove(md);
1947             File parent = md.getParentFile();
1948             if (parent.list().length == 0)
1949                 parent.delete();
1950         }
1951 
1952         void refresh() throws IOException {
1953             providingModuleIds = new LinkedHashMap<>();
1954             moduleIdsForName = new LinkedHashMap<>();
1955             modules = new HashSet<>();
1956 
1957             try (DirectoryStream<Path> ds = java.nio.file.Files.newDirectoryStream(root.toPath())) {
1958                 for (Path mnp : ds) {
1959                     String mn = mnp.toFile().getName();
1960                     if (mn.startsWith(FileConstants.META_PREFIX) ||
1961                         TRASH.equals(mn)) {
1962                         continue;
1963                     }
1964 
1965                     try (DirectoryStream<Path> mds = java.nio.file.Files.newDirectoryStream(mnp)) {
1966                         for (Path versionp : mds) {
1967                             File v = versionp.toFile();
1968                             if (!v.isDirectory()) {
1969                                 throw new IOException(versionp + ": Not a directory");
1970                             }
1971                             modules.add(jms.parseModuleId(mn, v.getName()));
1972                         }
1973                     }
1974                 }
1975             }
1976             for (ModuleId mid : modules) {
1977                 byte[] bs = Files.load(new File(moduleDir(root, mid), "info"));
1978                 ModuleInfo mi = jms.parseModuleInfo(bs);
1979                 addToDirectory(mi);
1980             }
1981         }
1982 
1983         private File ensureNewModule(ModuleInfo mi)
1984                 throws ConfigurationException, IOException
1985         {
1986             for (ModuleView view : mi.views()) {
1987                 if (providingModuleIds.containsKey(view.id())) {
1988                     throw new ConfigurationException("module view " + view.id()
1989                             + " already installed");
1990                 }
1991                 for (ModuleId alias : view.aliases()) {
1992                     ModuleId mid = alias;
1993                     if (providingModuleIds.containsKey(mid)) {
1994                         throw new ConfigurationException("alias " + alias
1995                                 + " already installed");
1996                     }
1997                 }
1998             }
1999             File md = moduleDir(root, mi.id());
2000             if (md.exists()) {
2001                 throw new ConfigurationException("module " + mi.id()
2002                         + " already installed");
2003             }
2004             if (!md.mkdirs()) {
2005                 throw new IOException(md + ": Cannot create");
2006             }
2007             return md;
2008         }
2009     }
2010 
2011     // -- Repositories --
2012 
2013     private static class RepoList
2014         implements RemoteRepositoryList
2015     {
2016 
2017         private static final int MINOR_VERSION = 0;
2018         private static final int MAJOR_VERSION = 0;
2019 
2020         private final File root;
2021         private final File listFile;
2022 
2023         private RepoList(File r) {
2024             root = new File(r, FileConstants.META_PREFIX + "repos");
2025             listFile = new File(root, FileConstants.META_PREFIX + "list");
2026         }
2027 
2028         private static FileHeader fileHeader() {
2029             return (new FileHeader()
2030                     .type(FileConstants.Type.REMOTE_REPO_LIST)
2031                     .majorVersion(MAJOR_VERSION)
2032                     .minorVersion(MINOR_VERSION));
2033         }
2034 
2035         private List<RemoteRepository> repos = null;
2036         private long nextRepoId = 0;
2037 
2038         private File repoDir(long id) {
2039             return new File(root, Long.toHexString(id));
2040         }
2041 
2042         private void load() throws IOException {
2043 
2044             repos = new ArrayList<>();
2045             if (!root.exists() || !listFile.exists())
2046                 return;
2047             FileInputStream fin = new FileInputStream(listFile);
2048             DataInputStream in
2049                 = new DataInputStream(new BufferedInputStream(fin));
2050             try {
2051 
2052                 FileHeader fh = fileHeader();
2053                 fh.read(in);
2054                 nextRepoId = in.readLong();
2055                 int n = in.readInt();
2056                 long[] ids = new long[n];
2057                 for (int i = 0; i < n; i++)
2058                     ids[i] = in.readLong();
2059                 RemoteRepository parent = null;
2060 
2061                 // Load in reverse order so that parents are correct
2062                 for (int i = n - 1; i >= 0; i--) {
2063                     long id = ids[i];
2064                     RemoteRepository rr
2065                         = RemoteRepository.open(repoDir(id), id, parent);
2066                     repos.add(rr);
2067                     parent = rr;
2068                 }
2069                 Collections.reverse(repos);
2070 
2071             } finally {
2072                 in.close();
2073             }
2074 
2075         }
2076 
2077         private List<RemoteRepository> roRepos = null;
2078 
2079         // Unmodifiable
2080         public List<RemoteRepository> repositories() throws IOException {
2081             if (repos == null) {
2082                 load();
2083                 roRepos = Collections.unmodifiableList(repos);
2084             }
2085             return roRepos;
2086         }
2087 
2088         public RemoteRepository firstRepository() throws IOException {
2089             repositories();
2090             return repos.isEmpty() ? null : repos.get(0);
2091         }
2092 
2093         private void store() throws IOException {
2094             File newfn = new File(root, "list.new");
2095             FileOutputStream fout = new FileOutputStream(newfn);
2096             DataOutputStream out
2097                 = new DataOutputStream(new BufferedOutputStream(fout));
2098             try {
2099                 try {
2100                     fileHeader().write(out);
2101                     out.writeLong(nextRepoId);
2102                     out.writeInt(repos.size());
2103                     for (RemoteRepository rr : repos)
2104                         out.writeLong(rr.id());
2105                 } finally {
2106                     out.close();
2107                 }
2108             } catch (IOException x) {
2109                 newfn.delete();
2110                 throw x;
2111             }
2112             java.nio.file.Files.move(newfn.toPath(), listFile.toPath(), ATOMIC_MOVE);
2113         }
2114 
2115         public RemoteRepository add(URI u, int position)
2116             throws IOException
2117         {
2118 
2119             if (repos == null)
2120                 load();
2121             for (RemoteRepository rr : repos) {
2122                 if (rr.location().equals(u)) // ## u not canonical
2123                     throw new IllegalStateException(u + ": Already in"
2124                                                     + " repository list");
2125             }
2126             if (!root.exists()) {
2127                 if (!root.mkdir())
2128                     throw new IOException(root + ": Cannot create directory");
2129             }
2130 
2131             if (repos.size() == Integer.MAX_VALUE)
2132                 throw new IllegalStateException("Too many repositories");
2133             if (position < 0)
2134                 throw new IllegalArgumentException("Invalid index");
2135 
2136             long id = nextRepoId++;
2137             RemoteRepository rr = RemoteRepository.create(repoDir(id), u, id);
2138             try {
2139                 rr.updateCatalog(true);
2140             } catch (IOException x) {
2141                 rr.delete();
2142                 nextRepoId--;
2143                 throw x;
2144             }
2145 
2146             if (position >= repos.size()) {
2147                 repos.add(rr);
2148             } else if (position >= 0) {
2149                 List<RemoteRepository> prefix
2150                     = new ArrayList<>(repos.subList(0, position));
2151                 List<RemoteRepository> suffix
2152                     = new ArrayList<>(repos.subList(position, repos.size()));
2153                 repos.clear();
2154                 repos.addAll(prefix);
2155                 repos.add(rr);
2156                 repos.addAll(suffix);
2157             }
2158             store();
2159 
2160             return rr;
2161 
2162         }
2163 
2164         public boolean remove(RemoteRepository rr)
2165             throws IOException
2166         {
2167             if (!repos.remove(rr))
2168                 return false;
2169             store();
2170             File rd = repoDir(rr.id());
2171             for (File f : rd.listFiles()) {
2172                 if (!f.delete())
2173                     throw new IOException(f + ": Cannot delete");
2174             }
2175             if (!rd.delete())
2176                 throw new IOException(rd + ": Cannot delete");
2177             return true;
2178         }
2179 
2180         public boolean areCatalogsStale() throws IOException {
2181             for (RemoteRepository rr : repos) {
2182                 if (rr.isCatalogStale())
2183                     return true;
2184             }
2185             return false;
2186         }
2187 
2188         public boolean updateCatalogs(boolean force) throws IOException {
2189             boolean updated = false;
2190             for (RemoteRepository rr : repos) {
2191                 if (rr.updateCatalog(force))
2192                     updated = true;
2193             }
2194             return updated;
2195         }
2196 
2197     }
2198 
2199     private RemoteRepositoryList repoList = null;
2200 
2201     public RemoteRepositoryList repositoryList()
2202         throws IOException
2203     {
2204         if (repoList == null)
2205             repoList = new RepoList(root);
2206         return repoList;
2207     }
2208 
2209 }