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