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