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