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