1 /*
   2  * Copyright (c) 2009, 2011, 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.security.*;
  32 import java.security.cert.*;
  33 import java.util.*;
  34 import java.util.jar.*;
  35 import java.util.zip.*;
  36 
  37 import static java.nio.file.StandardCopyOption.*;
  38 
  39 /**
  40  * A simple module library which stores data directly in the filesystem
  41  *
  42  * @see Library
  43  */
  44 
  45 // ## TODO: Move remaining parent-searching logic upward into Library class
  46 
  47 // On-disk library layout
  48 //
  49 //   $LIB/%jigsaw-library
  50 //        com.foo.bar/1.2.3/info (= module-info.class)
  51 //                          index (list of defined classes)
  52 //                          config (resolved configuration, if a root)
  53 //                          classes/com/foo/bar/...
  54 //                          resources/com/foo/bar/...
  55 //                          lib/libbar.so
  56 //                          bin/bar
  57 //                          signer (signer's certchain & timestamp)
  58 //
  59 // ## Issue: Concurrent access to the module library
  60 // ## e.g. a module is being removed while a running application
  61 // ## is depending on it
  62 
  63 public final class SimpleLibrary
  64     extends Library
  65 {
  66 
  67     private static abstract class MetaData {
  68 
  69         protected final int maxMajorVersion;
  70         protected final int maxMinorVersion;
  71         protected int majorVersion;
  72         protected int minorVersion;
  73         private FileConstants.Type type;
  74         private File file;
  75 
  76         protected MetaData(int maxMajor, int maxMinor,
  77                            FileConstants.Type t, File f)
  78         {
  79             maxMajorVersion = majorVersion = maxMajor;
  80             maxMinorVersion = minorVersion = maxMinor;
  81             type = t;
  82             file = f;
  83         }
  84 
  85         protected abstract void storeRest(DataOutputStream out)
  86             throws IOException;
  87 
  88         void store() throws IOException {
  89             OutputStream fo = new FileOutputStream(file);
  90             DataOutputStream out
  91                 = new DataOutputStream(new BufferedOutputStream(fo));
  92             try {
  93                 out.writeInt(FileConstants.MAGIC);
  94                 out.writeShort(type.value());
  95                 out.writeShort(majorVersion);
  96                 out.writeShort(minorVersion);
  97                 storeRest(out);
  98             } finally {
  99                 out.close();
 100             }
 101         }
 102 
 103         protected abstract void loadRest(DataInputStream in)
 104             throws IOException;
 105 
 106         protected void load() throws IOException {
 107             InputStream fi = new FileInputStream(file);
 108             try {
 109                 DataInputStream in
 110                     = new DataInputStream(new BufferedInputStream(fi));
 111                 int m = in.readInt();
 112                 if (m != FileConstants.MAGIC)
 113                     throw new IOException(file + ": Invalid magic number");
 114                 int typ = in.readShort();
 115                 if (typ != type.value())
 116                     throw new IOException(file + ": Invalid file type");
 117                 int maj = in.readShort();
 118                 int min = in.readShort();
 119                 if (   maj > maxMajorVersion
 120                     || (maj == maxMajorVersion && min > maxMinorVersion)) {
 121                     throw new IOException(file
 122                                           + ": Futuristic version number");
 123                 }
 124                 majorVersion = maj;
 125                 minorVersion = min;
 126                 loadRest(in);
 127             } catch (EOFException x) {
 128                 throw new IOException(file + ": Invalid library metadata",
 129                                       x);
 130             } finally {
 131                 fi.close();
 132             }
 133         }
 134 
 135     }
 136 
 137     /**
 138      * Defines the storage options that SimpleLibrary supports.
 139      */
 140     public static enum StorageOption {
 141         DEFLATED,
 142     }
 143 
 144     private static final class Header
 145         extends MetaData
 146     {
 147         private static final String FILE
 148             = FileConstants.META_PREFIX + "jigsaw-library";
 149 
 150         private static final int MAJOR_VERSION = 0;
 151         private static final int MINOR_VERSION = 1;
 152 
 153         private static final int DEFLATED = 1 << 0;
 154 
 155         private File parent;
 156         // location of native libs for this library (may be outside the library)
 157         // null:default, to use a per-module 'lib' directory
 158         private File natlibs;
 159         // location of native cmds for this library (may be outside the library)
 160         // null:default, to use a per-module 'bin' directory
 161         private File natcmds;
 162 
 163         private Set<StorageOption> opts;
 164 
 165         public File parent() { return parent; }
 166         public File natlibs() { return natlibs; }
 167         public File natcmds() { return natcmds; }
 168         public boolean isDeflated() {
 169            return opts.contains(StorageOption.DEFLATED);
 170         }
 171 
 172         private Header(File root, File p, File natlibs, File natcmds,
 173                        Set<StorageOption> opts) {
 174             super(MAJOR_VERSION, MINOR_VERSION,
 175                   FileConstants.Type.LIBRARY_HEADER,
 176                   new File(root, FILE));
 177             this.parent = p;
 178             this.natlibs = natlibs;
 179             this.natcmds = natcmds;
 180             this.opts = new HashSet<>(opts);
 181         }
 182 
 183         private Header(File root) {
 184             this(root, null, null, null, Collections.<StorageOption>emptySet());
 185         }
 186 
 187         protected void storeRest(DataOutputStream out)
 188             throws IOException
 189         {
 190             int flags = 0;
 191             if (isDeflated())
 192                 flags |= DEFLATED;
 193             out.writeShort(flags);
 194             out.writeByte((parent != null) ? 1 : 0);
 195             if (parent != null)
 196                 out.writeUTF(parent.toString());
 197             out.writeByte((natlibs != null) ? 1 : 0);
 198             if (natlibs != null)
 199                 out.writeUTF(natlibs.toString());
 200             out.writeByte((natcmds != null) ? 1 : 0);
 201             if (natcmds != null)
 202                 out.writeUTF(natcmds.toString());
 203         }
 204 
 205         protected void loadRest(DataInputStream in)
 206             throws IOException
 207         {
 208             opts = new HashSet<StorageOption>();
 209             int flags = in.readShort();
 210             if ((flags & DEFLATED) == DEFLATED)
 211                 opts.add(StorageOption.DEFLATED);
 212             int b = in.readByte();
 213             if (b != 0)
 214                 parent = new File(in.readUTF());
 215             b = in.readByte();
 216             if (b != 0)
 217                 natlibs = new File(in.readUTF());
 218             b = in.readByte();
 219             if (b != 0)
 220                 natcmds = new File(in.readUTF());
 221         }
 222 
 223         private static Header load(File f)
 224             throws IOException
 225         {
 226             Header h = new Header(f);
 227             h.load();
 228             return h;
 229         }
 230 
 231     }
 232 
 233     private final File root;
 234     private final File canonicalRoot;
 235     private File parentPath;
 236     private File natlibs;
 237     private File natcmds;
 238     private SimpleLibrary parent;
 239     private final Header hd;
 240 
 241     public String name() { return root.toString(); }
 242     public File root() { return canonicalRoot; }
 243     public int majorVersion() { return hd.majorVersion; }
 244     public int minorVersion() { return hd.minorVersion; }
 245     public SimpleLibrary parent() { return parent; }
 246     public File natlibs() { return natlibs; }
 247     public File natcmds() { return natcmds; }
 248     public boolean isDeflated() { return hd.isDeflated(); }
 249 
 250     private URI location = null;
 251     public URI location() {
 252         if (location == null)
 253             location = root().toURI();
 254         return location;
 255     }
 256 
 257     @Override
 258     public String toString() {
 259         return (this.getClass().getName()
 260                 + "[" + canonicalRoot
 261                 + ", v" + hd.majorVersion + "." + hd.minorVersion + "]");
 262     }
 263 
 264     private static void ensureDirectory(File dir) throws IOException {
 265         if (!dir.isDirectory())
 266             throw new IOException(dir + ": Not a directory");
 267     }
 268 
 269     private static void warnWriteable(File dir) {
 270         if (!dir.canWrite())
 271             // TODO: where to write the warning message???
 272             System.out.println("Warning: " + dir + " is not writeable.");
 273     }
 274 
 275     private SimpleLibrary(File path, boolean create, File parentPath,
 276                           File natlibs, File natcmds, Set<StorageOption> opts)
 277         throws IOException
 278     {
 279         root = path;
 280         canonicalRoot = root.getCanonicalFile();
 281         boolean exists = root.exists();
 282         boolean directory = root.isDirectory();
 283         if (!create) {
 284             if (!exists)
 285                 throw new FileNotFoundException(root.toString());
 286             if (!directory)
 287                 throw new IOException(root + ": Exists but is not a directory");
 288             hd = Header.load(root);
 289             if (hd.parent() != null) {
 290                 parent = open(hd.parent());
 291                 this.parentPath = hd.parent();
 292             }
 293             if (hd.natlibs() != null)
 294                 this.natlibs = hd.natlibs();
 295             if (hd.natcmds() != null)
 296                 this.natcmds = hd.natcmds();
 297             return;
 298         }
 299         // create
 300         if (exists) {
 301             if (!directory)
 302                 throw new IOException(root + ": Not a directory");
 303             else if (root.list().length != 0)
 304                 throw new IOException(root + ": Already Exists");
 305         } else if (!root.mkdirs())
 306             throw new IOException(root + ": Cannot create library directory");
 307         if (parentPath != null) {
 308             this.parent = open(parentPath);
 309             this.parentPath = this.parent.root();
 310         }
 311         if (natlibs != null) {
 312             // resolve against the working dir, and store the absolute path
 313             this.natlibs = natlibs.getCanonicalFile();
 314             if (!this.natlibs.exists()) {
 315                 if (!this.natlibs.mkdir())
 316                     throw new IOException(this.natlibs + ": Cannot create lib directory");
 317             } else {
 318                 ensureDirectory(this.natlibs);
 319                 warnWriteable(this.natlibs);
 320             }
 321         }
 322         if (natcmds != null) {
 323             this.natcmds = natcmds.getCanonicalFile();
 324             if (!this.natcmds.exists()) {
 325                 if (!this.natcmds.mkdir())
 326                     throw new IOException(this.natcmds + ": Cannot create cmd directory");
 327             } else {
 328                 ensureDirectory(this.natcmds);
 329                 warnWriteable(this.natcmds);
 330             }
 331         }
 332         hd = new Header(canonicalRoot, this.parentPath, this.natlibs,
 333                         this.natcmds, opts);
 334         hd.store();
 335     }
 336 
 337     public static SimpleLibrary create(File path, File parent, File natlibs,
 338                                        File natcmds, Set<StorageOption> opts)
 339         throws IOException
 340     {
 341         return new SimpleLibrary(path, true, parent, natlibs, natcmds, opts);
 342     }
 343 
 344     public static SimpleLibrary create(File path, File parent,
 345                                        Set<StorageOption> opts)
 346         throws IOException
 347     {
 348         return new SimpleLibrary(path, true, parent, null, null, opts);
 349     }
 350 
 351     public static SimpleLibrary create(File path, File parent)
 352         throws IOException
 353     {
 354         return new SimpleLibrary(path, true, parent, null, null,
 355                                  Collections.<StorageOption>emptySet());
 356     }
 357 
 358     public static SimpleLibrary create(File path, Set<StorageOption> opts)
 359         throws IOException
 360     {
 361         // ## Should default parent to $JAVA_HOME/lib/modules
 362         return new SimpleLibrary(path, true, null, null, null, opts);
 363     }
 364 
 365     public static SimpleLibrary open(File path)
 366         throws IOException
 367     {
 368         return new SimpleLibrary(path, false, null, null, null,
 369                                  Collections.<StorageOption>emptySet());
 370     }
 371 
 372     private static final JigsawModuleSystem jms
 373         = JigsawModuleSystem.instance();
 374 
 375     private static final class Index
 376         extends MetaData
 377     {
 378 
 379         private static String FILE = "index";
 380 
 381         private static int MAJOR_VERSION = 0;
 382         private static int MINOR_VERSION = 1;
 383 
 384         private Set<String> publicClasses;
 385         public Set<String> publicClasses() { return publicClasses; }
 386 
 387         private Set<String> otherClasses;
 388         public Set<String> otherClasses() { return otherClasses; }
 389 
 390         private Index(File root) {
 391             super(MAJOR_VERSION, MINOR_VERSION,
 392                   FileConstants.Type.LIBRARY_MODULE_INDEX,
 393                   new File(root, FILE));
 394             // Unsorted on input, because we don't need it sorted
 395             publicClasses = new HashSet<String>();
 396             otherClasses = new HashSet<String>();
 397         }
 398 
 399         private void storeSet(Set<String> cnset, DataOutputStream out)
 400             throws IOException
 401         {
 402             // Sorted on output, because we can afford it
 403             List<String> cns = new ArrayList<String>(cnset);
 404             Collections.sort(cns);
 405             out.writeInt(cns.size());
 406             for (String cn : cns)
 407                 out.writeUTF(cn);
 408         }
 409 
 410         protected void storeRest(DataOutputStream out)
 411             throws IOException
 412         {
 413             storeSet(publicClasses, out);
 414             storeSet(otherClasses, out);
 415         }
 416 
 417         private void loadSet(DataInputStream in, Set<String> cnset)
 418             throws IOException
 419         {
 420             int n = in.readInt();
 421             for (int i = 0; i < n; i++)
 422                 cnset.add(in.readUTF());
 423         }
 424 
 425         protected void loadRest(DataInputStream in)
 426             throws IOException
 427         {
 428             loadSet(in, publicClasses);
 429             loadSet(in, otherClasses);
 430         }
 431 
 432         private static Index load(File f)
 433             throws IOException
 434         {
 435             Index ix = new Index(f);
 436             ix.load();
 437             return ix;
 438         }
 439 
 440     }
 441 
 442     private static final class StoredConfiguration
 443         extends MetaData
 444     {
 445 
 446         private static String FILE = "config";
 447 
 448         private static int MAJOR_VERSION = 0;
 449         private static int MINOR_VERSION = 1;
 450 
 451         private Configuration<Context> cf;
 452 
 453         private static void delete(File root) {
 454             new File(root, FILE).delete();
 455         }
 456 
 457         private StoredConfiguration(File root, Configuration<Context> conf)
 458         {
 459             super(MAJOR_VERSION, MINOR_VERSION,
 460                   FileConstants.Type.LIBRARY_MODULE_CONFIG,
 461                   new File(root, FILE));
 462             cf = conf;
 463         }
 464 
 465         protected void storeRest(DataOutputStream out)
 466             throws IOException
 467         {
 468             assert cf.roots().size() == 1;
 469             out.writeUTF(cf.roots().iterator().next().toString());
 470             // Contexts
 471             out.writeInt(cf.contexts().size());
 472             for (Context cx : cf.contexts()) {
 473                 out.writeUTF(cx.name());
 474                 // Module ids, and their libraries
 475                 out.writeInt(cx.modules().size());
 476                 for (ModuleId mid : cx.modules()) {
 477                     out.writeUTF(mid.toString());
 478                     File lp = cx.findLibraryPathForModule(mid);
 479                     if (lp == null)
 480                         out.writeUTF("");
 481                     else
 482                         out.writeUTF(lp.toString());
 483                 }
 484                 // Local class map
 485                 out.writeInt(cx.localClasses().size());
 486                 for (Map.Entry<String,ModuleId> me
 487                          : cx.moduleForLocalClassMap().entrySet()) {
 488                     out.writeUTF(me.getKey());
 489                     out.writeUTF(me.getValue().toString());
 490                 }
 491                 // Remote package map
 492                 out.writeInt(cx.contextForRemotePackageMap().size());
 493                 for (Map.Entry<String,String> me
 494                          : cx.contextForRemotePackageMap().entrySet()) {
 495                     out.writeUTF(me.getKey());
 496                     out.writeUTF(me.getValue());
 497                 }
 498                 // Suppliers
 499                 out.writeInt(cx.remoteContexts().size());
 500                 for (String cxn : cx.remoteContexts()) {
 501                     out.writeUTF(cxn);
 502                 }
 503             }
 504         }
 505 
 506         protected void loadRest(DataInputStream in)
 507             throws IOException
 508         {
 509             String root = in.readUTF();
 510             ModuleId rmid = jms.parseModuleId(root);
 511             cf = new Configuration<Context>(rmid);
 512             // Contexts
 513             int nContexts = in.readInt();
 514             for (int i = 0; i < nContexts; i++) {
 515                 Context cx = new Context();
 516                 String cxn = in.readUTF();
 517                 // Module ids
 518                 int nModules = in.readInt();
 519                 for (int j = 0; j < nModules; j++) {
 520                     ModuleId mid = jms.parseModuleId(in.readUTF());
 521                     cx.add(mid);
 522                     cf.put(mid.name(), cx);
 523                     String lps = in.readUTF();
 524                     if (lps.length() > 0)
 525                         cx.putLibraryPathForModule(mid, new File(lps));
 526                 }
 527                 cx.freeze();
 528                 assert cx.name().equals(cxn);
 529                 cf.add(cx);
 530                 // Local class map
 531                 int nClasses = in.readInt();
 532                 for (int j = 0; j < nClasses; j++)
 533                     cx.putModuleForLocalClass(in.readUTF(),
 534                                               jms.parseModuleId(in.readUTF()));
 535                 // Remote package map
 536                 int nPackages = in.readInt();
 537                 for (int j = 0; j < nPackages; j++)
 538                     cx.putContextForRemotePackage(in.readUTF(), in.readUTF());
 539 
 540                 // Suppliers
 541                 int nSuppliers = in.readInt();
 542                 for (int j = 0; j < nSuppliers; j++)
 543                     cx.addSupplier(in.readUTF());
 544             }
 545 
 546         }
 547 
 548         private static StoredConfiguration load(File f)
 549             throws IOException
 550         {
 551             StoredConfiguration sp = new StoredConfiguration(f, null);
 552             sp.load();
 553             return sp;
 554         }
 555 
 556     }
 557 
 558     private static final class Signers
 559         extends MetaData {
 560 
 561         private static String FILE = "signer";
 562         private static int MAJOR_VERSION = 0;
 563         private static int MINOR_VERSION = 1;
 564 
 565         private CertificateFactory cf = null;
 566         private Set<CodeSigner> signers;
 567         private Set<CodeSigner> signers() { return signers; }
 568 
 569         private Signers(File root, Set<CodeSigner> signers) {
 570             super(MAJOR_VERSION, MINOR_VERSION,
 571                   FileConstants.Type.LIBRARY_MODULE_SIGNER,
 572                   new File(root, FILE));
 573             this.signers = signers;
 574         }
 575 
 576         protected void storeRest(DataOutputStream out)
 577             throws IOException
 578         {
 579             out.writeInt(signers.size());
 580             for (CodeSigner signer : signers) {
 581                 try {
 582                     CertPath signerCertPath = signer.getSignerCertPath();
 583                     out.write(signerCertPath.getEncoded("PkiPath"));
 584                     Timestamp ts = signer.getTimestamp();
 585                     out.writeByte((ts != null) ? 1 : 0);
 586                     if (ts != null) {
 587                         out.writeLong(ts.getTimestamp().getTime());
 588                         out.write(ts.getSignerCertPath().getEncoded("PkiPath"));
 589                     }
 590                 } catch (CertificateEncodingException cee) {
 591                     throw new IOException(cee);
 592                 }
 593             }
 594         }
 595 
 596         protected void loadRest(DataInputStream in)
 597             throws IOException
 598         {
 599             int size = in.readInt();
 600             for (int i = 0; i < size; i++) {
 601                 try {
 602                     if (cf == null)
 603                         cf = CertificateFactory.getInstance("X.509");
 604                     CertPath signerCertPath = cf.generateCertPath(in, "PkiPath");
 605                     int b = in.readByte();
 606                     if (b != 0) {
 607                         Date timestamp = new Date(in.readLong());
 608                         CertPath tsaCertPath = cf.generateCertPath(in, "PkiPath");
 609                         Timestamp ts = new Timestamp(timestamp, tsaCertPath);
 610                         signers.add(new CodeSigner(signerCertPath, ts));
 611                     } else {
 612                         signers.add(new CodeSigner(signerCertPath, null));
 613                     }
 614                 } catch (CertificateException ce) {
 615                     throw new IOException(ce);
 616                 }
 617             }
 618         }
 619 
 620         private static Signers load(File f)
 621             throws IOException
 622         {
 623             Signers signers = new Signers(f, new HashSet<CodeSigner>());
 624             signers.load();
 625             return signers;
 626         }
 627     }
 628 
 629     private void gatherLocalModuleIds(File mnd, Set<ModuleId> mids)
 630         throws IOException
 631     {
 632         if (!mnd.isDirectory())
 633             throw new IOException(mnd + ": Not a directory");
 634         if (!mnd.canRead())
 635             throw new IOException(mnd + ": Not readable");
 636         for (String v : mnd.list()) {
 637             mids.add(jms.parseModuleId(mnd.getName(), v));
 638         }
 639     }
 640 
 641     private void gatherLocalModuleIds(Set<ModuleId> mids)
 642         throws IOException
 643     {
 644         File[] mnds = root.listFiles();
 645         for (File mnd : mnds) {
 646             if (mnd.getName().startsWith(FileConstants.META_PREFIX))
 647                 continue;
 648             gatherLocalModuleIds(mnd, mids);
 649         }
 650     }
 651 
 652     protected void gatherLocalModuleIds(String moduleName,
 653                                         Set<ModuleId> mids)
 654         throws IOException
 655     {
 656         if (moduleName == null) {
 657             gatherLocalModuleIds(mids);
 658             return;
 659         }
 660         File mnd = new File(root, moduleName);
 661         if (mnd.exists())
 662             gatherLocalModuleIds(mnd, mids);
 663     }
 664 
 665     private void checkModuleId(ModuleId mid) {
 666         Version v = mid.version();
 667         if (v == null)
 668             return;
 669         if (!(v instanceof JigsawVersion))
 670             throw new IllegalArgumentException(mid + ": Not a Jigsaw module id");
 671     }
 672 
 673     private File moduleDir(ModuleId mid) {
 674         Version v = mid.version();
 675         String vs = (v != null) ? v.toString() : "default";
 676         return new File(new File(root, mid.name()), vs);
 677     }
 678 
 679     private void checkModuleDir(File md)
 680         throws IOException
 681     {
 682         if (!md.isDirectory())
 683             throw new IOException(md + ": Not a directory");
 684         if (!md.canRead())
 685             throw new IOException(md + ": Not readable");
 686     }
 687 
 688     private File findModuleDir(ModuleId mid)
 689         throws IOException
 690     {
 691         checkModuleId(mid);
 692         File md = moduleDir(mid);
 693         if (!md.exists())
 694             return null;
 695         checkModuleDir(md);
 696         return md;
 697     }
 698 
 699     private void deleteModuleDir(ModuleId mid)
 700         throws IOException
 701     {
 702         File md = findModuleDir(mid);
 703         if (md == null)
 704             return;
 705         Files.deleteTree(md);
 706         File mnd = md.getParentFile();
 707         if (mnd.list().length == 0) {
 708             if (!mnd.delete())
 709                 throw new IOException(mnd + ": Cannot delete");
 710         }
 711     }
 712 
 713     public byte[] readLocalModuleInfoBytes(ModuleId mid)
 714         throws IOException
 715     {
 716         File md = findModuleDir(mid);
 717         if (md == null)
 718             return null;
 719         return Files.load(new File(md, "info"));
 720     }
 721 
 722     public CodeSigner[] readLocalCodeSigners(ModuleId mid)
 723         throws IOException
 724     {
 725         File md = findModuleDir(mid);
 726         if (md == null)
 727             return null;
 728         // Only one signer is currently supported
 729         File f = new File(md, "signer");
 730         // ## concurrency issues : what is the expected behavior if file is
 731         // ## removed by another thread/process here?
 732         if (!f.exists())
 733             return null;
 734         return Signers.load(md).signers().toArray(new CodeSigner[0]);
 735     }
 736 
 737     // ## Close all zip files when we close this library
 738     private Map<ModuleId, Object> contentForModule = new HashMap<>();
 739     private Object NONE = new Object();
 740 
 741     private Object findContent(ModuleId mid)
 742         throws IOException
 743     {
 744         Object o = contentForModule.get(mid);
 745         if (o != null)
 746             return o;
 747         if (o == NONE)
 748             return null;
 749         File md = findModuleDir(mid);
 750         if (md == null) {
 751             contentForModule.put(mid, NONE);
 752             return null;
 753         }
 754         File cf = new File(md, "classes");
 755         if (cf.isFile()) {
 756             ZipFile zf = new ZipFile(cf);
 757             contentForModule.put(mid, zf);
 758             return zf;
 759         }
 760         if (cf.isDirectory()) {
 761             contentForModule.put(mid, cf);
 762             return cf;
 763         }
 764         contentForModule.put(mid, NONE);
 765         return null;
 766     }
 767 
 768     private byte[] loadContent(ZipFile zf, String path)
 769         throws IOException
 770     {
 771         ZipEntry ze = zf.getEntry(path);
 772         if (ze == null)
 773             return null;
 774         return Files.load(zf.getInputStream(ze), (int)ze.getSize());
 775     }
 776 
 777     private byte[] loadContent(ModuleId mid, String path)
 778         throws IOException
 779     {
 780         Object o = findContent(mid);
 781         if (o == null)
 782             return null;
 783         if (o instanceof ZipFile) {
 784             ZipFile zf = (ZipFile)o;
 785             ZipEntry ze = zf.getEntry(path);
 786             if (ze == null)
 787                 return null;
 788             return Files.load(zf.getInputStream(ze), (int)ze.getSize());
 789         }
 790         if (o instanceof File) {
 791             File f = new File((File)o, path);
 792             if (!f.exists())
 793                 return null;
 794             return Files.load(f);
 795         }
 796         assert false;
 797         return null;
 798     }
 799 
 800     private URI locateContent(ModuleId mid, String path)
 801         throws IOException
 802     {
 803         Object o = findContent(mid);
 804         if (o == null)
 805             return null;
 806         if (o instanceof ZipFile) {
 807             ZipFile zf = (ZipFile)o;
 808             ZipEntry ze = zf.getEntry(path);
 809             if (ze == null)
 810                 return null;
 811             return URI.create("jar:"
 812                               + new File(zf.getName()).toURI().toString()
 813                               + "!/" + path);
 814         }
 815         if (o instanceof File) {
 816             File f = new File((File)o, path);
 817             if (!f.exists())
 818                 return null;
 819             return f.toURI();
 820         }
 821         assert false;
 822         return null;
 823     }
 824 
 825     public byte[] readLocalClass(ModuleId mid, String className)
 826         throws IOException
 827     {
 828         return loadContent(mid, className.replace('.', '/') + ".class");
 829     }
 830 
 831     public List<String> listLocalClasses(ModuleId mid, boolean all)
 832         throws IOException
 833     {
 834         File md = findModuleDir(mid);
 835         if (md == null)
 836             return null;
 837         Index ix = Index.load(md);
 838         int os = all ? ix.otherClasses().size() : 0;
 839         ArrayList<String> cns
 840             = new ArrayList<String>(ix.publicClasses().size() + os);
 841         cns.addAll(ix.publicClasses());
 842         if (all)
 843             cns.addAll(ix.otherClasses());
 844         return cns;
 845     }
 846 
 847     public Configuration<Context> readConfiguration(ModuleId mid)
 848         throws IOException
 849     {
 850         File md = findModuleDir(mid);
 851         if (md == null) {
 852             if (parent != null)
 853                 return parent.readConfiguration(mid);
 854             return null;
 855         }
 856         StoredConfiguration scf = StoredConfiguration.load(md);
 857         return scf.cf;
 858     }
 859 
 860     private boolean addToIndex(ClassInfo ci, Index ix)
 861         throws IOException
 862     {
 863         if (ci.isModuleInfo())
 864             return false;
 865         if (ci.moduleName() != null) {
 866             // ## From early Jigsaw development; can probably delete now
 867             throw new IOException("Old-style class file with"
 868                                   + " module attribute");
 869         }
 870         if (ci.isPublic())
 871             ix.publicClasses().add(ci.name());
 872         else
 873             ix.otherClasses().add(ci.name());
 874         return true;
 875     }
 876 
 877     private void reIndex(ModuleId mid)
 878         throws IOException
 879     {
 880 
 881         File md = findModuleDir(mid);
 882         if (md == null)
 883             throw new IllegalArgumentException(mid + ": No such module");
 884         File cd = new File(md, "classes");
 885         final Index ix = new Index(md);
 886 
 887         if (cd.isDirectory()) {
 888             Files.walkTree(cd, new Files.Visitor<File>() {
 889                 public void accept(File f) throws IOException {
 890                     if (f.getPath().endsWith(".class"))
 891                         addToIndex(ClassInfo.read(f), ix);
 892                 }
 893             });
 894         } else if (cd.isFile()) {
 895             FileInputStream fis = new FileInputStream(cd);
 896             ZipInputStream zis = new ZipInputStream(fis);
 897             ZipEntry ze;
 898             while ((ze = zis.getNextEntry()) != null) {
 899                 if (!ze.getName().endsWith(".class"))
 900                     continue;
 901                 addToIndex(ClassInfo.read(Files.nonClosingStream(zis),
 902                                           ze.getSize(),
 903                                           mid + ":" + ze.getName()),
 904                            ix);
 905             }
 906         }
 907 
 908         ix.store();
 909     }
 910 
 911     /**
 912      * Strip the debug attributes from the classes in a given module
 913      * directory.
 914      */
 915     private void strip(File md) throws IOException {
 916         File classes = new File(md, "classes");
 917         if (classes.isFile()) {
 918             File pf = new File(md, "classes.pack");
 919             try (JarFile jf = new JarFile(classes);
 920                 FileOutputStream out = new FileOutputStream(pf))
 921             {
 922                 Pack200.Packer packer = Pack200.newPacker();
 923                 Map<String,String> p = packer.properties();
 924                 p.put("com.sun.java.util.jar.pack.strip.debug", Pack200.Packer.TRUE);
 925                 packer.pack(jf, out);
 926             }
 927 
 928             try (OutputStream out = new FileOutputStream(classes);
 929                  JarOutputStream jos = new JarOutputStream(out))
 930             {
 931                 Pack200.Unpacker unpacker = Pack200.newUnpacker();
 932                 unpacker.unpack(pf, jos);
 933             } finally {
 934                 pf.delete();
 935            }
 936         }
 937     }
 938 
 939     private void install(Manifest mf, File dst, boolean strip)
 940         throws IOException
 941     {
 942         if (mf.classes().size() > 1)
 943             throw new IllegalArgumentException("Multiple module-class"
 944                                                + " directories"
 945                                                + " not yet supported");
 946         if (mf.classes().size() < 1)
 947             throw new IllegalArgumentException("At least one module-class"
 948                                                + " directory required");
 949         File classes = mf.classes().get(0);
 950         final String mn = mf.module();
 951 
 952         File mif = new File(classes, "module-info.class");
 953         File src = null;
 954         if (mif.exists()) {
 955             src = classes;
 956         } else {
 957             src = new File(classes, mn);
 958             mif = new File(src, "module-info.class");
 959         }
 960         byte[] bs =  Files.load(mif);
 961         ModuleInfo mi = jms.parseModuleInfo(bs);
 962         if (!mi.id().name().equals(mn)) {
 963             // ## Need a more appropriate throwable here
 964             throw new Error(mif + " is for module " + mi.id().name()
 965                             + ", not " + mn);
 966         }
 967         String m = mi.id().name();
 968         JigsawVersion v = (JigsawVersion)mi.id().version();
 969         String vs = (v == null) ? "default" : v.toString();
 970         File mdst = new File(new File(dst, m), vs);
 971         if (mdst.exists())
 972             Files.deleteTree(mdst);
 973         Files.mkdirs(mdst, "module");
 974         Files.store(bs, new File(mdst, "info"));
 975         File cldst = new File(mdst, "classes");
 976 
 977         // Delete the config file, if one exists
 978         StoredConfiguration.delete(mdst);
 979 
 980         if (false) {
 981 
 982             // ## Retained for now in case we later want to add an option
 983             // ## to install into a tree rather than a zip file
 984 
 985             // Copy class files and build index
 986             final Index ix = new Index(mdst);
 987             Files.copyTree(src, cldst, new Files.Filter<File>() {
 988                     public boolean accept(File f) throws IOException {
 989                         if (f.isDirectory())
 990                             return true;
 991                         if (f.getName().endsWith(".class")) {
 992                             return addToIndex(ClassInfo.read(f), ix);
 993                         } else {
 994                             return true;
 995                         }
 996                     }});
 997             ix.store();
 998         } else {
 999             FileOutputStream fos
1000                 = new FileOutputStream(new File(mdst, "classes"));
1001             JarOutputStream jos
1002                 = new JarOutputStream(new BufferedOutputStream(fos));
1003             try {
1004 
1005                 // Copy class files and build index
1006                 final Index ix = new Index(mdst);
1007                 Files.storeTree(src, jos, isDeflated(), new Files.Filter<File>() {
1008                         public boolean accept(File f) throws IOException {
1009                             if (f.isDirectory())
1010                                 return true;
1011                             if (f.getName().endsWith(".class")) {
1012                                 return addToIndex(ClassInfo.read(f), ix);
1013                             } else {
1014                                 return true;
1015                             }
1016                         }});
1017                 ix.store();
1018             } finally {
1019                 jos.close();
1020             }
1021             if (strip)
1022                 strip(mdst);
1023         }
1024 
1025     }
1026 
1027     private void install(Collection<Manifest> mfs, File dst, boolean strip)
1028         throws IOException
1029     {
1030         for (Manifest mf : mfs)
1031             install(mf, dst, strip);
1032     }
1033 
1034     public void installFromManifests(Collection<Manifest> mfs, boolean strip)
1035         throws ConfigurationException, IOException
1036     {
1037         install(mfs, root, strip);
1038         configure(null);
1039     }
1040 
1041     @Override
1042     public void installFromManifests(Collection<Manifest> mfs)
1043         throws ConfigurationException, IOException
1044     {
1045         installFromManifests(mfs, false);
1046     }
1047 
1048     private ModuleFileVerifier.Parameters mfvParams;
1049 
1050     private ModuleId install(InputStream is, boolean verifySignature, boolean strip)
1051         throws ConfigurationException, IOException, SignatureException
1052     {
1053         BufferedInputStream bin = new BufferedInputStream(is);
1054         DataInputStream in = new DataInputStream(bin);
1055         File md = null;
1056         try (ModuleFile.Reader mr = new ModuleFile.Reader(in, this)) {
1057             byte[] mib = mr.readStart();
1058             ModuleInfo mi = jms.parseModuleInfo(mib);
1059             md = moduleDir(mi.id());
1060             ModuleId mid = mi.id();
1061             if (md.exists())
1062                 throw new ConfigurationException(mid + ": Already installed");
1063             if (!md.mkdirs())
1064                 throw new IOException(md + ": Cannot create");
1065 
1066             if (verifySignature && mr.hasSignature()) {
1067                 ModuleFileVerifier mfv = new SignedModule.PKCS7Verifier(mr);
1068                 if (mfvParams == null) {
1069                     mfvParams = new SignedModule.VerifierParameters();
1070                 }
1071                 // Verify the module signature and validate the signer's
1072                 // certificate chain
1073                 Set<CodeSigner> signers = mfv.verifySignature(mfvParams);
1074 
1075                 // Verify the module header hash and the module info hash
1076                 mfv.verifyHashesStart(mfvParams);
1077 
1078                 // ## Check policy - is signer trusted and what permissions
1079                 // ## should be granted?
1080 
1081                 // Store signer info
1082                 new Signers(md, signers).store();
1083 
1084                 // Read and verify the rest of the hashes
1085                 mr.readRest(md, isDeflated());
1086                 mfv.verifyHashesRest(mfvParams);
1087             } else {
1088                 mr.readRest(md, isDeflated());
1089             }
1090 
1091             if (strip)
1092                 strip(md);
1093             reIndex(mid);         // ## Could do this while reading module file
1094             return mid;
1095 
1096         } catch (IOException | SignatureException x) {
1097             if (md != null && md.exists()) {
1098                 try {
1099                    ModuleFile.Reader.remove(md);
1100                 } catch (IOException y) {
1101                     y.initCause(x);
1102                     throw y;
1103                 }
1104             }
1105             throw x;
1106         }
1107     }
1108 
1109     private ModuleId installFromJarFile(File mf, boolean verifySignature, boolean strip)
1110         throws ConfigurationException, IOException, SignatureException
1111     {
1112         File md = null;
1113         try (JarFile jf = new JarFile(mf, verifySignature)) {
1114             ModuleInfo mi = jf.getModuleInfo();
1115             if (mi == null)
1116                 throw new ConfigurationException(mf + ": not a modular JAR file");
1117 
1118             md = moduleDir(mi.id());
1119             ModuleId mid = mi.id();
1120             if (md.exists())
1121                 throw new ConfigurationException(mid + ": Already installed");
1122             if (!md.mkdirs())
1123                 throw new IOException(md + ": Cannot create");
1124 
1125             boolean signed = false;
1126 
1127             // copy the jar file to the module library
1128             File classesDir = new File(md, "classes");
1129             try (FileOutputStream fos = new FileOutputStream(classesDir);
1130                  BufferedOutputStream bos = new BufferedOutputStream(fos);
1131                  JarOutputStream jos = new JarOutputStream(bos)) {
1132                 jos.setLevel(0);
1133 
1134                 Enumeration<JarEntry> entries = jf.entries();
1135                 while (entries.hasMoreElements()) {
1136                     JarEntry je = entries.nextElement();
1137                     try (InputStream is = jf.getInputStream(je)) {
1138                        if (je.getName().equals(JarFile.MODULEINFO_NAME)) {
1139                             java.nio.file.Files.copy(is, md.toPath().resolve("info"));
1140                         } else {
1141                             writeJarEntry(is, je, jos);
1142                         }
1143                     }
1144                     if (!signed) {
1145                         String name = je.getName().toUpperCase(Locale.ENGLISH);
1146                         signed = name.startsWith("META-INF/")
1147                                  && name.endsWith(".SF");
1148                     }
1149                 }
1150             }
1151 
1152             try {
1153                 if (verifySignature && signed) {
1154                     // validate the code signers
1155                     Set<CodeSigner> signers = getSigners(jf);
1156                     SignedModule.validateSigners(signers);
1157                     // store the signers
1158                     new Signers(md, signers).store();
1159                 }
1160             } catch (CertificateException ce) {
1161                 throw new SignatureException(ce);
1162             }
1163 
1164             if (strip)
1165                 strip(md);
1166             reIndex(mid);
1167             return mid;
1168         } catch (IOException | SignatureException x) {
1169             if (md != null && md.exists()) {
1170                 try {
1171                     Files.deleteTree(md);
1172                 } catch (IOException y) {
1173                     y.initCause(x);
1174                     throw y;
1175                 }
1176             }
1177             throw x;
1178         }
1179     }
1180 
1181     /**
1182      * Returns the set of signers of the specified jar file. Each signer
1183      * must have signed all relevant entries.
1184      */
1185     private static Set<CodeSigner> getSigners(JarFile jf)
1186         throws SignatureException
1187     {
1188         Set<CodeSigner> signers = new HashSet<>();
1189         Enumeration<JarEntry> entries = jf.entries();
1190         while (entries.hasMoreElements()) {
1191             JarEntry je = entries.nextElement();
1192             String name = je.getName().toUpperCase(Locale.ENGLISH);
1193             if (name.endsWith("/") || isSigningRelated(name))
1194                 continue;
1195 
1196             // A signed modular jar can be signed by multiple signers.
1197             // However, all entries must be signed by each of these signers.
1198             // Signers that only sign a subset of entries are ignored.
1199             CodeSigner[] jeSigners = je.getCodeSigners();
1200             if (jeSigners == null || jeSigners.length == 0)
1201                 throw new SignatureException("Found unsigned entry in "
1202                                              + "signed modular JAR");
1203 
1204             Set<CodeSigner> jeSignerSet =
1205                 new HashSet<>(Arrays.asList(jeSigners));
1206             if (signers.isEmpty())
1207                 signers.addAll(jeSignerSet);
1208             else {
1209                 if (signers.retainAll(jeSignerSet) && signers.isEmpty())
1210                     throw new SignatureException("No signers in common in "
1211                                                  + "signed modular JAR");
1212             }
1213         }
1214         return signers;
1215     }
1216 
1217     // true if file is part of the signature mechanism itself
1218     private static boolean isSigningRelated(String name) {
1219         if (!name.startsWith("META-INF/")) {
1220             return false;
1221         }
1222         name = name.substring(9);
1223         if (name.indexOf('/') != -1) {
1224             return false;
1225         }
1226         if (name.endsWith(".DSA") ||
1227             name.endsWith(".RSA") ||
1228             name.endsWith(".SF")  ||
1229             name.endsWith(".EC")  ||
1230             name.startsWith("SIG-") ||
1231             name.equals("MANIFEST.MF")) {
1232             return true;
1233         }
1234         return false;
1235     }
1236 
1237     private void writeJarEntry(InputStream is, JarEntry je, JarOutputStream jos)
1238         throws IOException, SignatureException
1239     {
1240         JarEntry entry = new JarEntry(je.getName());
1241         entry.setMethod(isDeflated() ? ZipEntry.DEFLATED : ZipEntry.STORED);
1242         entry.setTime(je.getTime());
1243         try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
1244             int size = 0;
1245             byte[] bs = new byte[1024];
1246             int cc = 0;
1247             // This will throw a SecurityException if a signature is invalid.
1248             while ((cc = is.read(bs)) > 0) {
1249                 baos.write(bs, 0, cc);
1250                 size += cc;
1251             }
1252             if (!isDeflated()) {
1253                 entry.setSize(size);
1254                 entry.setCrc(je.getCrc());
1255                 entry.setCompressedSize(size);
1256             }
1257             jos.putNextEntry(entry);
1258             if (baos.size() > 0)
1259                 baos.writeTo(jos);
1260             jos.closeEntry();
1261         } catch (SecurityException se) {
1262             throw new SignatureException(se);
1263         }
1264     }
1265 
1266     private ModuleId install(File mf, boolean verifySignature, boolean strip)
1267         throws ConfigurationException, IOException, SignatureException
1268     {
1269         ModuleId mid;
1270         if (mf.getName().endsWith(".jar"))
1271             mid = installFromJarFile(mf, verifySignature, strip);
1272         else {
1273             // ## Assume jmod file, should we check extension?
1274             try (FileInputStream in = new FileInputStream(mf)) {
1275                 mid = install(in, verifySignature, strip);
1276             }
1277         }
1278         return mid;
1279     }
1280 
1281     public void install(Collection<File> mfs, boolean verifySignature, boolean strip)
1282         throws ConfigurationException, IOException, SignatureException
1283     {
1284         List<ModuleId> mids = new ArrayList<>();
1285         boolean complete = false;
1286         Throwable ox = null;
1287         try {
1288             for (File mf : mfs)
1289                 mids.add(install(mf, verifySignature, strip));
1290             configure(mids);
1291             complete = true;
1292         } catch (IOException|ConfigurationException x) {
1293             ox = x;
1294             throw x;
1295         } finally {
1296             if (!complete) {
1297                 try {
1298                     for (ModuleId mid : mids)
1299                         deleteModuleDir(mid);
1300                 } catch (IOException x) {
1301                     if (ox != null)
1302                         x.initCause(ox);
1303                     throw x;
1304                 }
1305             }
1306         }
1307     }
1308 
1309     @Override
1310     public void install(Collection<File> mfs, boolean verifySignature)
1311         throws ConfigurationException, IOException, SignatureException
1312     {
1313         install(mfs, verifySignature, false);
1314     }
1315 
1316     // Public entry point, since the Resolver itself is package-private
1317     //
1318     public Resolution resolve(Collection<ModuleIdQuery> midqs)
1319         throws ConfigurationException, IOException
1320     {
1321         return Resolver.run(this, midqs);
1322     }
1323 
1324     public void install(Resolution res, boolean verifySignature, boolean strip)
1325         throws ConfigurationException, IOException, SignatureException
1326     {
1327         // ## Handle case of installing multiple root modules
1328         assert res.rootQueries.size() == 1;
1329         ModuleIdQuery midq = res.rootQueries.iterator().next();
1330         ModuleInfo root = null;
1331         for (ModuleInfo mi : res.modules) {
1332             if (midq.matches(mi.id())) {
1333                 root = mi;
1334                 break;
1335             }
1336         }
1337         assert root != null;
1338 
1339         // Download
1340         //
1341         for (ModuleId mid : res.modulesNeeded()) {
1342             URI u = res.locationForName.get(mid.name());
1343             assert u != null;
1344             RemoteRepository rr = repositoryList().firstRepository();
1345             assert rr != null;
1346             install(rr.fetch(mid), verifySignature, strip);
1347             res.locationForName.put(mid.name(), location());
1348             // ## If something goes wrong, delete all our modules
1349         }
1350 
1351         // Configure
1352         //
1353         Configuration<Context> cf
1354             = Configurator.configure(this, res);
1355         new StoredConfiguration(moduleDir(root.id()), cf).store();
1356     }
1357 
1358     @Override
1359     public void install(Resolution res, boolean verifySignature)
1360         throws ConfigurationException, IOException, SignatureException
1361     {
1362         install(res, verifySignature, false);
1363     }
1364 
1365     /**
1366      * <p> Pre-install one or more modules to an arbitrary destination
1367      * directory. </p>
1368      *
1369      * <p> A pre-installed module has the same format as within the library
1370      * itself, except that there is never a configuration file. </p>
1371      *
1372      * <p> This method is provided for use by the module-packaging tool. </p>
1373      *
1374      * @param   mfs
1375      *          The manifest describing the contents of the modules to be
1376      *          pre-installed
1377      *
1378      * @param   dst
1379      *          The destination directory, with one subdirectory per module
1380      *          name, each of which contains one subdirectory per version
1381      */
1382     public void preInstall(Collection<Manifest> mfs, File dst)
1383         throws IOException
1384     {
1385         Files.mkdirs(dst, "module destination");
1386         install(mfs, dst, false);
1387     }
1388 
1389     public void preInstall(Manifest mf, File dst)
1390         throws IOException
1391     {
1392         preInstall(Collections.singleton(mf), dst);
1393     }
1394 
1395     /**
1396      * <p> Update the configurations of any root modules affected by the
1397      * copying of the named modules, in pre-installed format, into this
1398      * library. </p>
1399      *
1400      * @param   mids
1401      *          The module ids of the new or updated modules, or
1402      *          {@code null} if the configuration of every root module
1403      *          should be (re)computed
1404      */
1405     public void configure(List<ModuleId> mids)
1406         throws ConfigurationException, IOException
1407     {
1408         // ## mids not used yet
1409         for (ModuleInfo mi : listLocalRootModuleInfos()) {
1410             // ## We could be a lot more clever about this!
1411             Configuration<Context> cf
1412                 = Configurator.configure(this, mi.id().toQuery());
1413             new StoredConfiguration(moduleDir(mi.id()), cf).store();
1414         }
1415     }
1416 
1417     public URI findLocalResource(ModuleId mid, String name)
1418         throws IOException
1419     {
1420         return locateContent(mid, name);
1421     }
1422 
1423     public File findLocalNativeLibrary(ModuleId mid, String name)
1424         throws IOException
1425     {
1426         File f = natlibs();
1427         if (f == null) {
1428             f = findModuleDir(mid);
1429             if (f == null)
1430                 return null;
1431             f = new File(f, "lib");
1432         }
1433         f = new File(f, name);
1434         if (!f.exists())
1435             return null;
1436         return f;
1437     }
1438 
1439     public File classPath(ModuleId mid)
1440         throws IOException
1441     {
1442         File md = findModuleDir(mid);
1443         if (md == null) {
1444             if (parent != null)
1445                 return parent.classPath(mid);
1446             return null;
1447         }
1448         // ## Check for other formats here
1449         return new File(md, "classes");
1450     }
1451 
1452     /**
1453      * <p> Re-index the classes of the named previously-installed modules, and
1454      * then update the configurations of any affected root modules. </p>
1455      *
1456      * <p> This method is intended for use during development, when a build
1457      * process may update a previously-installed module in place, adding or
1458      * removing classes. </p>
1459      *
1460      * @param   mids
1461      *          The module ids of the new or updated modules, or
1462      *          {@code null} if the configuration of every root module
1463      *          should be (re)computed
1464      */
1465     public void reIndex(List<ModuleId> mids)
1466         throws ConfigurationException, IOException
1467     {
1468         for (ModuleId mid : mids)
1469             reIndex(mid);
1470         configure(mids);
1471     }
1472 
1473 
1474     // -- Repositories --
1475 
1476     private static class RepoList
1477         implements RemoteRepositoryList
1478     {
1479 
1480         private static final int MINOR_VERSION = 0;
1481         private static final int MAJOR_VERSION = 0;
1482 
1483         private final File root;
1484         private final File listFile;
1485 
1486         private RepoList(File r) {
1487             root = new File(r, FileConstants.META_PREFIX + "repos");
1488             listFile = new File(root, FileConstants.META_PREFIX + "list");
1489         }
1490 
1491         private static FileHeader fileHeader() {
1492             return (new FileHeader()
1493                     .type(FileConstants.Type.REMOTE_REPO_LIST)
1494                     .majorVersion(MAJOR_VERSION)
1495                     .minorVersion(MINOR_VERSION));
1496         }
1497 
1498         private List<RemoteRepository> repos = null;
1499         private long nextRepoId = 0;
1500 
1501         private File repoDir(long id) {
1502             return new File(root, Long.toHexString(id));
1503         }
1504 
1505         private void load() throws IOException {
1506 
1507             repos = new ArrayList<>();
1508             if (!root.exists() || !listFile.exists())
1509                 return;
1510             FileInputStream fin = new FileInputStream(listFile);
1511             DataInputStream in
1512                 = new DataInputStream(new BufferedInputStream(fin));
1513             try {
1514 
1515                 FileHeader fh = fileHeader();
1516                 fh.read(in);
1517                 nextRepoId = in.readLong();
1518                 int n = in.readInt();
1519                 long[] ids = new long[n];
1520                 for (int i = 0; i < n; i++)
1521                     ids[i] = in.readLong();
1522                 RemoteRepository parent = null;
1523 
1524                 // Load in reverse order so that parents are correct
1525                 for (int i = n - 1; i >= 0; i--) {
1526                     long id = ids[i];
1527                     RemoteRepository rr
1528                         = RemoteRepository.open(repoDir(id), id, parent);
1529                     repos.add(rr);
1530                     parent = rr;
1531                 }
1532                 Collections.reverse(repos);
1533 
1534             } finally {
1535                 in.close();
1536             }
1537 
1538         }
1539 
1540         private List<RemoteRepository> roRepos = null;
1541 
1542         // Unmodifiable
1543         public List<RemoteRepository> repositories() throws IOException {
1544             if (repos == null) {
1545                 load();
1546                 roRepos = Collections.unmodifiableList(repos);
1547             }
1548             return roRepos;
1549         }
1550 
1551         public RemoteRepository firstRepository() throws IOException {
1552             repositories();
1553             return repos.isEmpty() ? null : repos.get(0);
1554         }
1555 
1556         private void store() throws IOException {
1557             File newfn = new File(root, "list.new");
1558             FileOutputStream fout = new FileOutputStream(newfn);
1559             DataOutputStream out
1560                 = new DataOutputStream(new BufferedOutputStream(fout));
1561             try {
1562                 try {
1563                     fileHeader().write(out);
1564                     out.writeLong(nextRepoId);
1565                     out.writeInt(repos.size());
1566                     for (RemoteRepository rr : repos)
1567                         out.writeLong(rr.id());
1568                 } finally {
1569                     out.close();
1570                 }
1571             } catch (IOException x) {
1572                 newfn.delete();
1573                 throw x;
1574             }
1575             java.nio.file.Files.move(newfn.toPath(), listFile.toPath(), ATOMIC_MOVE);
1576         }
1577 
1578         public RemoteRepository add(URI u, int position)
1579             throws IOException
1580         {
1581 
1582             if (repos == null)
1583                 load();
1584             for (RemoteRepository rr : repos) {
1585                 if (rr.location().equals(u)) // ## u not canonical
1586                     throw new IllegalStateException(u + ": Already in"
1587                                                     + " repository list");
1588             }
1589             if (!root.exists()) {
1590                 if (!root.mkdir())
1591                     throw new IOException(root + ": Cannot create directory");
1592             }
1593 
1594             if (repos.size() == Integer.MAX_VALUE)
1595                 throw new IllegalStateException("Too many repositories");
1596             if (position < 0)
1597                 throw new IllegalArgumentException("Invalid index");
1598 
1599             long id = nextRepoId++;
1600             RemoteRepository rr = RemoteRepository.create(repoDir(id), u, id);
1601             try {
1602                 rr.updateCatalog(true);
1603             } catch (IOException x) {
1604                 repoDir(id).delete();
1605                 throw x;
1606             }
1607 
1608             if (position >= repos.size()) {
1609                 repos.add(rr);
1610             } else if (position >= 0) {
1611                 List<RemoteRepository> prefix
1612                     = new ArrayList<>(repos.subList(0, position));
1613                 List<RemoteRepository> suffix
1614                     = new ArrayList<>(repos.subList(position, repos.size()));
1615                 repos.clear();
1616                 repos.addAll(prefix);
1617                 repos.add(rr);
1618                 repos.addAll(suffix);
1619             }
1620             store();
1621 
1622             return rr;
1623 
1624         }
1625 
1626         public boolean remove(RemoteRepository rr)
1627             throws IOException
1628         {
1629             if (!repos.remove(rr))
1630                 return false;
1631             store();
1632             File rd = repoDir(rr.id());
1633             for (File f : rd.listFiles()) {
1634                 if (!f.delete())
1635                     throw new IOException(f + ": Cannot delete");
1636             }
1637             if (!rd.delete())
1638                 throw new IOException(rd + ": Cannot delete");
1639             return true;
1640         }
1641 
1642         public boolean areCatalogsStale() throws IOException {
1643             for (RemoteRepository rr : repos) {
1644                 if (rr.isCatalogStale())
1645                     return true;
1646             }
1647             return false;
1648         }
1649 
1650         public boolean updateCatalogs(boolean force) throws IOException {
1651             boolean updated = false;
1652             for (RemoteRepository rr : repos) {
1653                 if (rr.updateCatalog(force))
1654                     updated = true;
1655             }
1656             return updated;
1657         }
1658 
1659     }
1660 
1661     private RemoteRepositoryList repoList = null;
1662 
1663     public RemoteRepositoryList repositoryList()
1664         throws IOException
1665     {
1666         if (repoList == null)
1667             repoList = new RepoList(root);
1668         return repoList;
1669     }
1670 
1671 }