1 /* 2 * Copyright (c) 2009, 2012, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package org.openjdk.jigsaw; 27 28 import java.lang.module.*; 29 import java.io.*; 30 import java.net.URI; 31 import java.nio.channels.FileChannel; 32 import java.nio.file.*; 33 import java.nio.file.attribute.BasicFileAttributes; 34 import java.security.*; 35 import java.security.cert.*; 36 import java.util.*; 37 import java.util.jar.*; 38 import java.util.zip.*; 39 40 import static java.nio.file.StandardCopyOption.*; 41 import static java.nio.file.StandardOpenOption.*; 42 import org.openjdk.jigsaw.Repository.ModuleType; 43 44 /** 45 * A simple module library which stores data directly in the filesystem 46 * 47 * @see Library 48 */ 49 50 // ## TODO: Move remaining parent-searching logic upward into Library class 51 52 // On-disk library layout 53 // 54 // $LIB/%jigsaw-library 55 // com.foo.bar/1.2.3/info (= module-info.class) 56 // index (list of defined classes) 57 // config (resolved configuration, if a root) 58 // classes/com/foo/bar/... 59 // resources/com/foo/bar/... 60 // lib/libbar.so 61 // bin/bar 62 // signer (signer's certchain & timestamp) 63 // 64 // ## Issue: Concurrent access to the module library 65 // ## e.g. a module is being removed while a running application 66 // ## is depending on it 67 68 public final class SimpleLibrary 69 extends Library 70 { 71 72 private static abstract class MetaData { 73 74 protected final int maxMajorVersion; 75 protected final int maxMinorVersion; 76 protected int majorVersion; 77 protected int minorVersion; 78 private final FileConstants.Type type; 79 private final File file; 80 81 protected MetaData(int maxMajor, int maxMinor, 82 FileConstants.Type t, File f) 83 { 84 maxMajorVersion = majorVersion = maxMajor; 85 maxMinorVersion = minorVersion = maxMinor; 86 type = t; 87 file = f; 88 } 89 90 protected abstract void storeRest(DataOutputStream out) 91 throws IOException; 92 93 void store() throws IOException { 94 try (OutputStream fos = new FileOutputStream(file); 95 BufferedOutputStream bos = new BufferedOutputStream(fos); 96 DataOutputStream out = new DataOutputStream(bos)) { 97 out.writeInt(FileConstants.MAGIC); 98 out.writeShort(type.value()); 99 out.writeShort(majorVersion); 100 out.writeShort(minorVersion); 101 storeRest(out); 102 } 103 } 104 105 protected abstract void loadRest(DataInputStream in) 106 throws IOException; 107 108 protected void load() throws IOException { 109 try (InputStream fis = new FileInputStream(file); 110 BufferedInputStream bis = new BufferedInputStream(fis); 111 DataInputStream in = new DataInputStream(bis)) { 112 if (in.readInt() != FileConstants.MAGIC) 113 throw new IOException(file + ": Invalid magic number"); 114 if (in.readShort() != type.value()) 115 throw new IOException(file + ": Invalid file type"); 116 int maj = in.readShort(); 117 int min = in.readShort(); 118 if ( maj > maxMajorVersion 119 || (maj == maxMajorVersion && min > maxMinorVersion)) { 120 throw new IOException(file 121 + ": Futuristic version number"); 122 } 123 majorVersion = maj; 124 minorVersion = min; 125 loadRest(in); 126 } catch (EOFException x) { 127 throw new IOException(file + ": Invalid library metadata", x); 128 } 129 } 130 } 131 132 /** 133 * <p> Storage options supported by the {@link SimpleLibrary} 134 */ 135 public static enum StorageOption { 136 DEFLATED 137 } 138 139 private static final class Header 140 extends MetaData 141 { 142 private static final String FILE 143 = FileConstants.META_PREFIX + "jigsaw-library"; 144 145 private static final int MAJOR_VERSION = 0; 146 private static final int MINOR_VERSION = 1; 147 148 private static final int DEFLATED = 1 << 0; 149 150 private File parent; 151 // location of native libs for this library (may be outside the library) 152 // null:default, to use a per-module 'lib' directory 153 private File natlibs; 154 // location of native cmds for this library (may be outside the library) 155 // null:default, to use a per-module 'bin' directory 156 private File natcmds; 157 // location of config files for this library (may be outside the library) 158 // null:default, to use a per-module 'etc' directory 159 private File configs; 160 private Set<StorageOption> opts; 161 162 public File parent() { return parent; } 163 public File natlibs() { return natlibs; } 164 public File natcmds() { return natcmds; } 165 public File configs() { return configs; } 166 public boolean isDeflated() { 167 return opts.contains(StorageOption.DEFLATED); 168 } 169 170 private Header(File root) { 171 super(MAJOR_VERSION, MINOR_VERSION, 172 FileConstants.Type.LIBRARY_HEADER, 173 new File(root, FILE)); 174 } 175 176 private Header(File root, File parent, File natlibs, File natcmds, 177 File configs, Set<StorageOption> opts) { 178 this(root); 179 this.parent = parent; 180 this.natlibs = natlibs; 181 this.natcmds = natcmds; 182 this.configs = configs; 183 this.opts = new HashSet<>(opts); 184 } 185 186 private void storePath(File p, DataOutputStream out) throws IOException { 187 if (p != null) { 188 out.writeByte(1); 189 out.writeUTF(Files.convertSeparator(p.toString())); 190 } else { 191 out.write(0); 192 } 193 } 194 195 protected void storeRest(DataOutputStream out) throws IOException { 196 int flags = 0; 197 if (isDeflated()) 198 flags |= DEFLATED; 199 out.writeShort(flags); 200 201 storePath(parent, out); 202 storePath(natlibs, out); 203 storePath(natcmds, out); 204 storePath(configs, out); 205 } 206 207 private File loadPath(DataInputStream in) throws IOException { 208 if (in.readByte() != 0) 209 return new File(Files.platformSeparator(in.readUTF())); 210 return null; 211 } 212 213 protected void loadRest(DataInputStream in) throws IOException { 214 opts = new HashSet<StorageOption>(); 215 int flags = in.readShort(); 216 if ((flags & DEFLATED) == DEFLATED) 217 opts.add(StorageOption.DEFLATED); 218 parent = loadPath(in); 219 natlibs = loadPath(in); 220 natcmds = loadPath(in); 221 configs = loadPath(in); 222 } 223 224 private static Header load(File f) throws IOException { 225 Header h = new Header(f); 226 h.load(); 227 return h; 228 } 229 } 230 231 private final File root; 232 private final File canonicalRoot; 233 private final File parentPath; 234 private final File natlibs; 235 private final File natcmds; 236 private final File configs; 237 private final SimpleLibrary parent; 238 private final Header hd; 239 private final ModuleDictionary moduleDictionary; 240 private final File lockf; 241 private final File trash; 242 243 public String name() { return root.toString(); } 244 public File root() { return canonicalRoot; } 245 public int majorVersion() { return hd.majorVersion; } 246 public int minorVersion() { return hd.minorVersion; } 247 public SimpleLibrary parent() { return parent; } 248 public File natlibs() { return natlibs; } 249 public File natcmds() { return natcmds; } 250 public File configs() { return configs; } 251 public boolean isDeflated() { return hd.isDeflated(); } 252 253 private URI location = null; 254 public URI location() { 255 if (location == null) 256 location = root().toURI(); 257 return location; 258 } 259 260 @Override 261 public String toString() { 262 return (this.getClass().getName() 263 + "[" + canonicalRoot 264 + ", v" + hd.majorVersion + "." + hd.minorVersion + "]"); 265 } 266 267 private static File resolveAndEnsurePath(File path) throws IOException { 268 if (path == null) { return null; } 269 270 File p = path.getCanonicalFile(); 271 if (!p.exists()) { 272 Files.mkdirs(p, p.toString()); 273 } else { 274 Files.ensureIsDirectory(p); 275 Files.ensureWriteable(p); 276 } 277 return p; 278 } 279 280 private File relativize(File path) throws IOException { 281 if (path == null) { return null; } 282 // Return the path relative to the canonical root 283 return (canonicalRoot.toPath().relativize(path.toPath().toRealPath())).toFile(); 284 } 285 286 // Opens an existing library 287 private SimpleLibrary(File path) throws IOException { 288 root = path; 289 canonicalRoot = root.getCanonicalFile(); 290 Files.ensureIsDirectory(root); 291 hd = Header.load(root); 292 293 parentPath = hd.parent(); 294 parent = parentPath != null ? open(parentPath) : null; 295 296 natlibs = hd.natlibs() == null ? null : 297 new File(canonicalRoot, hd.natlibs().toString()).getCanonicalFile(); 298 natcmds = hd.natcmds() == null ? null : 299 new File(canonicalRoot, hd.natcmds().toString()).getCanonicalFile(); 300 configs = hd.configs() == null ? null : 301 new File(canonicalRoot, hd.configs().toString()).getCanonicalFile(); 302 303 lockf = new File(root, FileConstants.META_PREFIX + "lock"); 304 trash = new File(root, TRASH); 305 moduleDictionary = new ModuleDictionary(root); 306 } 307 308 // Creates a new library 309 private SimpleLibrary(File path, File parentPath, File natlibs, File natcmds, 310 File configs, Set<StorageOption> opts) 311 throws IOException 312 { 313 root = path; 314 canonicalRoot = root.getCanonicalFile(); 315 if (root.exists()) { 316 Files.ensureIsDirectory(root); 317 if (root.list().length != 0) 318 throw new IOException(root + ": Already Exists"); 319 Files.ensureWriteable(root); 320 } else 321 Files.mkdirs(root, root.toString()); 322 323 this.parent = parentPath != null ? open(parentPath) : null; 324 this.parentPath = parentPath != null ? this.parent.root() : null; 325 326 this.natlibs = resolveAndEnsurePath(natlibs); 327 this.natcmds = resolveAndEnsurePath(natcmds); 328 this.configs = resolveAndEnsurePath(configs); 329 330 hd = new Header(canonicalRoot, this.parentPath, relativize(this.natlibs), 331 relativize(this.natcmds), relativize(this.configs), opts); 332 hd.store(); 333 334 lockf = new File(root, FileConstants.META_PREFIX + "lock"); 335 lockf.createNewFile(); 336 trash = new File(root, TRASH); 337 Files.mkdirs(trash, "module library trash"); 338 moduleDictionary = new ModuleDictionary(canonicalRoot); 339 moduleDictionary.store(); 340 } 341 342 public static SimpleLibrary create(File path, File parent, File natlibs, 343 File natcmds, File configs, 344 Set<StorageOption> opts) 345 throws IOException 346 { 347 return new SimpleLibrary(path, parent, natlibs, natcmds, configs, opts); 348 } 349 350 public static SimpleLibrary create(File path, File parent, Set<StorageOption> opts) 351 throws IOException 352 { 353 return new SimpleLibrary(path, parent, null, null, null, opts); 354 } 355 356 public static SimpleLibrary create(File path, File parent) 357 throws IOException 358 { 359 return SimpleLibrary.create(path, parent, Collections.<StorageOption>emptySet()); 360 } 361 362 public static SimpleLibrary create(File path, Set<StorageOption> opts) 363 throws IOException 364 { 365 // ## Should default parent to $JAVA_HOME/lib/modules 366 return SimpleLibrary.create(path, null, opts); 367 } 368 369 public static SimpleLibrary open(File path) 370 throws IOException 371 { 372 return new SimpleLibrary(path); 373 } 374 375 private static final JigsawModuleSystem jms 376 = JigsawModuleSystem.instance(); 377 378 private static final class Index 379 extends MetaData 380 { 381 382 private static String FILE = "index"; 383 384 private static int MAJOR_VERSION = 0; 385 private static int MINOR_VERSION = 1; 386 387 private Set<String> publicClasses; 388 public Set<String> publicClasses() { return publicClasses; } 389 390 private Set<String> otherClasses; 391 public Set<String> otherClasses() { return otherClasses; } 392 393 private Index(File root) { 394 super(MAJOR_VERSION, MINOR_VERSION, 395 FileConstants.Type.LIBRARY_MODULE_INDEX, 396 new File(root, FILE)); 397 // Unsorted on input, because we don't need it sorted 398 publicClasses = new HashSet<String>(); 399 otherClasses = new HashSet<String>(); 400 } 401 402 private void storeSet(Set<String> cnset, DataOutputStream out) 403 throws IOException 404 { 405 // Sorted on output, because we can afford it 406 List<String> cns = new ArrayList<String>(cnset); 407 Collections.sort(cns); 408 out.writeInt(cns.size()); 409 for (String cn : cns) 410 out.writeUTF(cn); 411 } 412 413 protected void storeRest(DataOutputStream out) 414 throws IOException 415 { 416 storeSet(publicClasses, out); 417 storeSet(otherClasses, out); 418 } 419 420 private void loadSet(DataInputStream in, Set<String> cnset) 421 throws IOException 422 { 423 int n = in.readInt(); 424 for (int i = 0; i < n; i++) 425 cnset.add(in.readUTF()); 426 } 427 428 protected void loadRest(DataInputStream in) 429 throws IOException 430 { 431 loadSet(in, publicClasses); 432 loadSet(in, otherClasses); 433 } 434 435 private static Index load(File f) 436 throws IOException 437 { 438 Index ix = new Index(f); 439 ix.load(); 440 return ix; 441 } 442 443 } 444 445 private static final class StoredConfiguration 446 extends MetaData 447 { 448 449 private static String FILE = "config"; 450 451 private static int MAJOR_VERSION = 0; 452 private static int MINOR_VERSION = 1; 453 454 private Configuration<Context> cf; 455 456 private static void delete(File root) { 457 new File(root, FILE).delete(); 458 } 459 460 private StoredConfiguration(File root, Configuration<Context> conf) 461 { 462 super(MAJOR_VERSION, MINOR_VERSION, 463 FileConstants.Type.LIBRARY_MODULE_CONFIG, 464 new File(root, FILE)); 465 cf = conf; 466 } 467 468 protected void storeRest(DataOutputStream out) 469 throws IOException 470 { 471 // Roots 472 out.writeInt(cf.roots().size()); 473 for (ModuleId mid : cf.roots()) { 474 out.writeUTF(mid.toString()); 475 } 476 477 // Context names and package names 478 // Store these strings only once and the subsequent sections will 479 // reference these names by its index. 480 List<String> cxns = new ArrayList<>(); 481 Set<String> pkgs = new HashSet<>(); 482 for (Context cx : cf.contexts()) { 483 String cxn = cx.name(); 484 cxns.add(cxn); 485 pkgs.addAll(cx.remotePackages()); 486 for (String cn : cx.localClasses()) { 487 int i = cn.lastIndexOf('.'); 488 if (i >= 0) 489 pkgs.add(cn.substring(0, i)); 490 } 491 } 492 List<String> packages = Arrays.asList(pkgs.toArray(new String[0])); 493 Collections.sort(packages); 494 out.writeInt(cf.contexts().size()); 495 for (String cxn : cxns) { 496 out.writeUTF(cxn); 497 } 498 out.writeInt(packages.size()); 499 for (String pn : packages) { 500 out.writeUTF(pn); 501 } 502 503 // Contexts 504 for (Context cx : cf.contexts()) { 505 // Module ids, and their libraries 506 out.writeInt(cx.modules().size()); 507 List<ModuleId> mids = new ArrayList<>(cx.modules()); 508 for (ModuleId mid : mids) { 509 out.writeUTF(mid.toString()); 510 File lp = cx.findLibraryPathForModule(mid); 511 if (lp == null) 512 out.writeUTF(""); 513 else 514 out.writeUTF(lp.toString()); 515 516 // Module views 517 out.writeInt(cx.views(mid).size()); 518 for (ModuleId id : cx.views(mid)) { 519 out.writeUTF(id.toString()); 520 } 521 } 522 523 // Local class map 524 out.writeInt(cx.localClasses().size()); 525 for (Map.Entry<String,ModuleId> me 526 : cx.moduleForLocalClassMap().entrySet()) { 527 String cn = me.getKey(); 528 int i = cn.lastIndexOf('.'); 529 if (i == -1) { 530 out.writeInt(-1); 531 out.writeUTF(cn); 532 } else { 533 String pn = cn.substring(0, i); 534 assert packages.contains(pn); 535 out.writeInt(packages.indexOf(pn)); 536 out.writeUTF(cn.substring(i+1, cn.length())); 537 } 538 assert mids.contains(me.getValue()); 539 out.writeInt(mids.indexOf(me.getValue())); 540 } 541 542 // Remote package map 543 out.writeInt(cx.contextForRemotePackageMap().size()); 544 for (Map.Entry<String,String> me 545 : cx.contextForRemotePackageMap().entrySet()) { 546 assert packages.contains(me.getKey()) && cxns.contains(me.getValue()); 547 out.writeInt(packages.indexOf(me.getKey())); 548 out.writeInt(cxns.indexOf(me.getValue())); 549 } 550 551 // Suppliers 552 out.writeInt(cx.remoteContexts().size()); 553 for (String cxn : cx.remoteContexts()) { 554 assert cxns.contains(cxn); 555 out.writeInt(cxns.indexOf(cxn)); 556 } 557 558 // Local service implementations 559 Map<String,Set<String>> services = cx.services(); 560 out.writeInt(services.size()); 561 for (Map.Entry<String,Set<String>> me: services.entrySet()) { 562 out.writeUTF(me.getKey()); 563 Set<String> values = me.getValue(); 564 out.writeInt(values.size()); 565 for (String value: values) { 566 out.writeUTF(value); 567 } 568 } 569 } 570 } 571 572 // NOTE: jigsaw.c load_config is the native implementation of this method. 573 // Any change to the format of StoredConfiguration should be reflectd in 574 // both native and Java implementation 575 protected void loadRest(DataInputStream in) 576 throws IOException 577 { 578 // Roots 579 int nRoots = in.readInt(); 580 List<ModuleId> roots = new ArrayList<>(); 581 for (int i = 0; i < nRoots; i++) { 582 String root = in.readUTF(); 583 ModuleId rmid = jms.parseModuleId(root); 584 roots.add(rmid); 585 } 586 cf = new Configuration<>(roots); 587 588 // Context names 589 int nContexts = in.readInt(); 590 List<String> contexts = new ArrayList<>(nContexts); 591 for (int i = 0; i < nContexts; i++) { 592 contexts.add(in.readUTF()); 593 } 594 595 // Package names 596 int nPkgs = in.readInt(); 597 List<String> packages = new ArrayList<>(nPkgs); 598 for (int i = 0; i < nPkgs; i++) { 599 packages.add(in.readUTF()); 600 } 601 602 // Contexts 603 for (String cxn : contexts) { 604 Context cx = new Context(); 605 // Module ids 606 int nModules = in.readInt(); 607 List<ModuleId> mids = new ArrayList<>(nModules); 608 for (int j = 0; j < nModules; j++) { 609 ModuleId mid = jms.parseModuleId(in.readUTF()); 610 mids.add(mid); 611 String lps = in.readUTF(); 612 if (lps.length() > 0) 613 cx.putLibraryPathForModule(mid, new File(lps)); 614 // Module Views 615 int nViews = in.readInt(); 616 Set<ModuleId> views = new HashSet<>(); 617 for (int k = 0; k < nViews; k++) { 618 ModuleId id = jms.parseModuleId(in.readUTF()); 619 views.add(id); 620 cf.put(id.name(), cx); 621 } 622 cx.add(mid, views); 623 } 624 cx.freeze(); 625 assert cx.name().equals(cxn); 626 cf.add(cx); 627 628 // Local class map 629 int nClasses = in.readInt(); 630 for (int j = 0; j < nClasses; j++) { 631 int idx = in.readInt(); 632 String name = in.readUTF(); 633 String cn = (idx == -1) ? name : packages.get(idx) + "." + name; 634 ModuleId mid = mids.get(in.readInt()); 635 cx.putModuleForLocalClass(cn, mid); 636 } 637 // Remote package map 638 int nPackages = in.readInt(); 639 for (int j = 0; j < nPackages; j++) { 640 String pn = packages.get(in.readInt()); 641 String rcxn = contexts.get(in.readInt()); 642 cx.putContextForRemotePackage(pn, rcxn); 643 } 644 // Suppliers 645 int nSuppliers = in.readInt(); 646 for (int j = 0; j < nSuppliers; j++) { 647 String rcxn = contexts.get(in.readInt()); 648 cx.addSupplier(rcxn); 649 } 650 // Local service implementations 651 int nServices = in.readInt(); 652 for (int j = 0; j < nServices; j++) { 653 String sn = in.readUTF(); 654 int nImpl = in.readInt(); 655 for (int k = 0; k < nImpl; k++) { 656 String cn = in.readUTF(); 657 cx.putService(sn, cn); 658 } 659 } 660 } 661 662 } 663 664 private static StoredConfiguration load(File f) 665 throws IOException 666 { 667 StoredConfiguration sp = new StoredConfiguration(f, null); 668 sp.load(); 669 return sp; 670 } 671 672 } 673 674 private static final class Signers 675 extends MetaData { 676 677 private static final String FILE = "signer"; 678 private static final int MAJOR_VERSION = 0; 679 private static final int MINOR_VERSION = 1; 680 private static final String ENCODING = "PkiPath"; 681 682 private CertificateFactory cf; 683 private Set<CodeSigner> signers; 684 private Set<CodeSigner> signers() { return signers; } 685 686 private Signers(File root, Set<CodeSigner> signers) { 687 super(MAJOR_VERSION, MINOR_VERSION, 688 FileConstants.Type.LIBRARY_MODULE_SIGNER, 689 new File(root, FILE)); 690 this.signers = signers; 691 } 692 693 @Override 694 protected void storeRest(DataOutputStream out) 695 throws IOException 696 { 697 out.writeInt(signers.size()); 698 for (CodeSigner signer : signers) { 699 try { 700 CertPath signerCertPath = signer.getSignerCertPath(); 701 out.write(signerCertPath.getEncoded(ENCODING)); 702 Timestamp ts = signer.getTimestamp(); 703 if (ts != null) { 704 out.writeByte(1); 705 out.writeLong(ts.getTimestamp().getTime()); 706 out.write(ts.getSignerCertPath().getEncoded(ENCODING)); 707 } else { 708 out.writeByte(0); 709 } 710 } catch (CertificateEncodingException cee) { 711 throw new IOException(cee); 712 } 713 } 714 } 715 716 @Override 717 protected void loadRest(DataInputStream in) 718 throws IOException 719 { 720 int size = in.readInt(); 721 for (int i = 0; i < size; i++) { 722 try { 723 if (cf == null) 724 cf = CertificateFactory.getInstance("X.509"); 725 CertPath signerCertPath = cf.generateCertPath(in, ENCODING); 726 int b = in.readByte(); 727 if (b != 0) { 728 Date timestamp = new Date(in.readLong()); 729 CertPath tsaCertPath = cf.generateCertPath(in, ENCODING); 730 Timestamp ts = new Timestamp(timestamp, tsaCertPath); 731 signers.add(new CodeSigner(signerCertPath, ts)); 732 } else { 733 signers.add(new CodeSigner(signerCertPath, null)); 734 } 735 } catch (CertificateException ce) { 736 throw new IOException(ce); 737 } 738 } 739 } 740 741 private static Signers load(File f) 742 throws IOException 743 { 744 Signers signers = new Signers(f, new HashSet<CodeSigner>()); 745 signers.load(); 746 return signers; 747 } 748 } 749 750 protected void gatherLocalModuleIds(String moduleName, 751 Set<ModuleId> mids) 752 throws IOException 753 { 754 moduleDictionary.gatherLocalModuleIds(moduleName, mids); 755 } 756 757 protected void gatherLocalDeclaringModuleIds(Set<ModuleId> mids) 758 throws IOException 759 { 760 mids.addAll(moduleDictionary.modules()); 761 } 762 763 private void checkModuleId(ModuleId mid) { 764 Version v = mid.version(); 765 if (v == null) 766 return; 767 if (!(v instanceof JigsawVersion)) 768 throw new IllegalArgumentException(mid + ": Not a Jigsaw module id"); 769 } 770 771 private static File moduleDir(File root, ModuleId mid) { 772 Version v = mid.version(); 773 String vs = (v != null) ? v.toString() : "default"; 774 return new File(new File(root, mid.name()), vs); 775 } 776 777 private static void checkModuleDir(File md) 778 throws IOException 779 { 780 if (!md.isDirectory()) 781 throw new IOException(md + ": Not a directory"); 782 if (!md.canRead()) 783 throw new IOException(md + ": Not readable"); 784 } 785 786 private File preinstallModuleDir(File dst, ModuleInfo mi) throws IOException { 787 File md = moduleDir(dst, mi.id()); 788 if (md.exists()) { 789 Files.deleteTree(md); 790 } 791 if (!md.mkdirs()) { 792 throw new IOException(md + ": Cannot create"); 793 } 794 return md; 795 } 796 797 public byte[] readLocalModuleInfoBytes(ModuleId mid) 798 throws IOException 799 { 800 File md = moduleDictionary.findDeclaringModuleDir(mid); 801 if (md == null) 802 return null; 803 return Files.load(new File(md, "info")); 804 } 805 806 @Override 807 public CodeSigner[] readLocalCodeSigners(ModuleId mid) 808 throws IOException 809 { 810 File md = moduleDictionary.findDeclaringModuleDir(mid); 811 if (md == null) 812 return null; 813 814 File f = new File(md, "signer"); 815 // ## concurrency issues : what is the expected behavior if file is 816 // ## removed by another thread/process here? 817 if (!f.exists()) 818 return null; 819 return Signers.load(md).signers().toArray(new CodeSigner[0]); 820 } 821 822 // ## Close all zip files when we close this library 823 private Map<ModuleId, Object> contentForModule = new HashMap<>(); 824 private Object NONE = new Object(); 825 826 private Object findContent(ModuleId mid) 827 throws IOException 828 { 829 ModuleId dmid = moduleDictionary.getDeclaringModule(mid); 830 Object o = contentForModule.get(dmid); 831 if (o == NONE) 832 return null; 833 if (o != null) 834 return o; 835 File md = moduleDictionary.findDeclaringModuleDir(dmid); 836 if (md == null) { 837 contentForModule.put(mid, NONE); 838 return null; 839 } 840 File cf = new File(md, "classes"); 841 if (cf.isFile()) { 842 ZipFile zf = new ZipFile(cf); 843 contentForModule.put(mid, zf); 844 return zf; 845 } 846 if (cf.isDirectory()) { 847 contentForModule.put(mid, cf); 848 return cf; 849 } 850 contentForModule.put(mid, NONE); 851 return null; 852 } 853 854 private byte[] loadContent(ZipFile zf, String path) 855 throws IOException 856 { 857 ZipEntry ze = zf.getEntry(path); 858 if (ze == null) 859 return null; 860 return Files.load(zf.getInputStream(ze), (int)ze.getSize()); 861 } 862 863 private byte[] loadContent(ModuleId mid, String path) 864 throws IOException 865 { 866 Object o = findContent(mid); 867 if (o == null) 868 return null; 869 if (o instanceof ZipFile) { 870 ZipFile zf = (ZipFile)o; 871 ZipEntry ze = zf.getEntry(path); 872 if (ze == null) 873 return null; 874 return Files.load(zf.getInputStream(ze), (int)ze.getSize()); 875 } 876 if (o instanceof File) { 877 File f = new File((File)o, path); 878 if (!f.exists()) 879 return null; 880 return Files.load(f); 881 } 882 assert false; 883 return null; 884 } 885 886 private URI locateContent(ModuleId mid, String path) 887 throws IOException 888 { 889 Object o = findContent(mid); 890 if (o == null) 891 return null; 892 if (o instanceof ZipFile) { 893 ZipFile zf = (ZipFile)o; 894 ZipEntry ze = zf.getEntry(path); 895 if (ze == null) 896 return null; 897 return URI.create("jar:" 898 + new File(zf.getName()).toURI().toString() 899 + "!/" + path); 900 } 901 if (o instanceof File) { 902 File f = new File((File)o, path); 903 if (!f.exists()) 904 return null; 905 return f.toURI(); 906 } 907 return null; 908 } 909 910 public byte[] readLocalClass(ModuleId mid, String className) 911 throws IOException 912 { 913 return loadContent(mid, className.replace('.', '/') + ".class"); 914 } 915 916 public List<String> listLocalClasses(ModuleId mid, boolean all) 917 throws IOException 918 { 919 File md = moduleDictionary.findDeclaringModuleDir(mid); 920 if (md == null) 921 return null; 922 Index ix = Index.load(md); 923 int os = all ? ix.otherClasses().size() : 0; 924 ArrayList<String> cns 925 = new ArrayList<String>(ix.publicClasses().size() + os); 926 cns.addAll(ix.publicClasses()); 927 if (all) 928 cns.addAll(ix.otherClasses()); 929 return cns; 930 } 931 932 public Configuration<Context> readConfiguration(ModuleId mid) 933 throws IOException 934 { 935 File md = moduleDictionary.findDeclaringModuleDir(mid); 936 if (md == null) { 937 if (parent != null) { 938 return parent.readConfiguration(mid); 939 } 940 return null; 941 } 942 StoredConfiguration scf = StoredConfiguration.load(md); 943 return scf.cf; 944 } 945 946 private boolean addToIndex(ClassInfo ci, Index ix) 947 throws IOException 948 { 949 if (ci.isModuleInfo()) 950 return false; 951 if (ci.moduleName() != null) { 952 // ## From early Jigsaw development; can probably delete now 953 throw new IOException("Old-style class file with" 954 + " module attribute"); 955 } 956 if (ci.isPublic()) 957 ix.publicClasses().add(ci.name()); 958 else 959 ix.otherClasses().add(ci.name()); 960 return true; 961 } 962 963 private void reIndex(ModuleId mid) 964 throws IOException 965 { 966 967 File md = moduleDictionary.findDeclaringModuleDir(mid); 968 if (md == null) 969 throw new IllegalArgumentException(mid + ": No such module"); 970 File cd = new File(md, "classes"); 971 final Index ix = new Index(md); 972 973 if (cd.isDirectory()) { 974 Files.walkTree(cd, new Files.Visitor<File>() { 975 public void accept(File f) throws IOException { 976 if (f.getPath().endsWith(".class")) 977 addToIndex(ClassInfo.read(f), ix); 978 } 979 }); 980 } else if (cd.isFile()) { 981 try (FileInputStream fis = new FileInputStream(cd); 982 ZipInputStream zis = new ZipInputStream(fis)) 983 { 984 ZipEntry ze; 985 while ((ze = zis.getNextEntry()) != null) { 986 if (!ze.getName().endsWith(".class")) 987 continue; 988 addToIndex(ClassInfo.read(Files.nonClosingStream(zis), 989 ze.getSize(), 990 mid + ":" + ze.getName()), 991 ix); 992 } 993 } 994 } 995 996 ix.store(); 997 } 998 999 /** 1000 * Strip the debug attributes from the classes in a given module 1001 * directory. 1002 */ 1003 private void strip(File md) throws IOException { 1004 File classes = new File(md, "classes"); 1005 if (classes.isFile()) { 1006 File pf = new File(md, "classes.pack"); 1007 try (JarFile jf = new JarFile(classes); 1008 FileOutputStream out = new FileOutputStream(pf)) 1009 { 1010 Pack200.Packer packer = Pack200.newPacker(); 1011 Map<String,String> p = packer.properties(); 1012 p.put("com.sun.java.util.jar.pack.strip.debug", Pack200.Packer.TRUE); 1013 packer.pack(jf, out); 1014 } 1015 1016 try (OutputStream out = new FileOutputStream(classes); 1017 JarOutputStream jos = new JarOutputStream(out)) 1018 { 1019 Pack200.Unpacker unpacker = Pack200.newUnpacker(); 1020 unpacker.unpack(pf, jos); 1021 } finally { 1022 pf.delete(); 1023 } 1024 } 1025 } 1026 1027 private List<Path> listFiles(Path dir) throws IOException { 1028 final List<Path> files = new ArrayList<>(); 1029 java.nio.file.Files.walkFileTree(dir, new SimpleFileVisitor<Path>() { 1030 @Override 1031 public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) 1032 throws IOException 1033 { 1034 if (!file.endsWith("module-info.class")) 1035 files.add(file); 1036 1037 return FileVisitResult.CONTINUE; 1038 } 1039 }); 1040 return files; 1041 } 1042 1043 private ModuleId installWhileLocked(Manifest mf, File dst, boolean strip) 1044 throws IOException 1045 { 1046 if (mf.classes().size() > 1) 1047 throw new IllegalArgumentException("Multiple module-class" 1048 + " directories" 1049 + " not yet supported"); 1050 if (mf.classes().size() < 1) 1051 throw new IllegalArgumentException("At least one module-class" 1052 + " directory required"); 1053 File classes = mf.classes().get(0); 1054 final String mn = mf.module(); 1055 1056 File mif = new File(classes, "module-info.class"); 1057 File src = null; 1058 if (mif.exists()) { 1059 src = classes; 1060 } else { 1061 src = new File(classes, mn); 1062 mif = new File(src, "module-info.class"); 1063 } 1064 byte[] bs = Files.load(mif); 1065 ModuleInfo mi = jms.parseModuleInfo(bs); 1066 if (!mi.id().name().equals(mn)) { 1067 // ## Need a more appropriate throwable here 1068 throw new Error(mif + " is for module " + mi.id().name() 1069 + ", not " + mn); 1070 } 1071 String m = mi.id().name(); 1072 JigsawVersion v = (JigsawVersion)mi.id().version(); 1073 String vs = (v == null) ? "default" : v.toString(); 1074 1075 try { 1076 File mdst; 1077 if (dst.equals(root)) { 1078 mdst = moduleDictionary.add(mi); 1079 } else { 1080 mdst = preinstallModuleDir(dst, mi); 1081 } 1082 Files.store(bs, new File(mdst, "info")); 1083 File cldst = new File(mdst, "classes"); 1084 1085 // Delete the config file, if one exists 1086 StoredConfiguration.delete(mdst); 1087 1088 if (false) { 1089 1090 // ## Retained for now in case we later want to add an option 1091 // ## to install into a tree rather than a zip file 1092 1093 // Copy class files and build index 1094 final Index ix = new Index(mdst); 1095 Files.copyTree(src, cldst, new Files.Filter<File>() { 1096 1097 public boolean accept(File f) throws IOException { 1098 if (f.isDirectory()) 1099 return true; 1100 if (f.getName().endsWith(".class")) { 1101 return addToIndex(ClassInfo.read(f), ix); 1102 } else { 1103 return true; 1104 } 1105 } 1106 }); 1107 ix.store(); 1108 } else { 1109 // Copy class/resource files and build index 1110 Index ix = new Index(mdst); 1111 Path srcPath = src.toPath(); 1112 List<Path> files = listFiles(srcPath); 1113 1114 if (!files.isEmpty()) { 1115 try (FileOutputStream fos = new FileOutputStream(new File(mdst, "classes")); 1116 JarOutputStream jos = new JarOutputStream(new BufferedOutputStream(fos))) 1117 { 1118 boolean deflate = isDeflated(); 1119 for (Path path : files) { 1120 File file = path.toFile(); 1121 String jp = Files.convertSeparator(srcPath.relativize(path).toString()); 1122 try (OutputStream out = Files.newOutputStream(jos, deflate, jp)) { 1123 java.nio.file.Files.copy(path, out); 1124 } 1125 if (file.getName().endsWith(".class")) 1126 addToIndex(ClassInfo.read(file), ix); 1127 } 1128 } 1129 } 1130 ix.store(); 1131 if (strip) { 1132 strip(mdst); 1133 } 1134 } 1135 } catch (ConfigurationException x) { 1136 // module already exists 1137 throw new IOException(x); 1138 } catch (IOException x) { 1139 try { 1140 moduleDictionary.remove(mi); 1141 } catch (IOException y) { 1142 x.addSuppressed(y); 1143 } 1144 throw x; 1145 } 1146 return mi.id(); 1147 } 1148 1149 public void installFromManifests(Collection<Manifest> mfs, boolean strip) 1150 throws ConfigurationException, IOException 1151 { 1152 boolean complete = false; 1153 List<ModuleId> mids = new ArrayList<>(); 1154 FileChannel fc = FileChannel.open(lockf.toPath(), WRITE); 1155 try { 1156 fc.lock(); 1157 moduleDictionary.load(); 1158 for (Manifest mf : mfs) { 1159 mids.add(installWhileLocked(mf, root, strip)); 1160 } 1161 configureWhileModuleDirectoryLocked(null); 1162 complete = true; 1163 } catch (ConfigurationException | IOException x) { 1164 try { 1165 for (ModuleId mid : mids) { 1166 ModuleInfo mi = readLocalModuleInfo(mid); 1167 if (mi != null) { 1168 moduleDictionary.remove(mi); 1169 } 1170 } 1171 } catch (IOException y) { 1172 x.addSuppressed(y); 1173 } 1174 throw x; 1175 } finally { 1176 if (complete) { 1177 moduleDictionary.store(); 1178 } 1179 fc.close(); 1180 } 1181 } 1182 1183 @Override 1184 public void installFromManifests(Collection<Manifest> mfs) 1185 throws ConfigurationException, IOException 1186 { 1187 installFromManifests(mfs, false); 1188 } 1189 1190 private ModuleId installWhileLocked(ModuleType type, InputStream is, boolean verifySignature, 1191 boolean strip) 1192 throws ConfigurationException, IOException, SignatureException 1193 { 1194 switch (type) { 1195 case JAR: 1196 Path jf = java.nio.file.Files.createTempFile(null, null); 1197 try { 1198 java.nio.file.Files.copy(is, jf, StandardCopyOption.REPLACE_EXISTING); 1199 return installFromJarFile(jf.toFile(), verifySignature, strip); 1200 } finally { 1201 java.nio.file.Files.delete(jf); 1202 } 1203 case JMOD: 1204 default: 1205 return installWhileLocked(is, verifySignature, strip); 1206 } 1207 } 1208 1209 private ModuleId installWhileLocked(InputStream is, boolean verifySignature, 1210 boolean strip) 1211 throws ConfigurationException, IOException, SignatureException 1212 { 1213 BufferedInputStream bin = new BufferedInputStream(is); 1214 DataInputStream in = new DataInputStream(bin); 1215 ModuleInfo mi = null; 1216 try (ModuleFile.Reader mr = new ModuleFile.Reader(in)) { 1217 ModuleInfo moduleInfo = jms.parseModuleInfo(mr.getModuleInfoBytes()); 1218 File md = moduleDictionary.add(moduleInfo); 1219 mi = moduleInfo; 1220 if (verifySignature && mr.hasSignature()) { 1221 // Verify the module signature 1222 SignedModule sm = new SignedModule(mr); 1223 Set<CodeSigner> signers = sm.verifySignature(); 1224 1225 // Validate the signers 1226 try { 1227 SignedModule.validateSigners(signers); 1228 } catch (CertificateException x) { 1229 throw new SignatureException(x); 1230 } 1231 1232 // ## TODO: Check policy and determine if signer is trusted 1233 // ## and what permissions should be granted. 1234 // ## If there is no policy entry, show signers and prompt 1235 // ## user to accept before proceeding. 1236 1237 // Verify the module header hash and the module info hash 1238 sm.verifyHashesStart(); 1239 1240 // Extract remainder of the module file, and calculate hashes 1241 mr.extractTo(md, isDeflated(), natlibs(), natcmds(), configs()); 1242 1243 // Verify the rest of the hashes 1244 sm.verifyHashesRest(); 1245 1246 // Store signer info 1247 new Signers(md, signers).store(); 1248 } else { 1249 mr.extractTo(md, isDeflated(), natlibs(), natcmds(), configs()); 1250 } 1251 1252 if (strip) 1253 strip(md); 1254 reIndex(mi.id()); // ## Could do this while reading module file 1255 1256 return mi.id(); 1257 1258 } catch (ConfigurationException | IOException | SignatureException | 1259 ModuleFileParserException x) { // ## should we catch Throwable 1260 if (mi != null) { 1261 try { 1262 moduleDictionary.remove(mi); 1263 } catch (IOException y) { 1264 x.addSuppressed(y); 1265 } 1266 } 1267 throw x; 1268 } 1269 } 1270 1271 private ModuleId installFromJarFile(File mf, boolean verifySignature, boolean strip) 1272 throws ConfigurationException, IOException, SignatureException 1273 { 1274 ModuleInfo mi = null; 1275 try (JarFile jf = new JarFile(mf, verifySignature)) { 1276 ModuleInfo moduleInfo = jf.getModuleInfo(); 1277 if (moduleInfo == null) 1278 throw new ConfigurationException(mf + ": not a modular JAR file"); 1279 1280 File md = moduleDictionary.add(moduleInfo); 1281 mi = moduleInfo; 1282 ModuleId mid = mi.id(); 1283 1284 boolean signed = false; 1285 1286 // copy the jar file to the module library 1287 File classesDir = new File(md, "classes"); 1288 try (FileOutputStream fos = new FileOutputStream(classesDir); 1289 BufferedOutputStream bos = new BufferedOutputStream(fos); 1290 JarOutputStream jos = new JarOutputStream(bos)) { 1291 jos.setLevel(0); 1292 1293 Enumeration<JarEntry> entries = jf.entries(); 1294 while (entries.hasMoreElements()) { 1295 JarEntry je = entries.nextElement(); 1296 try (InputStream is = jf.getInputStream(je)) { 1297 if (je.getName().equals(JarFile.MODULEINFO_NAME)) { 1298 java.nio.file.Files.copy(is, md.toPath().resolve("info")); 1299 } else { 1300 writeJarEntry(is, je, jos); 1301 } 1302 } 1303 if (!signed) { 1304 String name = je.getName().toUpperCase(Locale.ENGLISH); 1305 signed = name.startsWith("META-INF/") 1306 && name.endsWith(".SF"); 1307 } 1308 } 1309 } 1310 1311 try { 1312 if (verifySignature && signed) { 1313 // validate the code signers 1314 Set<CodeSigner> signers = getSigners(jf); 1315 SignedModule.validateSigners(signers); 1316 // store the signers 1317 new Signers(md, signers).store(); 1318 } 1319 } catch (CertificateException ce) { 1320 throw new SignatureException(ce); 1321 } 1322 1323 if (strip) 1324 strip(md); 1325 reIndex(mid); 1326 1327 return mid; 1328 } catch (ConfigurationException | IOException | SignatureException x) { 1329 if (mi != null) { 1330 try { 1331 moduleDictionary.remove(mi); 1332 } catch (IOException y) { 1333 x.addSuppressed(y); 1334 } 1335 } 1336 throw x; 1337 } 1338 } 1339 1340 /** 1341 * Returns the set of signers of the specified jar file. Each signer 1342 * must have signed all relevant entries. 1343 */ 1344 private static Set<CodeSigner> getSigners(JarFile jf) 1345 throws SignatureException 1346 { 1347 Set<CodeSigner> signers = new HashSet<>(); 1348 Enumeration<JarEntry> entries = jf.entries(); 1349 while (entries.hasMoreElements()) { 1350 JarEntry je = entries.nextElement(); 1351 String name = je.getName().toUpperCase(Locale.ENGLISH); 1352 if (name.endsWith("/") || isSigningRelated(name)) 1353 continue; 1354 1355 // A signed modular jar can be signed by multiple signers. 1356 // However, all entries must be signed by each of these signers. 1357 // Signers that only sign a subset of entries are ignored. 1358 CodeSigner[] jeSigners = je.getCodeSigners(); 1359 if (jeSigners == null || jeSigners.length == 0) 1360 throw new SignatureException("Found unsigned entry in " 1361 + "signed modular JAR"); 1362 1363 Set<CodeSigner> jeSignerSet = 1364 new HashSet<>(Arrays.asList(jeSigners)); 1365 if (signers.isEmpty()) 1366 signers.addAll(jeSignerSet); 1367 else if (signers.retainAll(jeSignerSet) && signers.isEmpty()) 1368 throw new SignatureException("No signers in common in " 1369 + "signed modular JAR"); 1370 } 1371 return signers; 1372 } 1373 1374 // true if file is part of the signature mechanism itself 1375 private static boolean isSigningRelated(String name) { 1376 if (!name.startsWith("META-INF/")) { 1377 return false; 1378 } 1379 name = name.substring(9); 1380 if (name.indexOf('/') != -1) { 1381 return false; 1382 } 1383 if (name.endsWith(".DSA") || 1384 name.endsWith(".RSA") || 1385 name.endsWith(".SF") || 1386 name.endsWith(".EC") || 1387 name.startsWith("SIG-") || 1388 name.equals("MANIFEST.MF")) { 1389 return true; 1390 } 1391 return false; 1392 } 1393 1394 private void writeJarEntry(InputStream is, JarEntry je, JarOutputStream jos) 1395 throws IOException, SignatureException 1396 { 1397 JarEntry entry = new JarEntry(je.getName()); 1398 entry.setMethod(isDeflated() ? ZipEntry.DEFLATED : ZipEntry.STORED); 1399 entry.setTime(je.getTime()); 1400 try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { 1401 int size = 0; 1402 byte[] bs = new byte[1024]; 1403 int cc = 0; 1404 // This will throw a SecurityException if a signature is invalid. 1405 while ((cc = is.read(bs)) > 0) { 1406 baos.write(bs, 0, cc); 1407 size += cc; 1408 } 1409 if (!isDeflated()) { 1410 entry.setSize(size); 1411 entry.setCrc(je.getCrc()); 1412 entry.setCompressedSize(size); 1413 } 1414 jos.putNextEntry(entry); 1415 if (baos.size() > 0) 1416 baos.writeTo(jos); 1417 jos.closeEntry(); 1418 } catch (SecurityException se) { 1419 throw new SignatureException(se); 1420 } 1421 } 1422 1423 private ModuleId installWhileLocked(File mf, boolean verifySignature, boolean strip) 1424 throws ConfigurationException, IOException, SignatureException 1425 { 1426 if (mf.getName().endsWith(".jar")) 1427 return installFromJarFile(mf, verifySignature, strip); 1428 else { 1429 // Assume jmod file 1430 try (FileInputStream in = new FileInputStream(mf)) { 1431 return installWhileLocked(in, verifySignature, strip); 1432 } 1433 } 1434 } 1435 1436 public void install(Collection<File> mfs, boolean verifySignature, boolean strip) 1437 throws ConfigurationException, IOException, SignatureException 1438 { 1439 List<ModuleId> mids = new ArrayList<>(); 1440 boolean complete = false; 1441 FileChannel fc = FileChannel.open(lockf.toPath(), WRITE); 1442 try { 1443 fc.lock(); 1444 moduleDictionary.load(); 1445 for (File mf : mfs) 1446 mids.add(installWhileLocked(mf, verifySignature, strip)); 1447 configureWhileModuleDirectoryLocked(mids); 1448 complete = true; 1449 } catch (ConfigurationException | IOException | SignatureException | 1450 ModuleFileParserException x) { // ## catch throwable?? 1451 try { 1452 for (ModuleId mid : mids) { 1453 ModuleInfo mi = readLocalModuleInfo(mid); 1454 if (mi != null) { 1455 moduleDictionary.remove(mi); 1456 } 1457 } 1458 } catch (IOException y) { 1459 x.addSuppressed(y); 1460 } 1461 throw x; 1462 } finally { 1463 if (complete) { 1464 moduleDictionary.store(); 1465 } 1466 fc.close(); 1467 } 1468 } 1469 1470 @Override 1471 public void install(Collection<File> mfs, boolean verifySignature) 1472 throws ConfigurationException, IOException, SignatureException 1473 { 1474 install(mfs, verifySignature, false); 1475 } 1476 1477 // Public entry point, since the Resolver itself is package-private 1478 // 1479 @Override 1480 public Resolution resolve(Collection<ModuleIdQuery> midqs) 1481 throws ConfigurationException, IOException 1482 { 1483 try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) { 1484 fc.lock(); 1485 return Resolver.run(this, midqs); 1486 } 1487 } 1488 1489 public void install(Resolution res, boolean verifySignature, boolean strip) 1490 throws ConfigurationException, IOException, SignatureException 1491 { 1492 boolean complete = false; 1493 FileChannel fc = FileChannel.open(lockf.toPath(), WRITE); 1494 try { 1495 fc.lock(); 1496 moduleDictionary.load(); 1497 1498 // ## Handle case of installing multiple root modules 1499 assert res.rootQueries.size() == 1; 1500 ModuleIdQuery midq = res.rootQueries.iterator().next(); 1501 ModuleInfo root = null; 1502 for (String mn : res.moduleViewForName.keySet()) { 1503 ModuleView mv = res.moduleViewForName.get(mn); 1504 if (midq.matches(mv.id())) { 1505 root = mv.moduleInfo(); 1506 break; 1507 } 1508 } 1509 assert root != null; 1510 1511 // Download 1512 // 1513 for (ModuleId mid : res.modulesNeeded()) { 1514 URI u = res.locationForName.get(mid.name()); 1515 assert u != null; 1516 RemoteRepository rr = repositoryList().firstRepository(); 1517 assert rr != null; 1518 installWhileLocked(rr.fetchMetaData(mid).getType(), 1519 rr.fetch(mid), 1520 verifySignature, 1521 strip); 1522 res.locationForName.put(mid.name(), location()); 1523 // ## If something goes wrong, delete all our modules 1524 } 1525 1526 // Configure 1527 // 1528 configureWhileModuleDirectoryLocked(res.modulesNeeded()); 1529 complete = true; 1530 } catch (ConfigurationException | IOException | SignatureException | 1531 ModuleFileParserException x) { // ## catch throwable?? 1532 try { 1533 for (ModuleId mid : res.modulesNeeded()) { 1534 ModuleInfo mi = readLocalModuleInfo(mid); 1535 if (mi != null) { 1536 moduleDictionary.remove(mi); 1537 } 1538 } 1539 } catch (IOException y) { 1540 x.addSuppressed(y); 1541 } 1542 throw x; 1543 } finally { 1544 if (complete) { 1545 moduleDictionary.store(); 1546 } 1547 fc.close(); 1548 } 1549 } 1550 1551 @Override 1552 public void install(Resolution res, boolean verifySignature) 1553 throws ConfigurationException, IOException, SignatureException 1554 { 1555 install(res, verifySignature, false); 1556 } 1557 1558 @Override 1559 public void removeForcibly(List<ModuleId> mids) 1560 throws IOException 1561 { 1562 try { 1563 remove(mids, true, false); 1564 } catch (ConfigurationException x) { 1565 throw new Error("should not be thrown when forcibly removing", x); 1566 } 1567 } 1568 1569 @Override 1570 public void remove(List<ModuleId> mids, boolean dry) 1571 throws ConfigurationException, IOException 1572 { 1573 remove(mids, false, dry); 1574 } 1575 1576 private void remove(List<ModuleId> mids, boolean force, boolean dry) 1577 throws ConfigurationException, IOException 1578 { 1579 IOException ioe = null; 1580 1581 try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) { 1582 fc.lock(); 1583 for (ModuleId mid : mids) { 1584 // ## Should we support alias and/or non-default view names?? 1585 if (moduleDictionary.findDeclaringModuleDir(mid) == null) 1586 throw new IllegalArgumentException(mid + ": No such module"); 1587 } 1588 if (!force) 1589 ensureNotInConfiguration(mids); 1590 if (dry) 1591 return; 1592 1593 // The library may be altered after this point, so the modules 1594 // dictionary needs to be refreshed 1595 List<IOException> excs = removeWhileLocked(mids); 1596 try { 1597 moduleDictionary.refresh(); 1598 moduleDictionary.store(); 1599 } catch (IOException x) { 1600 excs.add(x); 1601 } 1602 if (!excs.isEmpty()) { 1603 ioe = excs.remove(0); 1604 for (IOException x : excs) 1605 ioe.addSuppressed(x); 1606 } 1607 } finally { 1608 if (ioe != null) 1609 throw ioe; 1610 } 1611 } 1612 1613 private void ensureNotInConfiguration(List<ModuleId> mids) 1614 throws ConfigurationException, IOException 1615 { 1616 // ## We do not know if a root module in a child library depends on one 1617 // ## of the 'to be removed' modules. We would break it's configuration. 1618 1619 // check each root configuration for reference to a module in mids 1620 for (ModuleId rootid : libraryRoots()) { 1621 // skip any root modules being removed 1622 if (mids.contains(rootid)) 1623 continue; 1624 1625 Configuration<Context> cf = readConfiguration(rootid); 1626 for (Context cx : cf.contexts()) { 1627 for (ModuleId mid : cx.modules()) { 1628 if (mids.contains(mid)) 1629 throw new ConfigurationException(mid + 1630 ": being used by " + rootid); 1631 } 1632 } 1633 } 1634 } 1635 1636 private static final String TRASH = ".trash"; 1637 // lazy initialization of Random 1638 private static class LazyInitialization { 1639 static final Random random = new Random(); 1640 } 1641 private static Path moduleTrashDir(File trash, ModuleId mid) 1642 throws IOException 1643 { 1644 String mn = mid.name(); 1645 Version version = mid.version(); 1646 String v = (version != null) ? version.toString() : "default"; 1647 for (;;) { 1648 long n = LazyInitialization.random.nextLong(); 1649 n = (n == Long.MIN_VALUE) ? 0 : Math.abs(n); 1650 String modTrashName = mn + '_' + v + '_' + Long.toString(n); 1651 File mtd = new File(trash, modTrashName); 1652 if (!mtd.exists()) 1653 return mtd.toPath(); 1654 } 1655 } 1656 1657 private List<IOException> removeWhileLocked(List<ModuleId> mids) { 1658 List<IOException> excs = new ArrayList<>(); 1659 // First move the modules to the .trash dir 1660 for (ModuleId mid : mids) { 1661 try { 1662 File md = moduleDir(root, mid); 1663 java.nio.file.Files.move(md.toPath(), 1664 moduleTrashDir(trash, mid), 1665 ATOMIC_MOVE); 1666 File p = md.getParentFile(); 1667 if (p.list().length == 0) 1668 java.nio.file.Files.delete(p.toPath()); 1669 } catch (IOException x) { 1670 excs.add(x); 1671 } 1672 } 1673 for (String tm : trash.list()) 1674 excs.addAll(ModuleFile.Reader.remove(new File(trash, tm))); 1675 1676 return excs; 1677 } 1678 1679 /** 1680 * <p> Pre-install one or more modules to an arbitrary destination 1681 * directory. </p> 1682 * 1683 * <p> A pre-installed module has the same format as within the library 1684 * itself, except that there is never a configuration file. </p> 1685 * 1686 * <p> This method is provided for use by the module-packaging tool. </p> 1687 * 1688 * @param mfs 1689 * The manifest describing the contents of the modules to be 1690 * pre-installed 1691 * 1692 * @param dst 1693 * The destination directory, with one subdirectory per module 1694 * name, each of which contains one subdirectory per version 1695 */ 1696 public void preInstall(Collection<Manifest> mfs, File dst) 1697 throws IOException 1698 { 1699 Files.mkdirs(dst, "module destination"); 1700 try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) { 1701 fc.lock(); 1702 for (Manifest mf : mfs) { 1703 installWhileLocked(mf, dst, false); 1704 } 1705 // no update to the module directory 1706 } 1707 } 1708 1709 public void preInstall(Manifest mf, File dst) 1710 throws IOException 1711 { 1712 preInstall(Collections.singleton(mf), dst); 1713 } 1714 1715 /** 1716 * Refresh the module library. 1717 */ 1718 public void refresh() throws IOException { 1719 try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) { 1720 fc.lock(); 1721 moduleDictionary.refresh(); 1722 moduleDictionary.store(); 1723 } 1724 } 1725 1726 /** 1727 * <p> Update the configurations of any root modules affected by the 1728 * copying of the named modules, in pre-installed format, into this 1729 * library. </p> 1730 * 1731 * @param mids 1732 * The module ids of the new or updated modules, or 1733 * {@code null} if the configuration of every root module 1734 * should be (re)computed 1735 */ 1736 public void configure(Collection<ModuleId> mids) 1737 throws ConfigurationException, IOException 1738 { 1739 try (FileChannel fc = FileChannel.open(lockf.toPath(), WRITE)) { 1740 fc.lock(); 1741 configureWhileModuleDirectoryLocked(mids); 1742 } 1743 } 1744 1745 private void configureWhileModuleDirectoryLocked(Collection<ModuleId> mids) 1746 throws ConfigurationException, IOException 1747 { 1748 // ## mids not used yet 1749 for (ModuleId mid : libraryRoots()) { 1750 // ## We could be a lot more clever about this! 1751 Configuration<Context> cf 1752 = Configurator.configure(this, mid.toQuery()); 1753 File md = moduleDictionary.findDeclaringModuleDir(mid); 1754 new StoredConfiguration(md, cf).store(); 1755 } 1756 } 1757 1758 private List<ModuleId> libraryRoots() 1759 throws IOException 1760 { 1761 List<ModuleId> roots = new ArrayList<>(); 1762 for (ModuleId mid : listLocalDeclaringModuleIds()) { 1763 // each module can have multiple entry points, but 1764 // only one configuration for each module. 1765 ModuleInfo mi = readModuleInfo(mid); 1766 for (ModuleView mv : mi.views()) { 1767 if (mv.mainClass() != null) { 1768 roots.add(mid); 1769 break; 1770 } 1771 } 1772 } 1773 return roots; 1774 } 1775 1776 public URI findLocalResource(ModuleId mid, String name) 1777 throws IOException 1778 { 1779 return locateContent(mid, name); 1780 } 1781 1782 public File findLocalNativeLibrary(ModuleId mid, String name) 1783 throws IOException 1784 { 1785 File f = natlibs(); 1786 if (f == null) { 1787 f = moduleDictionary.findDeclaringModuleDir(mid); 1788 if (f == null) 1789 return null; 1790 f = new File(f, "lib"); 1791 } 1792 f = new File(f, name); 1793 if (!f.exists()) 1794 return null; 1795 return f; 1796 } 1797 1798 public File classPath(ModuleId mid) 1799 throws IOException 1800 { 1801 File md = moduleDictionary.findDeclaringModuleDir(mid); 1802 if (md == null) { 1803 if (parent != null) 1804 return parent.classPath(mid); 1805 return null; 1806 } 1807 // ## Check for other formats here 1808 return new File(md, "classes"); 1809 } 1810 1811 /** 1812 * <p> Re-index the classes of the named previously-installed modules, and 1813 * then update the configurations of any affected root modules. </p> 1814 * 1815 * <p> This method is intended for use during development, when a build 1816 * process may update a previously-installed module in place, adding or 1817 * removing classes. </p> 1818 * 1819 * @param mids 1820 * The module ids of the new or updated modules, or 1821 * {@code null} if the configuration of every root module 1822 * should be (re)computed 1823 */ 1824 public void reIndex(List<ModuleId> mids) 1825 throws ConfigurationException, IOException 1826 { 1827 for (ModuleId mid : mids) 1828 reIndex(mid); 1829 configure(mids); 1830 } 1831 1832 private static final class ModuleDictionary 1833 { 1834 private static final String FILE 1835 = FileConstants.META_PREFIX + "mids"; 1836 1837 private final File root; 1838 private final File file; 1839 private Map<String,Set<ModuleId>> moduleIdsForName; 1840 private Map<ModuleId,ModuleId> providingModuleIds; 1841 private Set<ModuleId> modules; 1842 private long lastUpdated; 1843 1844 ModuleDictionary(File root) { 1845 this.root = root; 1846 this.file = new File(root, FILE); 1847 this.providingModuleIds = new LinkedHashMap<>(); 1848 this.moduleIdsForName = new LinkedHashMap<>(); 1849 this.modules = new HashSet<>(); 1850 this.lastUpdated = -1; 1851 } 1852 1853 private static FileHeader fileHeader() { 1854 return (new FileHeader() 1855 .type(FileConstants.Type.LIBRARY_MODULE_IDS) 1856 .majorVersion(Header.MAJOR_VERSION) 1857 .minorVersion(Header.MINOR_VERSION)); 1858 } 1859 1860 void load() throws IOException { 1861 if (lastUpdated == file.lastModified()) 1862 return; 1863 1864 providingModuleIds = new LinkedHashMap<>(); 1865 moduleIdsForName = new LinkedHashMap<>(); 1866 modules = new HashSet<>(); 1867 lastUpdated = file.lastModified(); 1868 1869 try (FileInputStream fin = new FileInputStream(file); 1870 DataInputStream in = new DataInputStream(new BufferedInputStream(fin))) 1871 { 1872 FileHeader fh = fileHeader(); 1873 fh.read(in); 1874 int nMids = in.readInt(); 1875 for (int j = 0; j < nMids; j++) { 1876 ModuleId mid = jms.parseModuleId(in.readUTF()); 1877 ModuleId pmid = jms.parseModuleId(in.readUTF()); 1878 providingModuleIds.put(mid, pmid); 1879 addModuleId(mid); 1880 addModuleId(pmid); 1881 if (mid.equals(pmid)) 1882 modules.add(mid); 1883 } 1884 } 1885 } 1886 1887 void store() throws IOException { 1888 File newfn = new File(root, "mids.new"); 1889 FileOutputStream fout = new FileOutputStream(newfn); 1890 DataOutputStream out = new DataOutputStream(new BufferedOutputStream(fout)); 1891 try { 1892 try { 1893 fileHeader().write(out); 1894 out.writeInt(providingModuleIds.size()); 1895 for (Map.Entry<ModuleId, ModuleId> e : providingModuleIds.entrySet()) { 1896 out.writeUTF(e.getKey().toString()); 1897 out.writeUTF(e.getValue().toString()); 1898 } 1899 } finally { 1900 out.close(); 1901 } 1902 } catch (IOException x) { 1903 newfn.delete(); 1904 throw x; 1905 } 1906 java.nio.file.Files.move(newfn.toPath(), file.toPath(), ATOMIC_MOVE); 1907 } 1908 1909 void gatherLocalModuleIds(String moduleName, Set<ModuleId> mids) 1910 throws IOException 1911 { 1912 if (lastUpdated != file.lastModified()) 1913 load(); 1914 1915 if (moduleName == null) { 1916 mids.addAll(providingModuleIds.keySet()); 1917 } else { 1918 Set<ModuleId> res = moduleIdsForName.get(moduleName); 1919 if (res != null) 1920 mids.addAll(res); 1921 } 1922 } 1923 1924 ModuleId getDeclaringModule(ModuleId mid) throws IOException { 1925 if (lastUpdated != file.lastModified()) 1926 load(); 1927 1928 ModuleId pmid = providingModuleIds.get(mid); 1929 if (pmid != null && !pmid.equals(providingModuleIds.get(pmid))) { 1930 // mid is an alias 1931 pmid = providingModuleIds.get(pmid); 1932 } 1933 return pmid; 1934 } 1935 1936 File findDeclaringModuleDir(ModuleId mid) 1937 throws IOException 1938 { 1939 ModuleId dmid = getDeclaringModule(mid); 1940 if (dmid == null) 1941 return null; 1942 1943 File md = moduleDir(root, dmid); 1944 assert md.exists(); 1945 checkModuleDir(md); 1946 return md; 1947 } 1948 1949 Set<ModuleId> modules() throws IOException { 1950 if (lastUpdated != file.lastModified()) 1951 load(); 1952 return modules; 1953 } 1954 1955 void addModuleId(ModuleId mid) { 1956 Set<ModuleId> mids = moduleIdsForName.get(mid.name()); 1957 if (mids == null) { 1958 mids = new HashSet<>(); 1959 moduleIdsForName.put(mid.name(), mids); 1960 } 1961 mids.add(mid); 1962 } 1963 1964 File add(ModuleInfo mi) 1965 throws ConfigurationException, IOException 1966 { 1967 File md = ensureNewModule(mi); 1968 addToDirectory(mi); 1969 return md; 1970 } 1971 1972 private void addToDirectory(ModuleInfo mi) { 1973 modules.add(mi.id()); 1974 for (ModuleView view : mi.views()) { 1975 providingModuleIds.put(view.id(), mi.id()); 1976 addModuleId(view.id()); 1977 for (ModuleId alias : view.aliases()) { 1978 providingModuleIds.put(alias, view.id()); 1979 addModuleId(alias); 1980 } 1981 } 1982 } 1983 1984 void remove(ModuleInfo mi) throws IOException { 1985 modules.remove(mi.id()); 1986 for (ModuleView view : mi.views()) { 1987 providingModuleIds.remove(view.id()); 1988 Set<ModuleId> mids = moduleIdsForName.get(view.id().name()); 1989 if (mids != null) 1990 mids.remove(view.id()); 1991 for (ModuleId alias : view.aliases()) { 1992 providingModuleIds.remove(alias); 1993 mids = moduleIdsForName.get(alias.name()); 1994 if (mids != null) 1995 mids.remove(view.id()); 1996 } 1997 } 1998 File md = moduleDir(root, mi.id()); 1999 delete(md); 2000 } 2001 2002 private void delete(File md) throws IOException { 2003 if (!md.exists()) 2004 return; 2005 2006 checkModuleDir(md); 2007 ModuleFile.Reader.remove(md); 2008 File parent = md.getParentFile(); 2009 if (parent.list().length == 0) 2010 parent.delete(); 2011 } 2012 2013 void refresh() throws IOException { 2014 providingModuleIds = new LinkedHashMap<>(); 2015 moduleIdsForName = new LinkedHashMap<>(); 2016 modules = new HashSet<>(); 2017 2018 try (DirectoryStream<Path> ds = java.nio.file.Files.newDirectoryStream(root.toPath())) { 2019 for (Path mnp : ds) { 2020 String mn = mnp.toFile().getName(); 2021 if (mn.startsWith(FileConstants.META_PREFIX) || 2022 TRASH.equals(mn)) { 2023 continue; 2024 } 2025 2026 try (DirectoryStream<Path> mds = java.nio.file.Files.newDirectoryStream(mnp)) { 2027 for (Path versionp : mds) { 2028 File v = versionp.toFile(); 2029 if (!v.isDirectory()) { 2030 throw new IOException(versionp + ": Not a directory"); 2031 } 2032 modules.add(jms.parseModuleId(mn, v.getName())); 2033 } 2034 } 2035 } 2036 } 2037 for (ModuleId mid : modules) { 2038 byte[] bs = Files.load(new File(moduleDir(root, mid), "info")); 2039 ModuleInfo mi = jms.parseModuleInfo(bs); 2040 addToDirectory(mi); 2041 } 2042 } 2043 2044 private File ensureNewModule(ModuleInfo mi) 2045 throws ConfigurationException, IOException 2046 { 2047 for (ModuleView view : mi.views()) { 2048 if (providingModuleIds.containsKey(view.id())) { 2049 throw new ConfigurationException("module view " + view.id() 2050 + " already installed"); 2051 } 2052 for (ModuleId alias : view.aliases()) { 2053 ModuleId mid = alias; 2054 if (providingModuleIds.containsKey(mid)) { 2055 throw new ConfigurationException("alias " + alias 2056 + " already installed"); 2057 } 2058 } 2059 } 2060 File md = moduleDir(root, mi.id()); 2061 if (md.exists()) { 2062 throw new ConfigurationException("module " + mi.id() 2063 + " already installed"); 2064 } 2065 if (!md.mkdirs()) { 2066 throw new IOException(md + ": Cannot create"); 2067 } 2068 return md; 2069 } 2070 } 2071 2072 // -- Repositories -- 2073 2074 private static class RepoList 2075 implements RemoteRepositoryList 2076 { 2077 2078 private static final int MINOR_VERSION = 0; 2079 private static final int MAJOR_VERSION = 0; 2080 2081 private final File root; 2082 private final File listFile; 2083 2084 private RepoList(File r) { 2085 root = new File(r, FileConstants.META_PREFIX + "repos"); 2086 listFile = new File(root, FileConstants.META_PREFIX + "list"); 2087 } 2088 2089 private static FileHeader fileHeader() { 2090 return (new FileHeader() 2091 .type(FileConstants.Type.REMOTE_REPO_LIST) 2092 .majorVersion(MAJOR_VERSION) 2093 .minorVersion(MINOR_VERSION)); 2094 } 2095 2096 private List<RemoteRepository> repos = null; 2097 private long nextRepoId = 0; 2098 2099 private File repoDir(long id) { 2100 return new File(root, Long.toHexString(id)); 2101 } 2102 2103 private void load() throws IOException { 2104 2105 repos = new ArrayList<>(); 2106 if (!root.exists() || !listFile.exists()) 2107 return; 2108 FileInputStream fin = new FileInputStream(listFile); 2109 DataInputStream in 2110 = new DataInputStream(new BufferedInputStream(fin)); 2111 try { 2112 2113 FileHeader fh = fileHeader(); 2114 fh.read(in); 2115 nextRepoId = in.readLong(); 2116 int n = in.readInt(); 2117 long[] ids = new long[n]; 2118 for (int i = 0; i < n; i++) 2119 ids[i] = in.readLong(); 2120 RemoteRepository parent = null; 2121 2122 // Load in reverse order so that parents are correct 2123 for (int i = n - 1; i >= 0; i--) { 2124 long id = ids[i]; 2125 RemoteRepository rr 2126 = RemoteRepository.open(repoDir(id), id, parent); 2127 repos.add(rr); 2128 parent = rr; 2129 } 2130 Collections.reverse(repos); 2131 2132 } finally { 2133 in.close(); 2134 } 2135 2136 } 2137 2138 private List<RemoteRepository> roRepos = null; 2139 2140 // Unmodifiable 2141 public List<RemoteRepository> repositories() throws IOException { 2142 if (repos == null) { 2143 load(); 2144 roRepos = Collections.unmodifiableList(repos); 2145 } 2146 return roRepos; 2147 } 2148 2149 public RemoteRepository firstRepository() throws IOException { 2150 repositories(); 2151 return repos.isEmpty() ? null : repos.get(0); 2152 } 2153 2154 private void store() throws IOException { 2155 File newfn = new File(root, "list.new"); 2156 FileOutputStream fout = new FileOutputStream(newfn); 2157 DataOutputStream out 2158 = new DataOutputStream(new BufferedOutputStream(fout)); 2159 try { 2160 try { 2161 fileHeader().write(out); 2162 out.writeLong(nextRepoId); 2163 out.writeInt(repos.size()); 2164 for (RemoteRepository rr : repos) 2165 out.writeLong(rr.id()); 2166 } finally { 2167 out.close(); 2168 } 2169 } catch (IOException x) { 2170 newfn.delete(); 2171 throw x; 2172 } 2173 java.nio.file.Files.move(newfn.toPath(), listFile.toPath(), ATOMIC_MOVE); 2174 } 2175 2176 public RemoteRepository add(URI u, int position) 2177 throws IOException 2178 { 2179 2180 if (repos == null) 2181 load(); 2182 for (RemoteRepository rr : repos) { 2183 if (rr.location().equals(u)) // ## u not canonical 2184 throw new IllegalStateException(u + ": Already in" 2185 + " repository list"); 2186 } 2187 if (!root.exists()) { 2188 if (!root.mkdir()) 2189 throw new IOException(root + ": Cannot create directory"); 2190 } 2191 2192 if (repos.size() == Integer.MAX_VALUE) 2193 throw new IllegalStateException("Too many repositories"); 2194 if (position < 0) 2195 throw new IllegalArgumentException("Invalid index"); 2196 2197 long id = nextRepoId++; 2198 RemoteRepository rr = RemoteRepository.create(repoDir(id), u, id); 2199 try { 2200 rr.updateCatalog(true); 2201 } catch (IOException x) { 2202 rr.delete(); 2203 nextRepoId--; 2204 throw x; 2205 } 2206 2207 if (position >= repos.size()) { 2208 repos.add(rr); 2209 } else if (position >= 0) { 2210 List<RemoteRepository> prefix 2211 = new ArrayList<>(repos.subList(0, position)); 2212 List<RemoteRepository> suffix 2213 = new ArrayList<>(repos.subList(position, repos.size())); 2214 repos.clear(); 2215 repos.addAll(prefix); 2216 repos.add(rr); 2217 repos.addAll(suffix); 2218 } 2219 store(); 2220 2221 return rr; 2222 2223 } 2224 2225 public boolean remove(RemoteRepository rr) 2226 throws IOException 2227 { 2228 if (!repos.remove(rr)) 2229 return false; 2230 store(); 2231 File rd = repoDir(rr.id()); 2232 for (File f : rd.listFiles()) { 2233 if (!f.delete()) 2234 throw new IOException(f + ": Cannot delete"); 2235 } 2236 if (!rd.delete()) 2237 throw new IOException(rd + ": Cannot delete"); 2238 return true; 2239 } 2240 2241 public boolean areCatalogsStale() throws IOException { 2242 for (RemoteRepository rr : repos) { 2243 if (rr.isCatalogStale()) 2244 return true; 2245 } 2246 return false; 2247 } 2248 2249 public boolean updateCatalogs(boolean force) throws IOException { 2250 boolean updated = false; 2251 for (RemoteRepository rr : repos) { 2252 if (rr.updateCatalog(force)) 2253 updated = true; 2254 } 2255 return updated; 2256 } 2257 2258 } 2259 2260 private RemoteRepositoryList repoList = null; 2261 2262 public RemoteRepositoryList repositoryList() 2263 throws IOException 2264 { 2265 if (repoList == null) 2266 repoList = new RepoList(root); 2267 return repoList; 2268 } 2269 2270 } --- EOF ---