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