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             mi = jms.parseModuleInfo(mib);
1164             File md = moduleDictionary.add(mi);
1165             if (verifySignature && mr.hasSignature()) {
1166                 // Verify the module signature
1167                 SignedModule sm = new SignedModule(mr);
1168                 Set<CodeSigner> signers = sm.verifySignature();
1169 
1170                 // Validate the signers
1171                 try {
1172                     SignedModule.validateSigners(signers);
1173                 } catch (CertificateException x) {
1174                     throw new SignatureException(x);
1175                 }
1176 
1177                 // ## TODO: Check policy and determine if signer is trusted
1178                 // ## and what permissions should be granted.
1179                 // ## If there is no policy entry, show signers and prompt
1180                 // ## user to accept before proceeding.
1181 
1182                 // Verify the module header hash and the module info hash
1183                 sm.verifyHashesStart();
1184 
1185                 // Read the rest of the hashes
1186                 mr.readRest(md, isDeflated(), natlibs(), natcmds(), configs());
1187 
1188                 // Verify the rest of the hashes
1189                 sm.verifyHashesRest();
1190 
1191                 // Store signer info
1192                 new Signers(md, signers).store();
1193             } else {
1194                 mr.readRest(md, isDeflated(), natlibs(), natcmds(), configs());
1195             }
1196 
1197             if (strip)
1198                 strip(md);
1199             reIndex(mi.id());         // ## Could do this while reading module file
1200 
1201             return mi.id();
1202 
1203         } catch (ConfigurationException | IOException | SignatureException x) {
1204             if (mi != null) {
1205                 try {
1206                     moduleDictionary.remove(mi);
1207                 } catch (IOException y) {
1208                     x.addSuppressed(y);
1209                 }
1210             }
1211             throw x;
1212         }
1213     }
1214 
1215     private ModuleId installFromJarFile(File mf, boolean verifySignature, boolean strip)
1216         throws ConfigurationException, IOException, SignatureException
1217     {
1218         ModuleInfo mi = null;
1219         try (JarFile jf = new JarFile(mf, verifySignature)) {
1220             mi = jf.getModuleInfo();
1221             if (mi == null)
1222                 throw new ConfigurationException(mf + ": not a modular JAR file");
1223 
1224             File md = moduleDictionary.add(mi);
1225             ModuleId mid = mi.id();
1226 
1227             boolean signed = false;
1228 
1229             // copy the jar file to the module library
1230             File classesDir = new File(md, "classes");
1231             try (FileOutputStream fos = new FileOutputStream(classesDir);
1232                  BufferedOutputStream bos = new BufferedOutputStream(fos);
1233                  JarOutputStream jos = new JarOutputStream(bos)) {
1234                 jos.setLevel(0);
1235 
1236                 Enumeration<JarEntry> entries = jf.entries();
1237                 while (entries.hasMoreElements()) {
1238                     JarEntry je = entries.nextElement();
1239                     try (InputStream is = jf.getInputStream(je)) {
1240                         if (je.getName().equals(JarFile.MODULEINFO_NAME)) {
1241                             java.nio.file.Files.copy(is, md.toPath().resolve("info"));
1242                         } else {
1243                             writeJarEntry(is, je, jos);
1244                         }
1245                     }
1246                     if (!signed) {
1247                         String name = je.getName().toUpperCase(Locale.ENGLISH);
1248                         signed = name.startsWith("META-INF/")
1249                                  && name.endsWith(".SF");
1250                     }
1251                 }
1252             }
1253 
1254             try {
1255                 if (verifySignature && signed) {
1256                     // validate the code signers
1257                     Set<CodeSigner> signers = getSigners(jf);
1258                     SignedModule.validateSigners(signers);
1259                     // store the signers
1260                     new Signers(md, signers).store();
1261                 }
1262             } catch (CertificateException ce) {
1263                 throw new SignatureException(ce);
1264             }
1265 
1266             if (strip)
1267                 strip(md);
1268             reIndex(mid);
1269 
1270             return mid;
1271         } catch (ConfigurationException | IOException | SignatureException x) {
1272             if (mi != null) {
1273                 try {
1274                     moduleDictionary.remove(mi);
1275                 } catch (IOException y) {
1276                     x.addSuppressed(y);
1277                 }
1278             }
1279             throw x;
1280         }
1281     }
1282 
1283     /**
1284      * Returns the set of signers of the specified jar file. Each signer
1285      * must have signed all relevant entries.
1286      */
1287     private static Set<CodeSigner> getSigners(JarFile jf)
1288         throws SignatureException
1289     {
1290         Set<CodeSigner> signers = new HashSet<>();
1291         Enumeration<JarEntry> entries = jf.entries();
1292         while (entries.hasMoreElements()) {
1293             JarEntry je = entries.nextElement();
1294             String name = je.getName().toUpperCase(Locale.ENGLISH);
1295             if (name.endsWith("/") || isSigningRelated(name))
1296                 continue;
1297 
1298             // A signed modular jar can be signed by multiple signers.
1299             // However, all entries must be signed by each of these signers.
1300             // Signers that only sign a subset of entries are ignored.
1301             CodeSigner[] jeSigners = je.getCodeSigners();
1302             if (jeSigners == null || jeSigners.length == 0)
1303                 throw new SignatureException("Found unsigned entry in "
1304                                              + "signed modular JAR");
1305 
1306             Set<CodeSigner> jeSignerSet =
1307                 new HashSet<>(Arrays.asList(jeSigners));
1308             if (signers.isEmpty())
1309                 signers.addAll(jeSignerSet);
1310             else if (signers.retainAll(jeSignerSet) && signers.isEmpty())
1311                 throw new SignatureException("No signers in common in "
1312                                              + "signed modular JAR");
1313         }
1314         return signers;
1315     }
1316 
1317     // true if file is part of the signature mechanism itself
1318     private static boolean isSigningRelated(String name) {
1319         if (!name.startsWith("META-INF/")) {
1320             return false;
1321         }
1322         name = name.substring(9);
1323         if (name.indexOf('/') != -1) {
1324             return false;
1325         }
1326         if (name.endsWith(".DSA") ||
1327             name.endsWith(".RSA") ||
1328             name.endsWith(".SF")  ||
1329             name.endsWith(".EC")  ||
1330             name.startsWith("SIG-") ||
1331             name.equals("MANIFEST.MF")) {
1332             return true;
1333         }
1334         return false;
1335     }
1336 
1337     private void writeJarEntry(InputStream is, JarEntry je, JarOutputStream jos)
1338         throws IOException, SignatureException
1339     {
1340         JarEntry entry = new JarEntry(je.getName());
1341         entry.setMethod(isDeflated() ? ZipEntry.DEFLATED : ZipEntry.STORED);
1342         entry.setTime(je.getTime());
1343         try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
1344             int size = 0;
1345             byte[] bs = new byte[1024];
1346             int cc = 0;
1347             // This will throw a SecurityException if a signature is invalid.
1348             while ((cc = is.read(bs)) > 0) {
1349                 baos.write(bs, 0, cc);
1350                 size += cc;
1351             }
1352             if (!isDeflated()) {
1353                 entry.setSize(size);
1354                 entry.setCrc(je.getCrc());
1355                 entry.setCompressedSize(size);
1356             }
1357             jos.putNextEntry(entry);
1358             if (baos.size() > 0)
1359                 baos.writeTo(jos);
1360             jos.closeEntry();
1361         } catch (SecurityException se) {
1362             throw new SignatureException(se);
1363         }
1364     }
1365 
1366     private ModuleId installWhileLocked(File mf, boolean verifySignature, boolean strip)
1367         throws ConfigurationException, IOException, SignatureException
1368     {
1369         if (mf.getName().endsWith(".jar"))
1370             return installFromJarFile(mf, verifySignature, strip);
1371         else {
1372             // Assume jmod file
1373             try (FileInputStream in = new FileInputStream(mf)) {
1374                 return installWhileLocked(in, verifySignature, strip);
1375             }
1376         }
1377     }
1378 
1379     public void install(Collection<File> mfs, boolean verifySignature, boolean strip)
1380         throws ConfigurationException, IOException, SignatureException
1381     {
1382         List<ModuleId> mids = new ArrayList<>();
1383         boolean complete = false;
1384         FileChannel fc = FileChannel.open(lockf.toPath(), WRITE);
1385         try {
1386             fc.lock();
1387             moduleDictionary.load();
1388             for (File mf : mfs)
1389                 mids.add(installWhileLocked(mf, verifySignature, strip));
1390             configureWhileModuleDirectoryLocked(mids);
1391             complete = true;
1392         } catch (ConfigurationException | IOException | SignatureException x) {
1393             try {
1394                 for (ModuleId mid : mids) {
1395                     ModuleInfo mi = readLocalModuleInfo(mid);
1396                     if (mi != null) {
1397                         moduleDictionary.remove(mi);
1398                     }
1399                 }
1400             } catch (IOException y) {
1401                 x.addSuppressed(y);
1402             }
1403             throw x;
1404         } finally {
1405             if (complete) {
1406                 moduleDictionary.store();
1407             }
1408             fc.close();
1409         }
1410     }
1411 
1412     @Override
1413     public void install(Collection<File> mfs, boolean verifySignature)
1414         throws ConfigurationException, IOException, SignatureException
1415     {
1416         install(mfs, verifySignature, false);
1417     }
1418 
1419     // Public entry point, since the Resolver itself is package-private
1420     //
1421     public Resolution resolve(Collection<ModuleIdQuery> midqs)
1422         throws ConfigurationException, IOException
1423     {
1424         try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) {
1425             fc.lock();
1426             return Resolver.run(this, midqs);
1427         }
1428     }
1429 
1430     public void install(Resolution res, boolean verifySignature, boolean strip)
1431         throws ConfigurationException, IOException, SignatureException
1432     {
1433         boolean complete = false;
1434         FileChannel fc = FileChannel.open(lockf.toPath(), WRITE);
1435         try {
1436             fc.lock();
1437             moduleDictionary.load();
1438 
1439             // ## Handle case of installing multiple root modules
1440             assert res.rootQueries.size() == 1;
1441             ModuleIdQuery midq = res.rootQueries.iterator().next();
1442             ModuleInfo root = null;
1443             for (String mn : res.moduleViewForName.keySet()) {
1444                 ModuleView mv = res.moduleViewForName.get(mn);
1445                 if (midq.matches(mv.id())) {
1446                     root = mv.moduleInfo();
1447                     break;
1448                 }
1449             }
1450             assert root != null;
1451 
1452             // Download
1453             //
1454             for (ModuleId mid : res.modulesNeeded()) {
1455                 URI u = res.locationForName.get(mid.name());
1456                 assert u != null;
1457                 RemoteRepository rr = repositoryList().firstRepository();
1458                 assert rr != null;
1459                 installWhileLocked(rr.fetch(mid), verifySignature, strip);
1460                 res.locationForName.put(mid.name(), location());
1461                 // ## If something goes wrong, delete all our modules
1462             }
1463 
1464             // Configure
1465             //
1466             configureWhileModuleDirectoryLocked(res.modulesNeeded());
1467             complete = true;
1468         } catch (ConfigurationException | IOException | SignatureException x) {
1469             try {
1470                 for (ModuleId mid : res.modulesNeeded()) {
1471                     ModuleInfo mi = readLocalModuleInfo(mid);
1472                     if (mi != null) {
1473                         moduleDictionary.remove(mi);
1474                     }
1475                 }
1476             } catch (IOException y) {
1477                 x.addSuppressed(y);
1478             }
1479             throw x;
1480         } finally {
1481             if (complete) {
1482                 moduleDictionary.store();
1483             }
1484             fc.close();
1485         }
1486     }
1487 
1488     @Override
1489     public void install(Resolution res, boolean verifySignature)
1490         throws ConfigurationException, IOException, SignatureException
1491     {
1492         install(res, verifySignature, false);
1493     }
1494 
1495     @Override
1496     public void removeForcibly(List<ModuleId> mids)
1497         throws IOException
1498     {
1499         try {
1500             remove(mids, true, false);
1501         } catch (ConfigurationException x) {
1502             throw new Error("should not be thrown when forcibly removing", x);
1503         }
1504     }
1505 
1506     @Override
1507     public void remove(List<ModuleId> mids, boolean dry)
1508         throws ConfigurationException, IOException
1509     {
1510         remove(mids, false,  dry);
1511     }
1512 
1513     private void remove(List<ModuleId> mids, boolean force, boolean dry)
1514         throws ConfigurationException, IOException
1515     {
1516         IOException ioe = null;
1517 
1518         try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) {
1519             fc.lock();
1520             for (ModuleId mid : mids) {
1521                 // ## Should we support alias and/or non-default view names??
1522                 if (moduleDictionary.findDeclaringModuleDir(mid) == null)
1523                     throw new IllegalArgumentException(mid + ": No such module");
1524             }
1525             if (!force)
1526                 ensureNotInConfiguration(mids);
1527             if (dry)
1528                 return;
1529 
1530             // The library may be altered after this point, so the modules
1531             // dictionary needs to be refreshed
1532             List<IOException> excs = removeWhileLocked(mids);
1533             try {
1534                 moduleDictionary.refresh();
1535                 moduleDictionary.store();
1536             } catch (IOException x) {
1537                 excs.add(x);
1538             }
1539             if (!excs.isEmpty()) {
1540                 ioe = excs.remove(0);
1541                 for (IOException x : excs)
1542                     ioe.addSuppressed(x);
1543             }
1544         } finally {
1545             if (ioe != null)
1546                 throw ioe;
1547         }
1548     }
1549 
1550     private void ensureNotInConfiguration(List<ModuleId> mids)
1551         throws ConfigurationException, IOException
1552     {
1553         // ## We do not know if a root module in a child library depends on one
1554         // ## of the 'to be removed' modules. We would break it's configuration.
1555 
1556         // check each root configuration for reference to a module in mids
1557         for (ModuleId rootid : libraryRoots()) {
1558             // skip any root modules being removed
1559             if (mids.contains(rootid))
1560                 continue;
1561 
1562             Configuration<Context> cf = readConfiguration(rootid);
1563             for (Context cx : cf.contexts()) {
1564                 for (ModuleId mid : cx.modules()) {
1565                     if (mids.contains(mid))
1566                         throw new ConfigurationException(mid +
1567                                 ": being used by " + rootid);
1568                 }
1569             }  
1570         }
1571     }
1572 
1573     private static final String TRASH = ".trash";
1574     // lazy initialization of Random
1575     private static class LazyInitialization {
1576         static final Random random = new Random();
1577     }
1578     private static Path moduleTrashDir(File trash, ModuleId mid)
1579         throws IOException
1580     {
1581         String mn = mid.name();
1582         Version version = mid.version();
1583         String v = (version != null) ? version.toString() : "default";
1584         for (;;) {
1585             long n = LazyInitialization.random.nextLong();
1586             n = (n == Long.MIN_VALUE) ? 0 : Math.abs(n);
1587             String modTrashName = mn + '_' + v + '_' + Long.toString(n);
1588             File mtd = new File(trash, modTrashName);
1589             if (!mtd.exists())
1590                 return mtd.toPath();
1591         }
1592     }
1593 
1594     private List<IOException> removeWhileLocked(List<ModuleId> mids) {
1595         List<IOException> excs = new ArrayList<>();
1596         // First move the modules to the .trash dir
1597         for (ModuleId mid : mids) {
1598             try {
1599                 File md = moduleDir(root, mid);
1600                 java.nio.file.Files.move(md.toPath(),
1601                                          moduleTrashDir(trash, mid),
1602                                          ATOMIC_MOVE);
1603                 File p = md.getParentFile();
1604                 if (p.list().length == 0)
1605                     java.nio.file.Files.delete(p.toPath());
1606             } catch (IOException x) {
1607                 excs.add(x);
1608             }
1609         }
1610         for (String tm : trash.list())
1611             excs.addAll(ModuleFile.Reader.remove(new File(trash, tm)));
1612 
1613         return excs;
1614     }
1615 
1616     /**
1617      * <p> Pre-install one or more modules to an arbitrary destination
1618      * directory. </p>
1619      *
1620      * <p> A pre-installed module has the same format as within the library
1621      * itself, except that there is never a configuration file. </p>
1622      *
1623      * <p> This method is provided for use by the module-packaging tool. </p>
1624      *
1625      * @param   mfs
1626      *          The manifest describing the contents of the modules to be
1627      *          pre-installed
1628      *
1629      * @param   dst
1630      *          The destination directory, with one subdirectory per module
1631      *          name, each of which contains one subdirectory per version
1632      */
1633     public void preInstall(Collection<Manifest> mfs, File dst)
1634         throws IOException
1635     {
1636         Files.mkdirs(dst, "module destination");
1637         try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) {
1638             fc.lock();
1639             for (Manifest mf : mfs) {
1640                 installWhileLocked(mf, dst, false);
1641             }
1642             // no update to the module directory
1643         }
1644     }
1645 
1646     public void preInstall(Manifest mf, File dst)
1647         throws IOException
1648     {
1649         preInstall(Collections.singleton(mf), dst);
1650     }
1651 
1652     /**
1653      * Refresh the module library.
1654      */
1655     public void refresh() throws IOException {
1656         try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) {
1657             fc.lock();
1658             moduleDictionary.refresh();
1659             moduleDictionary.store();
1660         }
1661     }
1662 
1663     /**
1664      * <p> Update the configurations of any root modules affected by the
1665      * copying of the named modules, in pre-installed format, into this
1666      * library. </p>
1667      *
1668      * @param   mids
1669      *          The module ids of the new or updated modules, or
1670      *          {@code null} if the configuration of every root module
1671      *          should be (re)computed
1672      */
1673     public void configure(Collection<ModuleId> mids)
1674         throws ConfigurationException, IOException
1675     {
1676         try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) {
1677             fc.lock();
1678             configureWhileModuleDirectoryLocked(mids);
1679         }
1680     }
1681 
1682     private void configureWhileModuleDirectoryLocked(Collection<ModuleId> mids)
1683         throws ConfigurationException, IOException
1684     {
1685         // ## mids not used yet
1686         for (ModuleId mid : libraryRoots()) {
1687             // ## We could be a lot more clever about this!
1688             Configuration<Context> cf
1689                 = Configurator.configure(this, mid.toQuery());
1690             File md = moduleDictionary.findDeclaringModuleDir(mid);
1691             new StoredConfiguration(md, cf).store();
1692         }
1693     }
1694 
1695     private List<ModuleId> libraryRoots()
1696         throws IOException
1697     {
1698         List<ModuleId> roots = new ArrayList<>();
1699         for (ModuleId mid : listLocalDeclaringModuleIds()) {
1700             // each module can have multiple entry points, but
1701             // only one configuration for each module.
1702             ModuleInfo mi = readModuleInfo(mid);
1703             for (ModuleView mv : mi.views()) {
1704                 if (mv.mainClass() != null) {
1705                     roots.add(mid);
1706                     break;
1707                 }
1708             }
1709         }
1710         return roots;
1711     }
1712 
1713     public URI findLocalResource(ModuleId mid, String name)
1714         throws IOException
1715     {
1716         return locateContent(mid, name);
1717     }
1718 
1719     public File findLocalNativeLibrary(ModuleId mid, String name)
1720         throws IOException
1721     {
1722         File f = natlibs();
1723         if (f == null) {
1724             f = moduleDictionary.findDeclaringModuleDir(mid);
1725             if (f == null)
1726                 return null;
1727             f = new File(f, "lib");
1728         }
1729         f = new File(f, name);
1730         if (!f.exists())
1731             return null;
1732         return f;
1733     }
1734 
1735     public File classPath(ModuleId mid)
1736         throws IOException
1737     {
1738         File md = moduleDictionary.findDeclaringModuleDir(mid);
1739         if (md == null) {
1740             if (parent != null)
1741                 return parent.classPath(mid);
1742             return null;
1743         }
1744         // ## Check for other formats here
1745         return new File(md, "classes");
1746     }
1747 
1748     /**
1749      * <p> Re-index the classes of the named previously-installed modules, and
1750      * then update the configurations of any affected root modules. </p>
1751      *
1752      * <p> This method is intended for use during development, when a build
1753      * process may update a previously-installed module in place, adding or
1754      * removing classes. </p>
1755      *
1756      * @param   mids
1757      *          The module ids of the new or updated modules, or
1758      *          {@code null} if the configuration of every root module
1759      *          should be (re)computed
1760      */
1761     public void reIndex(List<ModuleId> mids)
1762         throws ConfigurationException, IOException
1763     {
1764         for (ModuleId mid : mids)
1765             reIndex(mid);
1766         configure(mids);
1767     }
1768 
1769     private static final class ModuleDictionary
1770     {
1771         private static final String FILE
1772             = FileConstants.META_PREFIX + "mids";
1773 
1774         private final File root;
1775         private final File file;
1776         private Map<String,Set<ModuleId>> moduleIdsForName;
1777         private Map<ModuleId,ModuleId> providingModuleIds;
1778         private Set<ModuleId> modules;
1779         private long lastUpdated;
1780 
1781         ModuleDictionary(File root) {
1782             this.root = root;
1783             this.file = new File(root, FileConstants.META_PREFIX + "mids");
1784             this.providingModuleIds = new LinkedHashMap<>();
1785             this.moduleIdsForName = new LinkedHashMap<>();
1786             this.modules = new HashSet<>();
1787             this.lastUpdated = -1;
1788         }
1789 
1790         private static FileHeader fileHeader() {
1791             return (new FileHeader()
1792                     .type(FileConstants.Type.LIBRARY_MODULE_IDS)
1793                     .majorVersion(Header.MAJOR_VERSION)
1794                     .minorVersion(Header.MINOR_VERSION));
1795         }
1796 
1797         void load() throws IOException {
1798             if (lastUpdated == file.lastModified())
1799                 return;
1800 
1801             providingModuleIds = new LinkedHashMap<>();
1802             moduleIdsForName = new LinkedHashMap<>();
1803             modules = new HashSet<>();
1804             lastUpdated = file.lastModified();
1805 
1806             try (FileInputStream fin = new FileInputStream(file);
1807                  DataInputStream in = new DataInputStream(new BufferedInputStream(fin)))
1808             {
1809                 FileHeader fh = fileHeader();
1810                 fh.read(in);
1811                 int nMids = in.readInt();
1812                 for (int j = 0; j < nMids; j++) {
1813                     ModuleId mid = jms.parseModuleId(in.readUTF());
1814                     ModuleId pmid = jms.parseModuleId(in.readUTF());
1815                     providingModuleIds.put(mid, pmid);
1816                     addModuleId(mid);
1817                     addModuleId(pmid);
1818                     if (mid.equals(pmid))
1819                         modules.add(mid);
1820                 }
1821             }
1822         }
1823 
1824         void store() throws IOException {
1825             File newfn = new File(root, "mids.new");
1826             FileOutputStream fout = new FileOutputStream(newfn);
1827             DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fout));
1828             try {
1829                 try {
1830                     fileHeader().write(out);
1831                     out.writeInt(providingModuleIds.size());
1832                     for (Map.Entry<ModuleId, ModuleId> e : providingModuleIds.entrySet()) {
1833                         out.writeUTF(e.getKey().toString());
1834                         out.writeUTF(e.getValue().toString());
1835                     }
1836                 } finally {
1837                     out.close();
1838                 }
1839             } catch (IOException x) {
1840                 newfn.delete();
1841                 throw x;
1842             }
1843             java.nio.file.Files.move(newfn.toPath(), file.toPath(), ATOMIC_MOVE);
1844         }
1845 
1846         void gatherLocalModuleIds(String moduleName, Set<ModuleId> mids)
1847                 throws IOException
1848         {
1849             if (lastUpdated != file.lastModified())
1850                 load();
1851 
1852             if (moduleName == null) {
1853                 mids.addAll(providingModuleIds.keySet());
1854             } else {
1855                 Set<ModuleId> res = moduleIdsForName.get(moduleName);
1856                 if (res != null)
1857                     mids.addAll(res);
1858             }
1859         }
1860 
1861         ModuleId getDeclaringModule(ModuleId mid) throws IOException {
1862             if (lastUpdated != file.lastModified())
1863                 load();
1864 
1865             ModuleId pmid = providingModuleIds.get(mid);
1866             if (pmid != null && !pmid.equals(providingModuleIds.get(pmid))) {
1867                 // mid is an alias
1868                 pmid = providingModuleIds.get(pmid);
1869             }
1870             return pmid;
1871         }
1872 
1873         File findDeclaringModuleDir(ModuleId mid)
1874                 throws IOException
1875         {
1876             ModuleId dmid = getDeclaringModule(mid);
1877             if (dmid == null)
1878                 return null;
1879 
1880             File md = moduleDir(root, dmid);
1881             assert md.exists();
1882             checkModuleDir(md);
1883             return md;
1884         }
1885 
1886         Set<ModuleId> modules() throws IOException {
1887             if (lastUpdated != file.lastModified())
1888                 load();
1889             return modules;
1890         }
1891 
1892         void addModuleId(ModuleId mid) {
1893             Set<ModuleId> mids = moduleIdsForName.get(mid.name());
1894             if (mids == null) {
1895                 mids = new HashSet<>();
1896                 moduleIdsForName.put(mid.name(), mids);
1897             }
1898             mids.add(mid);
1899         }
1900 
1901         File add(ModuleInfo mi)
1902                 throws ConfigurationException, IOException
1903         {
1904             File md = ensureNewModule(mi);
1905             addToDirectory(mi);
1906             return md;
1907         }
1908 
1909         private void addToDirectory(ModuleInfo mi) {
1910             modules.add(mi.id());
1911             for (ModuleView view : mi.views()) {
1912                 providingModuleIds.put(view.id(), mi.id());
1913                 addModuleId(view.id());
1914                 for (ModuleId alias : view.aliases()) {
1915                     providingModuleIds.put(alias, view.id());
1916                     addModuleId(alias);
1917                 }
1918             }
1919         }
1920 
1921         void remove(ModuleInfo mi) throws IOException {
1922             modules.remove(mi.id());
1923             for (ModuleView view : mi.views()) {
1924                 providingModuleIds.remove(view.id());
1925                 Set<ModuleId> mids = moduleIdsForName.get(view.id().name());
1926                 if (mids != null)
1927                     mids.remove(view.id());
1928                 for (ModuleId alias : view.aliases()) {
1929                     providingModuleIds.remove(alias);
1930                     mids = moduleIdsForName.get(alias.name());
1931                     if (mids != null)
1932                         mids.remove(view.id());
1933                 }
1934             }
1935             File md = moduleDir(root, mi.id());
1936             delete(md);
1937         }
1938 
1939         private void delete(File md) throws IOException {
1940             if (!md.exists())
1941                 return;
1942 
1943             checkModuleDir(md);
1944             ModuleFile.Reader.remove(md);
1945             File parent = md.getParentFile();
1946             if (parent.list().length == 0)
1947                 parent.delete();
1948         }
1949 
1950         void refresh() throws IOException {
1951             providingModuleIds = new LinkedHashMap<>();
1952             moduleIdsForName = new LinkedHashMap<>();
1953             modules = new HashSet<>();
1954 
1955             try (DirectoryStream<Path> ds = java.nio.file.Files.newDirectoryStream(root.toPath())) {
1956                 for (Path mnp : ds) {
1957                     String mn = mnp.toFile().getName();
1958                     if (mn.startsWith(FileConstants.META_PREFIX) ||
1959                         TRASH.equals(mn)) {
1960                         continue;
1961                     }
1962 
1963                     try (DirectoryStream<Path> mds = java.nio.file.Files.newDirectoryStream(mnp)) {
1964                         for (Path versionp : mds) {
1965                             File v = versionp.toFile();
1966                             if (!v.isDirectory()) {
1967                                 throw new IOException(versionp + ": Not a directory");
1968                             }
1969                             modules.add(jms.parseModuleId(mn, v.getName()));
1970                         }
1971                     }
1972                 }
1973             }
1974             for (ModuleId mid : modules) {
1975                 byte[] bs = Files.load(new File(moduleDir(root, mid), "info"));
1976                 ModuleInfo mi = jms.parseModuleInfo(bs);
1977                 addToDirectory(mi);
1978             }
1979         }
1980 
1981         private File ensureNewModule(ModuleInfo mi)
1982                 throws ConfigurationException, IOException
1983         {
1984             for (ModuleView view : mi.views()) {
1985                 if (providingModuleIds.containsKey(view.id())) {
1986                     throw new ConfigurationException("module view " + view.id()
1987                             + " already installed");
1988                 }
1989                 for (ModuleId alias : view.aliases()) {
1990                     ModuleId mid = alias;
1991                     if (providingModuleIds.containsKey(mid)) {
1992                         throw new ConfigurationException("alias " + alias
1993                                 + " already installed");
1994                     }
1995                 }
1996             }
1997             File md = moduleDir(root, mi.id());
1998             if (md.exists()) {
1999                 throw new ConfigurationException("module " + mi.id()
2000                         + " already installed");
2001             }
2002             if (!md.mkdirs()) {
2003                 throw new IOException(md + ": Cannot create");
2004             }
2005             return md;
2006         }
2007     }
2008 
2009     // -- Repositories --
2010 
2011     private static class RepoList
2012         implements RemoteRepositoryList
2013     {
2014 
2015         private static final int MINOR_VERSION = 0;
2016         private static final int MAJOR_VERSION = 0;
2017 
2018         private final File root;
2019         private final File listFile;
2020 
2021         private RepoList(File r) {
2022             root = new File(r, FileConstants.META_PREFIX + "repos");
2023             listFile = new File(root, FileConstants.META_PREFIX + "list");
2024         }
2025 
2026         private static FileHeader fileHeader() {
2027             return (new FileHeader()
2028                     .type(FileConstants.Type.REMOTE_REPO_LIST)
2029                     .majorVersion(MAJOR_VERSION)
2030                     .minorVersion(MINOR_VERSION));
2031         }
2032 
2033         private List<RemoteRepository> repos = null;
2034         private long nextRepoId = 0;
2035 
2036         private File repoDir(long id) {
2037             return new File(root, Long.toHexString(id));
2038         }
2039 
2040         private void load() throws IOException {
2041 
2042             repos = new ArrayList<>();
2043             if (!root.exists() || !listFile.exists())
2044                 return;
2045             FileInputStream fin = new FileInputStream(listFile);
2046             DataInputStream in
2047                 = new DataInputStream(new BufferedInputStream(fin));
2048             try {
2049 
2050                 FileHeader fh = fileHeader();
2051                 fh.read(in);
2052                 nextRepoId = in.readLong();
2053                 int n = in.readInt();
2054                 long[] ids = new long[n];
2055                 for (int i = 0; i < n; i++)
2056                     ids[i] = in.readLong();
2057                 RemoteRepository parent = null;
2058 
2059                 // Load in reverse order so that parents are correct
2060                 for (int i = n - 1; i >= 0; i--) {
2061                     long id = ids[i];
2062                     RemoteRepository rr
2063                         = RemoteRepository.open(repoDir(id), id, parent);
2064                     repos.add(rr);
2065                     parent = rr;
2066                 }
2067                 Collections.reverse(repos);
2068 
2069             } finally {
2070                 in.close();
2071             }
2072 
2073         }
2074 
2075         private List<RemoteRepository> roRepos = null;
2076 
2077         // Unmodifiable
2078         public List<RemoteRepository> repositories() throws IOException {
2079             if (repos == null) {
2080                 load();
2081                 roRepos = Collections.unmodifiableList(repos);
2082             }
2083             return roRepos;
2084         }
2085 
2086         public RemoteRepository firstRepository() throws IOException {
2087             repositories();
2088             return repos.isEmpty() ? null : repos.get(0);
2089         }
2090 
2091         private void store() throws IOException {
2092             File newfn = new File(root, "list.new");
2093             FileOutputStream fout = new FileOutputStream(newfn);
2094             DataOutputStream out
2095                 = new DataOutputStream(new BufferedOutputStream(fout));
2096             try {
2097                 try {
2098                     fileHeader().write(out);
2099                     out.writeLong(nextRepoId);
2100                     out.writeInt(repos.size());
2101                     for (RemoteRepository rr : repos)
2102                         out.writeLong(rr.id());
2103                 } finally {
2104                     out.close();
2105                 }
2106             } catch (IOException x) {
2107                 newfn.delete();
2108                 throw x;
2109             }
2110             java.nio.file.Files.move(newfn.toPath(), listFile.toPath(), ATOMIC_MOVE);
2111         }
2112 
2113         public RemoteRepository add(URI u, int position)
2114             throws IOException
2115         {
2116 
2117             if (repos == null)
2118                 load();
2119             for (RemoteRepository rr : repos) {
2120                 if (rr.location().equals(u)) // ## u not canonical
2121                     throw new IllegalStateException(u + ": Already in"
2122                                                     + " repository list");
2123             }
2124             if (!root.exists()) {
2125                 if (!root.mkdir())
2126                     throw new IOException(root + ": Cannot create directory");
2127             }
2128 
2129             if (repos.size() == Integer.MAX_VALUE)
2130                 throw new IllegalStateException("Too many repositories");
2131             if (position < 0)
2132                 throw new IllegalArgumentException("Invalid index");
2133 
2134             long id = nextRepoId++;
2135             RemoteRepository rr = RemoteRepository.create(repoDir(id), u, id);
2136             try {
2137                 rr.updateCatalog(true);
2138             } catch (IOException x) {
2139                 rr.delete();
2140                 nextRepoId--;
2141                 throw x;
2142             }
2143 
2144             if (position >= repos.size()) {
2145                 repos.add(rr);
2146             } else if (position >= 0) {
2147                 List<RemoteRepository> prefix
2148                     = new ArrayList<>(repos.subList(0, position));
2149                 List<RemoteRepository> suffix
2150                     = new ArrayList<>(repos.subList(position, repos.size()));
2151                 repos.clear();
2152                 repos.addAll(prefix);
2153                 repos.add(rr);
2154                 repos.addAll(suffix);
2155             }
2156             store();
2157 
2158             return rr;
2159 
2160         }
2161 
2162         public boolean remove(RemoteRepository rr)
2163             throws IOException
2164         {
2165             if (!repos.remove(rr))
2166                 return false;
2167             store();
2168             File rd = repoDir(rr.id());
2169             for (File f : rd.listFiles()) {
2170                 if (!f.delete())
2171                     throw new IOException(f + ": Cannot delete");
2172             }
2173             if (!rd.delete())
2174                 throw new IOException(rd + ": Cannot delete");
2175             return true;
2176         }
2177 
2178         public boolean areCatalogsStale() throws IOException {
2179             for (RemoteRepository rr : repos) {
2180                 if (rr.isCatalogStale())
2181                     return true;
2182             }
2183             return false;
2184         }
2185 
2186         public boolean updateCatalogs(boolean force) throws IOException {
2187             boolean updated = false;
2188             for (RemoteRepository rr : repos) {
2189                 if (rr.updateCatalog(force))
2190                     updated = true;
2191             }
2192             return updated;
2193         }
2194 
2195     }
2196 
2197     private RemoteRepositoryList repoList = null;
2198 
2199     public RemoteRepositoryList repositoryList()
2200         throws IOException
2201     {
2202         if (repoList == null)
2203             repoList = new RepoList(root);
2204         return repoList;
2205     }
2206 
2207 }