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