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






1694     }

1695 
1696     public URI findLocalResource(ModuleId mid, String name)
1697         throws IOException
1698     {
1699         return locateContent(mid, name);
1700     }
1701 
1702     public File findLocalNativeLibrary(ModuleId mid, String name)
1703         throws IOException
1704     {
1705         File f = natlibs();
1706         if (f == null) {
1707             f = moduleDictionary.findDeclaringModuleDir(mid);
1708             if (f == null)
1709                 return null;
1710             f = new File(f, "lib");
1711         }
1712         f = new File(f, name);
1713         if (!f.exists())
1714             return null;
1715         return f;
1716     }
1717 
1718     public File classPath(ModuleId mid)
1719         throws IOException
1720     {
1721         File md = moduleDictionary.findDeclaringModuleDir(mid);
1722         if (md == null) {
1723             if (parent != null)
1724                 return parent.classPath(mid);
1725             return null;
1726         }
1727         // ## Check for other formats here
1728         return new File(md, "classes");
1729     }
1730 
1731     /**
1732      * <p> Re-index the classes of the named previously-installed modules, and
1733      * then update the configurations of any affected root modules. </p>
1734      *
1735      * <p> This method is intended for use during development, when a build
1736      * process may update a previously-installed module in place, adding or
1737      * removing classes. </p>
1738      *
1739      * @param   mids
1740      *          The module ids of the new or updated modules, or
1741      *          {@code null} if the configuration of every root module
1742      *          should be (re)computed
1743      */
1744     public void reIndex(List<ModuleId> mids)
1745         throws ConfigurationException, IOException
1746     {
1747         for (ModuleId mid : mids)
1748             reIndex(mid);
1749         configure(mids);
1750     }
1751 
1752     private static final class ModuleDictionary
1753     {
1754         private static final String FILE
1755             = FileConstants.META_PREFIX + "mids";
1756 
1757         private static final int MAJOR_VERSION = 0;
1758         private static final int MINOR_VERSION = 0;
1759 
1760         private final File root;
1761         private final File file;
1762         private Map<String,Set<ModuleId>> moduleIdsForName;
1763         private Map<ModuleId,ModuleId> providingModuleIds;
1764         private Set<ModuleId> modules;
1765         private long lastUpdated;
1766 
1767         ModuleDictionary(File root) {
1768             this.root = root;
1769             this.file = new File(root, FileConstants.META_PREFIX + "mids");
1770             this.providingModuleIds = new LinkedHashMap<>();
1771             this.moduleIdsForName = new LinkedHashMap<>();
1772             this.modules = new HashSet<>();
1773             this.lastUpdated = -1;
1774         }
1775 
1776         private static FileHeader fileHeader() {
1777             return (new FileHeader()
1778                     .type(FileConstants.Type.LIBRARY_MODULE_IDS)
1779                     .majorVersion(MAJOR_VERSION)
1780                     .minorVersion(MINOR_VERSION));
1781         }
1782 
1783         void load() throws IOException {
1784             if (lastUpdated == file.lastModified())
1785                 return;
1786 
1787             providingModuleIds = new LinkedHashMap<>();
1788             moduleIdsForName = new LinkedHashMap<>();
1789             modules = new HashSet<>();
1790             lastUpdated = file.lastModified();
1791 
1792             try (FileInputStream fin = new FileInputStream(file);
1793                  DataInputStream in = new DataInputStream(new BufferedInputStream(fin)))
1794             {
1795                 FileHeader fh = fileHeader();
1796                 fh.read(in);
1797                 int nMids = in.readInt();
1798                 for (int j = 0; j < nMids; j++) {
1799                     ModuleId mid = jms.parseModuleId(in.readUTF());
1800                     ModuleId pmid = jms.parseModuleId(in.readUTF());
1801                     providingModuleIds.put(mid, pmid);
1802                     addModuleId(mid);
1803                     addModuleId(pmid);
1804                     if (mid.equals(pmid))
1805                         modules.add(mid);
1806                 }
1807             }
1808         }
1809 
1810         void store() throws IOException {
1811             File newfn = new File(root, "mids.new");
1812             FileOutputStream fout = new FileOutputStream(newfn);
1813             DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fout));
1814             try {
1815                 try {
1816                     fileHeader().write(out);
1817                     out.writeInt(providingModuleIds.size());
1818                     for (Map.Entry<ModuleId, ModuleId> e : providingModuleIds.entrySet()) {
1819                         out.writeUTF(e.getKey().toString());
1820                         out.writeUTF(e.getValue().toString());
1821                     }
1822                 } finally {
1823                     out.close();
1824                 }
1825             } catch (IOException x) {
1826                 newfn.delete();
1827                 throw x;
1828             }
1829             java.nio.file.Files.move(newfn.toPath(), file.toPath(), ATOMIC_MOVE);
1830         }
1831 
1832         void gatherLocalModuleIds(String moduleName, Set<ModuleId> mids)
1833                 throws IOException
1834         {
1835             if (lastUpdated != file.lastModified())
1836                 load();
1837 
1838             if (moduleName == null) {
1839                 mids.addAll(providingModuleIds.keySet());
1840             } else {
1841                 Set<ModuleId> res = moduleIdsForName.get(moduleName);
1842                 if (res != null)
1843                     mids.addAll(res);
1844             }
1845         }
1846 
1847         ModuleId getDeclaringModule(ModuleId mid) throws IOException {
1848             if (lastUpdated != file.lastModified())
1849                 load();
1850 
1851             ModuleId pmid = providingModuleIds.get(mid);
1852             if (pmid != null && !pmid.equals(providingModuleIds.get(pmid))) {
1853                 // mid is an alias
1854                 pmid = providingModuleIds.get(pmid);
1855             }
1856             return pmid;
1857         }
1858 
1859         File findDeclaringModuleDir(ModuleId mid)
1860                 throws IOException
1861         {
1862             ModuleId dmid = getDeclaringModule(mid);
1863             if (dmid == null)
1864                 return null;
1865 
1866             File md = moduleDir(root, dmid);
1867             assert md.exists();
1868             checkModuleDir(md);
1869             return md;
1870         }
1871 
1872         Set<ModuleId> modules() throws IOException {
1873             if (lastUpdated != file.lastModified())
1874                 load();
1875             return modules;
1876         }
1877 
1878         void addModuleId(ModuleId mid) {
1879             Set<ModuleId> mids = moduleIdsForName.get(mid.name());
1880             if (mids == null) {
1881                 mids = new HashSet<>();
1882                 moduleIdsForName.put(mid.name(), mids);
1883             }
1884             mids.add(mid);
1885         }
1886 
1887         File add(ModuleInfo mi)
1888                 throws ConfigurationException, IOException
1889         {
1890             File md = ensureNewModule(mi);
1891             addToDirectory(mi);
1892             return md;
1893         }
1894 
1895         private void addToDirectory(ModuleInfo mi) {
1896             modules.add(mi.id());
1897             for (ModuleView view : mi.views()) {
1898                 providingModuleIds.put(view.id(), mi.id());
1899                 addModuleId(view.id());
1900                 for (ModuleId alias : view.aliases()) {
1901                     providingModuleIds.put(alias, view.id());
1902                     addModuleId(alias);
1903                 }
1904             }
1905         }
1906 
1907         void remove(ModuleInfo mi) throws IOException {
1908             modules.remove(mi.id());
1909             for (ModuleView view : mi.views()) {
1910                 providingModuleIds.remove(view.id());
1911                 Set<ModuleId> mids = moduleIdsForName.get(view.id().name());
1912                 if (mids != null)
1913                     mids.remove(view.id());
1914                 for (ModuleId alias : view.aliases()) {
1915                     providingModuleIds.remove(alias);
1916                     mids = moduleIdsForName.get(alias.name());
1917                     if (mids != null)
1918                         mids.remove(view.id());
1919                 }
1920             }
1921             File md = moduleDir(root, mi.id());
1922             delete(md);
1923         }
1924 
1925         private void delete(File md) throws IOException {
1926             if (!md.exists())
1927                 return;
1928 
1929             checkModuleDir(md);
1930             ModuleFile.Reader.remove(md);
1931             File parent = md.getParentFile();
1932             if (parent.list().length == 0)
1933                 parent.delete();
1934         }
1935 
1936         void refresh() throws IOException {
1937             providingModuleIds = new LinkedHashMap<>();
1938             moduleIdsForName = new LinkedHashMap<>();
1939             modules = new HashSet<>();
1940 
1941             try (DirectoryStream<Path> ds = java.nio.file.Files.newDirectoryStream(root.toPath())) {
1942                 for (Path mnp : ds) {
1943                     String mn = mnp.toFile().getName();
1944                     if (mn.startsWith(FileConstants.META_PREFIX) ||
1945                         TRASH.equals(mn)) {
1946                         continue;
1947                     }
1948 
1949                     try (DirectoryStream<Path> mds = java.nio.file.Files.newDirectoryStream(mnp)) {
1950                         for (Path versionp : mds) {
1951                             File v = versionp.toFile();
1952                             if (!v.isDirectory()) {
1953                                 throw new IOException(versionp + ": Not a directory");
1954                             }
1955                             modules.add(jms.parseModuleId(mn, v.getName()));
1956                         }
1957                     }
1958                 }
1959             }
1960             for (ModuleId mid : modules) {
1961                 byte[] bs = Files.load(new File(moduleDir(root, mid), "info"));
1962                 ModuleInfo mi = jms.parseModuleInfo(bs);
1963                 addToDirectory(mi);
1964             }
1965         }
1966 
1967         private File ensureNewModule(ModuleInfo mi)
1968                 throws ConfigurationException, IOException
1969         {
1970             for (ModuleView view : mi.views()) {
1971                 if (providingModuleIds.containsKey(view.id())) {
1972                     throw new ConfigurationException("module view " + view.id()
1973                             + " already installed");
1974                 }
1975                 for (ModuleId alias : view.aliases()) {
1976                     ModuleId mid = alias;
1977                     if (providingModuleIds.containsKey(mid)) {
1978                         throw new ConfigurationException("alias " + alias
1979                                 + " already installed");
1980                     }
1981                 }
1982             }
1983             File md = moduleDir(root, mi.id());
1984             if (md.exists()) {
1985                 throw new ConfigurationException("module " + mi.id()
1986                         + " already installed");
1987             }
1988             if (!md.mkdirs()) {
1989                 throw new IOException(md + ": Cannot create");
1990             }
1991             return md;
1992         }
1993     }
1994 
1995     // -- Repositories --
1996 
1997     private static class RepoList
1998         implements RemoteRepositoryList
1999     {
2000 
2001         private static final int MINOR_VERSION = 0;
2002         private static final int MAJOR_VERSION = 0;
2003 
2004         private final File root;
2005         private final File listFile;
2006 
2007         private RepoList(File r) {
2008             root = new File(r, FileConstants.META_PREFIX + "repos");
2009             listFile = new File(root, FileConstants.META_PREFIX + "list");
2010         }
2011 
2012         private static FileHeader fileHeader() {
2013             return (new FileHeader()
2014                     .type(FileConstants.Type.REMOTE_REPO_LIST)
2015                     .majorVersion(MAJOR_VERSION)
2016                     .minorVersion(MINOR_VERSION));
2017         }
2018 
2019         private List<RemoteRepository> repos = null;
2020         private long nextRepoId = 0;
2021 
2022         private File repoDir(long id) {
2023             return new File(root, Long.toHexString(id));
2024         }
2025 
2026         private void load() throws IOException {
2027 
2028             repos = new ArrayList<>();
2029             if (!root.exists() || !listFile.exists())
2030                 return;
2031             FileInputStream fin = new FileInputStream(listFile);
2032             DataInputStream in
2033                 = new DataInputStream(new BufferedInputStream(fin));
2034             try {
2035 
2036                 FileHeader fh = fileHeader();
2037                 fh.read(in);
2038                 nextRepoId = in.readLong();
2039                 int n = in.readInt();
2040                 long[] ids = new long[n];
2041                 for (int i = 0; i < n; i++)
2042                     ids[i] = in.readLong();
2043                 RemoteRepository parent = null;
2044 
2045                 // Load in reverse order so that parents are correct
2046                 for (int i = n - 1; i >= 0; i--) {
2047                     long id = ids[i];
2048                     RemoteRepository rr
2049                         = RemoteRepository.open(repoDir(id), id, parent);
2050                     repos.add(rr);
2051                     parent = rr;
2052                 }
2053                 Collections.reverse(repos);
2054 
2055             } finally {
2056                 in.close();
2057             }
2058 
2059         }
2060 
2061         private List<RemoteRepository> roRepos = null;
2062 
2063         // Unmodifiable
2064         public List<RemoteRepository> repositories() throws IOException {
2065             if (repos == null) {
2066                 load();
2067                 roRepos = Collections.unmodifiableList(repos);
2068             }
2069             return roRepos;
2070         }
2071 
2072         public RemoteRepository firstRepository() throws IOException {
2073             repositories();
2074             return repos.isEmpty() ? null : repos.get(0);
2075         }
2076 
2077         private void store() throws IOException {
2078             File newfn = new File(root, "list.new");
2079             FileOutputStream fout = new FileOutputStream(newfn);
2080             DataOutputStream out
2081                 = new DataOutputStream(new BufferedOutputStream(fout));
2082             try {
2083                 try {
2084                     fileHeader().write(out);
2085                     out.writeLong(nextRepoId);
2086                     out.writeInt(repos.size());
2087                     for (RemoteRepository rr : repos)
2088                         out.writeLong(rr.id());
2089                 } finally {
2090                     out.close();
2091                 }
2092             } catch (IOException x) {
2093                 newfn.delete();
2094                 throw x;
2095             }
2096             java.nio.file.Files.move(newfn.toPath(), listFile.toPath(), ATOMIC_MOVE);
2097         }
2098 
2099         public RemoteRepository add(URI u, int position)
2100             throws IOException
2101         {
2102 
2103             if (repos == null)
2104                 load();
2105             for (RemoteRepository rr : repos) {
2106                 if (rr.location().equals(u)) // ## u not canonical
2107                     throw new IllegalStateException(u + ": Already in"
2108                                                     + " repository list");
2109             }
2110             if (!root.exists()) {
2111                 if (!root.mkdir())
2112                     throw new IOException(root + ": Cannot create directory");
2113             }
2114 
2115             if (repos.size() == Integer.MAX_VALUE)
2116                 throw new IllegalStateException("Too many repositories");
2117             if (position < 0)
2118                 throw new IllegalArgumentException("Invalid index");
2119 
2120             long id = nextRepoId++;
2121             RemoteRepository rr = RemoteRepository.create(repoDir(id), u, id);
2122             try {
2123                 rr.updateCatalog(true);
2124             } catch (IOException x) {
2125                 rr.delete();
2126                 nextRepoId--;
2127                 throw x;
2128             }
2129 
2130             if (position >= repos.size()) {
2131                 repos.add(rr);
2132             } else if (position >= 0) {
2133                 List<RemoteRepository> prefix
2134                     = new ArrayList<>(repos.subList(0, position));
2135                 List<RemoteRepository> suffix
2136                     = new ArrayList<>(repos.subList(position, repos.size()));
2137                 repos.clear();
2138                 repos.addAll(prefix);
2139                 repos.add(rr);
2140                 repos.addAll(suffix);
2141             }
2142             store();
2143 
2144             return rr;
2145 
2146         }
2147 
2148         public boolean remove(RemoteRepository rr)
2149             throws IOException
2150         {
2151             if (!repos.remove(rr))
2152                 return false;
2153             store();
2154             File rd = repoDir(rr.id());
2155             for (File f : rd.listFiles()) {
2156                 if (!f.delete())
2157                     throw new IOException(f + ": Cannot delete");
2158             }
2159             if (!rd.delete())
2160                 throw new IOException(rd + ": Cannot delete");
2161             return true;
2162         }
2163 
2164         public boolean areCatalogsStale() throws IOException {
2165             for (RemoteRepository rr : repos) {
2166                 if (rr.isCatalogStale())
2167                     return true;
2168             }
2169             return false;
2170         }
2171 
2172         public boolean updateCatalogs(boolean force) throws IOException {
2173             boolean updated = false;
2174             for (RemoteRepository rr : repos) {
2175                 if (rr.updateCatalog(force))
2176                     updated = true;
2177             }
2178             return updated;
2179         }
2180 
2181     }
2182 
2183     private RemoteRepositoryList repoList = null;
2184 
2185     public RemoteRepositoryList repositoryList()
2186         throws IOException
2187     {
2188         if (repoList == null)
2189             repoList = new RepoList(root);
2190         return repoList;
2191     }
2192 
2193 }
--- EOF ---