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