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