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