1 /* 2 * Copyright (c) 2014, 2025, 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 package jdk.internal.jimage; 26 27 import java.io.IOException; 28 import java.nio.ByteBuffer; 29 import java.nio.ByteOrder; 30 import java.nio.IntBuffer; 31 import java.nio.file.Files; 32 import java.nio.file.Path; 33 import java.nio.file.attribute.BasicFileAttributes; 34 import java.util.ArrayList; 35 import java.util.Arrays; 36 import java.util.Collections; 37 import java.util.HashMap; 38 import java.util.HashSet; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Objects; 42 import java.util.Set; 43 import java.util.function.Function; 44 import java.util.function.Supplier; 45 import java.util.stream.Stream; 46 47 /** 48 * A view over the entries of a jimage file with a unified namespace suitable 49 * for file system use. The jimage entries (resources, module and package 50 * information) are mapped into a unified hierarchy of named nodes, which serve 51 * as the underlying structure for {@code JrtFileSystem} and other utilities. 52 * 53 * <p>Entries in jimage are expressed as one of three {@link Node} types; 54 * resource nodes, directory nodes and link nodes. 55 * 56 * <p>When remapping jimage entries, jimage location names (e.g. {@code 57 * "/java.base/java/lang/Integer.class"}) are prefixed with {@code "/modules"} 58 * to form the names of resource nodes. This aligns with the naming of module 59 * entries in jimage (e.g. "/modules/java.base/java/lang"), which appear as 60 * directory nodes in {@code ImageReader}. 61 * 62 * <p>Package entries (e.g. {@code "/packages/java.lang"} appear as directory 63 * nodes containing link nodes, which resolve back to the root directory of the 64 * module in which that package exists (e.g. {@code "/modules/java.base"}). 65 * Unlike other nodes, the jimage file does not contain explicit entries for 66 * link nodes, and their existence is derived only from the contents of the 67 * parent directory. 68 * 69 * <p>While similar to {@code BasicImageReader}, this class is not a conceptual 70 * subtype of it, and deliberately hides types such as {@code ImageLocation} to 71 * give a focused API based only on nodes. 72 * 73 * @implNote This class needs to maintain JDK 8 source compatibility. 74 * 75 * It is used internally in the JDK to implement jimage/jrtfs access, 76 * but also compiled and delivered as part of the jrtfs.jar to support access 77 * to the jimage file provided by the shipped JDK by tools running on JDK 8. 78 */ 79 public final class ImageReader implements AutoCloseable { 80 private final SharedImageReader reader; 81 82 private volatile boolean closed; 83 84 private ImageReader(SharedImageReader reader) { 85 this.reader = reader; 86 } 87 88 /** 89 * Opens an image reader for a jimage file at the specified path, using the 90 * given byte order. 91 */ 92 public static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException { 93 Objects.requireNonNull(imagePath); 94 Objects.requireNonNull(byteOrder); 95 96 return SharedImageReader.open(imagePath, byteOrder); 97 } 98 99 /** 100 * Opens an image reader for a jimage file at the specified path, using the 101 * platform native byte order. 102 */ 103 public static ImageReader open(Path imagePath) throws IOException { 104 return open(imagePath, ByteOrder.nativeOrder()); 105 } 106 107 @Override 108 public void close() throws IOException { 109 if (closed) { 110 throw new IOException("image file already closed"); 111 } 112 reader.close(this); 113 closed = true; 114 } 115 116 private void ensureOpen() throws IOException { 117 if (closed) { 118 throw new IOException("image file closed"); 119 } 120 } 121 122 private void requireOpen() { 123 if (closed) { 124 throw new IllegalStateException("image file closed"); 125 } 126 } 127 128 /** 129 * Finds the node with the given name. 130 * 131 * @param name a node name of the form {@code "/modules/<module>/...} or 132 * {@code "/packages/<package>/...}. 133 * @return a node representing a resource, directory or symbolic link. 134 */ 135 public Node findNode(String name) throws IOException { 136 ensureOpen(); 137 return reader.findNode(name); 138 } 139 140 /** 141 * Returns a resource node in the given module, or null if no resource of 142 * that name exists. 143 * 144 * <p>This is equivalent to: 145 * <pre>{@code 146 * findNode("/modules/" + moduleName + "/" + resourcePath) 147 * }</pre> 148 * but more performant, and returns {@code null} for directories. 149 * 150 * @param moduleName The module name of the requested resource. 151 * @param resourcePath Trailing module-relative resource path, not starting 152 * with {@code '/'}. 153 */ 154 public Node findResourceNode(String moduleName, String resourcePath) 155 throws IOException { 156 ensureOpen(); 157 return reader.findResourceNode(moduleName, resourcePath); 158 } 159 160 /** 161 * Returns whether a resource exists in the given module. 162 * 163 * <p>This is equivalent to: 164 * <pre>{@code 165 * findResourceNode(moduleName, resourcePath) != null 166 * }</pre> 167 * but more performant, and will not create or cache new nodes. 168 * 169 * @param moduleName The module name of the resource being tested for. 170 * @param resourcePath Trailing module-relative resource path, not starting 171 * with {@code '/'}. 172 */ 173 public boolean containsResource(String moduleName, String resourcePath) 174 throws IOException { 175 ensureOpen(); 176 return reader.containsResource(moduleName, resourcePath); 177 } 178 179 /** 180 * Returns a copy of the content of a resource node. The buffer returned by 181 * this method is not cached by the node, and each call returns a new array 182 * instance. 183 * 184 * @throws IOException if the content cannot be returned (including if the 185 * given node is not a resource node). 186 */ 187 public byte[] getResource(Node node) throws IOException { 188 ensureOpen(); 189 return reader.getResource(node); 190 } 191 192 /** 193 * Releases a (possibly cached) {@link ByteBuffer} obtained via 194 * {@link #getResourceBuffer(Node)}. 195 * 196 * <p>Note that no testing is performed to check whether the buffer about 197 * to be released actually came from a call to {@code getResourceBuffer()}. 198 */ 199 public static void releaseByteBuffer(ByteBuffer buffer) { 200 BasicImageReader.releaseByteBuffer(buffer); 201 } 202 203 /** 204 * Returns the content of a resource node in a possibly cached byte buffer. 205 * Callers of this method must call {@link #releaseByteBuffer(ByteBuffer)} 206 * when they are finished with it. 207 */ 208 public ByteBuffer getResourceBuffer(Node node) { 209 requireOpen(); 210 if (!node.isResource()) { 211 throw new IllegalArgumentException("Not a resource node: " + node); 212 } 213 return reader.getResourceBuffer(node.getLocation()); 214 } 215 216 private static final class SharedImageReader extends BasicImageReader { 217 private static final Map<Path, SharedImageReader> OPEN_FILES = new HashMap<>(); 218 private static final String MODULES_ROOT = "/modules"; 219 private static final String PACKAGES_ROOT = "/packages"; 220 // There are >30,000 nodes in a complete jimage tree, and even relatively 221 // common tasks (e.g. starting up javac) load somewhere in the region of 222 // 1000 classes. Thus, an initial capacity of 2000 is a reasonable guess. 223 private static final int INITIAL_NODE_CACHE_CAPACITY = 2000; 224 225 // List of openers for this shared image. 226 private final Set<ImageReader> openers = new HashSet<>(); 227 228 // Attributes of the jimage file. The jimage file does not contain 229 // attributes for the individual resources (yet). We use attributes 230 // of the jimage file itself (creation, modification, access times). 231 private final BasicFileAttributes imageFileAttributes; 232 233 // Cache of all user visible nodes, guarded by synchronizing 'this' instance. 234 private final Map<String, Node> nodes; 235 // Used to classify ImageLocation instances without string comparison. 236 private final int modulesStringOffset; 237 private final int packagesStringOffset; 238 239 private SharedImageReader(Path imagePath, ByteOrder byteOrder) throws IOException { 240 super(imagePath, byteOrder); 241 this.imageFileAttributes = Files.readAttributes(imagePath, BasicFileAttributes.class); 242 this.nodes = new HashMap<>(INITIAL_NODE_CACHE_CAPACITY); 243 // Pick stable jimage names from which to extract string offsets (we cannot 244 // use "/modules" or "/packages", since those have a module offset of zero). 245 this.modulesStringOffset = getModuleOffset("/modules/java.base"); 246 this.packagesStringOffset = getModuleOffset("/packages/java.lang"); 247 248 // Node creation is very lazy, so we can just make the top-level directories 249 // now without the risk of triggering the building of lots of other nodes. 250 Directory packages = newDirectory(PACKAGES_ROOT); 251 nodes.put(packages.getName(), packages); 252 Directory modules = newDirectory(MODULES_ROOT); 253 nodes.put(modules.getName(), modules); 254 255 Directory root = newDirectory("/"); 256 root.setChildren(Arrays.asList(packages, modules)); 257 nodes.put(root.getName(), root); 258 } 259 260 /** 261 * Returns the offset of the string denoting the leading "module" segment in 262 * the given path (e.g. {@code <module>/<path>}). We can't just pass in the 263 * {@code /<module>} string here because that has a module offset of zero. 264 */ 265 private int getModuleOffset(String path) { 266 ImageLocation location = findLocation(path); 267 assert location != null : "Cannot find expected jimage location: " + path; 268 int offset = location.getModuleOffset(); 269 assert offset != 0 : "Invalid module offset for jimage location: " + path; 270 return offset; 271 } 272 273 private static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException { 274 Objects.requireNonNull(imagePath); 275 Objects.requireNonNull(byteOrder); 276 277 synchronized (OPEN_FILES) { 278 SharedImageReader reader = OPEN_FILES.get(imagePath); 279 280 if (reader == null) { 281 // Will fail with an IOException if wrong byteOrder. 282 reader = new SharedImageReader(imagePath, byteOrder); 283 OPEN_FILES.put(imagePath, reader); 284 } else if (reader.getByteOrder() != byteOrder) { 285 throw new IOException("\"" + reader.getName() + "\" is not an image file"); 286 } 287 288 ImageReader image = new ImageReader(reader); 289 reader.openers.add(image); 290 291 return image; 292 } 293 } 294 295 public void close(ImageReader image) throws IOException { 296 Objects.requireNonNull(image); 297 298 synchronized (OPEN_FILES) { 299 if (!openers.remove(image)) { 300 throw new IOException("image file already closed"); 301 } 302 303 if (openers.isEmpty()) { 304 close(); 305 nodes.clear(); 306 307 if (!OPEN_FILES.remove(this.getImagePath(), this)) { 308 throw new IOException("image file not found in open list"); 309 } 310 } 311 } 312 } 313 314 /** 315 * Returns a node with the given name, or null if no resource or directory of 316 * that name exists. 317 * 318 * <p>Note that there is no reentrant calling back to this method from within 319 * the node handling code. 320 * 321 * @param name an absolute, {@code /}-separated path string, prefixed with either 322 * "/modules" or "/packages". 323 */ 324 synchronized Node findNode(String name) { 325 Node node = nodes.get(name); 326 if (node == null) { 327 // We cannot get the root paths ("/modules" or "/packages") here 328 // because those nodes are already in the nodes cache. 329 if (name.startsWith(MODULES_ROOT + "/")) { 330 // This may perform two lookups, one for a directory (in 331 // "/modules/...") and one for a non-prefixed resource 332 // (with "/modules" removed). 333 node = buildModulesNode(name); 334 } else if (name.startsWith(PACKAGES_ROOT + "/")) { 335 node = buildPackagesNode(name); 336 } 337 if (node != null) { 338 nodes.put(node.getName(), node); 339 } 340 } else if (!node.isCompleted()) { 341 // Only directories can be incomplete. 342 assert node instanceof Directory : "Invalid incomplete node: " + node; 343 completeDirectory((Directory) node); 344 } 345 assert node == null || node.isCompleted() : "Incomplete node: " + node; 346 return node; 347 } 348 349 /** 350 * Returns a resource node in the given module, or null if no resource of 351 * that name exists. 352 * 353 * <p>Note that there is no reentrant calling back to this method from within 354 * the node handling code. 355 */ 356 Node findResourceNode(String moduleName, String resourcePath) { 357 // Unlike findNode(), this method makes only one lookup in the 358 // underlying jimage, but can only reliably return resource nodes. 359 if (moduleName.indexOf('/') >= 0) { 360 throw new IllegalArgumentException("invalid module name: " + moduleName); 361 } 362 String nodeName = MODULES_ROOT + "/" + moduleName + "/" + resourcePath; 363 // Synchronize as tightly as possible to reduce locking contention. 364 synchronized (this) { 365 Node node = nodes.get(nodeName); 366 if (node == null) { 367 ImageLocation loc = findLocation(moduleName, resourcePath); 368 if (loc != null && isResource(loc)) { 369 node = newResource(nodeName, loc); 370 nodes.put(node.getName(), node); 371 } 372 return node; 373 } else { 374 return node.isResource() ? node : null; 375 } 376 } 377 } 378 379 /** 380 * Returns whether a resource exists in the given module. 381 * 382 * <p>This method is expected to be called frequently for resources 383 * which do not exist in the given module (e.g. as part of classpath 384 * search). As such, it skips checking the nodes cache and only checks 385 * for an entry in the jimage file, as this is faster if the resource 386 * is not present. This also means it doesn't need synchronization. 387 */ 388 boolean containsResource(String moduleName, String resourcePath) { 389 if (moduleName.indexOf('/') >= 0) { 390 throw new IllegalArgumentException("invalid module name: " + moduleName); 391 } 392 // If the given module name is 'modules', then 'isResource()' 393 // returns false to prevent false positives. 394 ImageLocation loc = findLocation(moduleName, resourcePath); 395 return loc != null && isResource(loc); 396 } 397 398 /** 399 * Builds a node in the "/modules/..." namespace. 400 * 401 * <p>Called by {@link #findNode(String)} if a {@code /modules/...} node 402 * is not present in the cache. 403 */ 404 private Node buildModulesNode(String name) { 405 assert name.startsWith(MODULES_ROOT + "/") : "Invalid module node name: " + name; 406 // Returns null for non-directory resources, since the jimage name does not 407 // start with "/modules" (e.g. "/java.base/java/lang/Object.class"). 408 ImageLocation loc = findLocation(name); 409 if (loc != null) { 410 assert name.equals(loc.getFullName()) : "Mismatched location for directory: " + name; 411 assert isModulesSubdirectory(loc) : "Invalid modules directory: " + name; 412 return completeModuleDirectory(newDirectory(name), loc); 413 } 414 // Now try the non-prefixed resource name, but be careful to avoid false 415 // positives for names like "/modules/modules/xxx" which could return a 416 // location of a directory entry. 417 loc = findLocation(name.substring(MODULES_ROOT.length())); 418 return loc != null && isResource(loc) ? newResource(name, loc) : null; 419 } 420 421 /** 422 * Builds a node in the "/packages/..." namespace. 423 * 424 * <p>Called by {@link #findNode(String)} if a {@code /packages/...} node 425 * is not present in the cache. 426 */ 427 private Node buildPackagesNode(String name) { 428 // There are only locations for the root "/packages" or "/packages/xxx" 429 // directories, but not the symbolic links below them (the links can be 430 // entirely derived from the name information in the parent directory). 431 // However, unlike resources this means that we do not have a constant 432 // time lookup for link nodes when creating them. 433 int packageStart = PACKAGES_ROOT.length() + 1; 434 int packageEnd = name.indexOf('/', packageStart); 435 if (packageEnd == -1) { 436 ImageLocation loc = findLocation(name); 437 return loc != null ? completePackageDirectory(newDirectory(name), loc) : null; 438 } else { 439 // We cannot assume that the parent directory exists for a link node, since 440 // the given name is untrusted and could reference a non-existent link. 441 // However, if the parent directory is present, we can conclude that the 442 // given name was not a valid link (or else it would already be cached). 443 String dirName = name.substring(0, packageEnd); 444 if (!nodes.containsKey(dirName)) { 445 ImageLocation loc = findLocation(dirName); 446 // If the parent location doesn't exist, the link node cannot exist. 447 if (loc != null) { 448 nodes.put(dirName, completePackageDirectory(newDirectory(dirName), loc)); 449 // When the parent is created its child nodes are created and cached, 450 // but this can still return null if given name wasn't a valid link. 451 return nodes.get(name); 452 } 453 } 454 } 455 return null; 456 } 457 458 /** Completes a directory by ensuring its child list is populated correctly. */ 459 private void completeDirectory(Directory dir) { 460 String name = dir.getName(); 461 // Since the node exists, we can assert that its name starts with 462 // either "/modules" or "/packages", making differentiation easy. 463 // It also means that the name is valid, so it must yield a location. 464 assert name.startsWith(MODULES_ROOT) || name.startsWith(PACKAGES_ROOT); 465 ImageLocation loc = findLocation(name); 466 assert loc != null && name.equals(loc.getFullName()) : "Invalid location for name: " + name; 467 // We cannot use 'isXxxSubdirectory()' methods here since we could 468 // be given a top-level directory (for which that test doesn't work). 469 // The string MUST start "/modules" or "/packages" here. 470 if (name.charAt(1) == 'm') { 471 completeModuleDirectory(dir, loc); 472 } else { 473 completePackageDirectory(dir, loc); 474 } 475 assert dir.isCompleted() : "Directory must be complete by now: " + dir; 476 } 477 478 /** 479 * Completes a modules directory by setting the list of child nodes. 480 * 481 * <p>The given directory can be the top level {@code /modules} directory, 482 * so it is NOT safe to use {@code isModulesSubdirectory(loc)} here. 483 */ 484 private Directory completeModuleDirectory(Directory dir, ImageLocation loc) { 485 assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir; 486 List<Node> children = createChildNodes(loc, childLoc -> { 487 if (isModulesSubdirectory(childLoc)) { 488 return nodes.computeIfAbsent(childLoc.getFullName(), this::newDirectory); 489 } else { 490 // Add "/modules" prefix to image location paths to get node names. 491 String resourceName = childLoc.getFullName(true); 492 return nodes.computeIfAbsent(resourceName, n -> newResource(n, childLoc)); 493 } 494 }); 495 dir.setChildren(children); 496 return dir; 497 } 498 499 /** 500 * Completes a package directory by setting the list of child nodes. 501 * 502 * <p>The given directory can be the top level {@code /packages} directory, 503 * so it is NOT safe to use {@code isPackagesSubdirectory(loc)} here. 504 */ 505 private Directory completePackageDirectory(Directory dir, ImageLocation loc) { 506 assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir; 507 // The only directories in the "/packages" namespace are "/packages" or 508 // "/packages/<package>". However, unlike "/modules" directories, the 509 // location offsets mean different things. 510 List<Node> children; 511 if (dir.getName().equals(PACKAGES_ROOT)) { 512 // Top-level directory just contains a list of subdirectories. 513 children = createChildNodes(loc, c -> nodes.computeIfAbsent(c.getFullName(), this::newDirectory)); 514 } else { 515 // A package directory's content is array of offset PAIRS in the 516 // Strings table, but we only need the 2nd value of each pair. 517 IntBuffer intBuffer = getOffsetBuffer(loc); 518 int offsetCount = intBuffer.capacity(); 519 assert (offsetCount & 0x1) == 0 : "Offset count must be even: " + offsetCount; 520 children = new ArrayList<>(offsetCount / 2); 521 // Iterate the 2nd offset in each pair (odd indices). 522 for (int i = 1; i < offsetCount; i += 2) { 523 String moduleName = getString(intBuffer.get(i)); 524 children.add(nodes.computeIfAbsent( 525 dir.getName() + "/" + moduleName, 526 n -> newLinkNode(n, MODULES_ROOT + "/" + moduleName))); 527 } 528 } 529 // This only happens once and "completes" the directory. 530 dir.setChildren(children); 531 return dir; 532 } 533 534 /** 535 * Creates the list of child nodes for a {@code Directory} based on a given 536 * 537 * <p>Note: This cannot be used for package subdirectories as they have 538 * child offsets stored differently to other directories. 539 */ 540 private List<Node> createChildNodes(ImageLocation loc, Function<ImageLocation, Node> newChildFn) { 541 IntBuffer offsets = getOffsetBuffer(loc); 542 int childCount = offsets.capacity(); 543 List<Node> children = new ArrayList<>(childCount); 544 for (int i = 0; i < childCount; i++) { 545 children.add(newChildFn.apply(getLocation(offsets.get(i)))); 546 } 547 return children; 548 } 549 550 /** Helper to extract the integer offset buffer from a directory location. */ 551 private IntBuffer getOffsetBuffer(ImageLocation dir) { 552 assert !isResource(dir) : "Not a directory: " + dir.getFullName(); 553 byte[] offsets = getResource(dir); 554 ByteBuffer buffer = ByteBuffer.wrap(offsets); 555 buffer.order(getByteOrder()); 556 return buffer.asIntBuffer(); 557 } 558 559 /** 560 * Efficiently determines if an image location is a resource. 561 * 562 * <p>A resource must have a valid module associated with it, so its 563 * module offset must be non-zero, and not equal to the offsets for 564 * "/modules/..." or "/packages/..." entries. 565 */ 566 private boolean isResource(ImageLocation loc) { 567 int moduleOffset = loc.getModuleOffset(); 568 return moduleOffset != 0 569 && moduleOffset != modulesStringOffset 570 && moduleOffset != packagesStringOffset; 571 } 572 573 /** 574 * Determines if an image location is a directory in the {@code /modules} 575 * namespace (if so, the location name is the node name). 576 * 577 * <p>In jimage, every {@code ImageLocation} under {@code /modules/} is a 578 * directory and has the same value for {@code getModule()}, and {@code 579 * getModuleOffset()}. 580 */ 581 private boolean isModulesSubdirectory(ImageLocation loc) { 582 return loc.getModuleOffset() == modulesStringOffset; 583 } 584 585 /** 586 * Creates an "incomplete" directory node with no child nodes set. 587 * Directories need to be "completed" before they are returned by 588 * {@link #findNode(String)}. 589 */ 590 private Directory newDirectory(String name) { 591 return new Directory(name, imageFileAttributes); 592 } 593 594 /** 595 * Creates a new resource from an image location. This is the only case 596 * where the image location name does not match the requested node name. 597 * In image files, resource locations are NOT prefixed by {@code /modules}. 598 */ 599 private Resource newResource(String name, ImageLocation loc) { 600 assert name.equals(loc.getFullName(true)) : "Mismatched location for resource: " + name; 601 return new Resource(name, loc, imageFileAttributes); 602 } 603 604 /** 605 * Creates a new link node pointing at the given target name. 606 * 607 * <p>Note that target node is resolved each time {@code resolve()} is called, 608 * so if a link node is retained after its reader is closed, it will fail. 609 */ 610 private LinkNode newLinkNode(String name, String targetName) { 611 return new LinkNode(name, () -> findNode(targetName), imageFileAttributes); 612 } 613 614 /** Returns the content of a resource node. */ 615 private byte[] getResource(Node node) throws IOException { 616 // We could have been given a non-resource node here. 617 if (node.isResource()) { 618 return super.getResource(node.getLocation()); 619 } 620 throw new IOException("Not a resource: " + node); 621 } 622 } 623 624 /** 625 * A directory, resource or symbolic link. 626 * 627 * <h3 id="node_equality">Node Equality</h3> 628 * 629 * Nodes are identified solely by their name, and it is not valid to attempt 630 * to compare nodes from different reader instances. Different readers may 631 * produce nodes with the same names, but different contents. 632 * 633 * <p>Furthermore, since a {@link ImageReader} provides "perfect" caching of 634 * nodes, equality of nodes from the same reader is equivalent to instance 635 * identity. 636 */ 637 public abstract static class Node { 638 private final String name; 639 private final BasicFileAttributes fileAttrs; 640 641 /** 642 * Creates an abstract {@code Node}, which is either a resource, directory 643 * or symbolic link. 644 * 645 * <p>This constructor is only non-private so it can be used by the 646 * {@code ExplodedImage} class, and must not be used otherwise. 647 */ 648 protected Node(String name, BasicFileAttributes fileAttrs) { 649 this.name = Objects.requireNonNull(name); 650 this.fileAttrs = Objects.requireNonNull(fileAttrs); 651 } 652 653 // A node is completed when all its direct children have been built. 654 // As such, non-directory nodes are always complete. 655 boolean isCompleted() { 656 return true; 657 } 658 659 // Only resources can return a location. 660 ImageLocation getLocation() { 661 throw new IllegalStateException("not a resource: " + getName()); 662 } 663 664 /** 665 * Returns the name of this node (e.g. {@code 666 * "/modules/java.base/java/lang/Object.class"} or {@code 667 * "/packages/java.lang"}). 668 * 669 * <p>Note that for resource nodes this is NOT the underlying jimage 670 * resource name (it is prefixed with {@code "/modules"}). 671 */ 672 public final String getName() { 673 return name; 674 } 675 676 /** 677 * Returns file attributes for this node. The value returned may be the 678 * same for all nodes, and should not be relied upon for accuracy. 679 */ 680 public final BasicFileAttributes getFileAttributes() { 681 return fileAttrs; 682 } 683 684 /** 685 * Resolves a symbolic link to its target node. If this code is not a 686 * symbolic link, then it resolves to itself. 687 */ 688 public final Node resolveLink() { 689 return resolveLink(false); 690 } 691 692 /** 693 * Resolves a symbolic link to its target node. If this code is not a 694 * symbolic link, then it resolves to itself. 695 */ 696 public Node resolveLink(boolean recursive) { 697 return this; 698 } 699 700 /** Returns whether this node is a symbolic link. */ 701 public boolean isLink() { 702 return false; 703 } 704 705 /** 706 * Returns whether this node is a directory. Directory nodes can have 707 * {@link #getChildNames()} invoked to get the fully qualified names 708 * of any child nodes. 709 */ 710 public boolean isDirectory() { 711 return false; 712 } 713 714 /** 715 * Returns whether this node is a resource. Resource nodes can have 716 * their contents obtained via {@link ImageReader#getResource(Node)} 717 * or {@link ImageReader#getResourceBuffer(Node)}. 718 */ 719 public boolean isResource() { 720 return false; 721 } 722 723 /** 724 * Returns the fully qualified names of any child nodes for a directory. 725 * 726 * <p>By default, this method throws {@link IllegalStateException} and 727 * is overridden for directories. 728 */ 729 public Stream<String> getChildNames() { 730 throw new IllegalStateException("not a directory: " + getName()); 731 } 732 733 /** 734 * Returns the uncompressed size of this node's content. If this node is 735 * not a resource, this method returns zero. 736 */ 737 public long size() { 738 return 0L; 739 } 740 741 /** 742 * Returns the compressed size of this node's content. If this node is 743 * not a resource, this method returns zero. 744 */ 745 public long compressedSize() { 746 return 0L; 747 } 748 749 /** 750 * Returns the extension string of a resource node. If this node is not 751 * a resource, this method returns null. 752 */ 753 public String extension() { 754 return null; 755 } 756 757 @Override 758 public final String toString() { 759 return getName(); 760 } 761 762 /** See <a href="#node_equality">Node Equality</a>. */ 763 @Override 764 public final int hashCode() { 765 return name.hashCode(); 766 } 767 768 /** See <a href="#node_equality">Node Equality</a>. */ 769 @Override 770 public final boolean equals(Object other) { 771 if (this == other) { 772 return true; 773 } 774 775 if (other instanceof Node) { 776 return name.equals(((Node) other).name); 777 } 778 779 return false; 780 } 781 } 782 783 /** 784 * Directory node (referenced from a full path, without a trailing '/'). 785 * 786 * <p>Directory nodes have two distinct states: 787 * <ul> 788 * <li>Incomplete: The child list has not been set. 789 * <li>Complete: The child list has been set. 790 * </ul> 791 * 792 * <p>When a directory node is returned by {@link ImageReader#findNode(String)} 793 * it is always complete, but this DOES NOT mean that its child nodes are 794 * complete yet. 795 * 796 * <p>To avoid users being able to access incomplete child nodes, the 797 * {@code Node} API offers only a way to obtain child node names, forcing 798 * callers to invoke {@code findNode()} if they need to access the child 799 * node itself. 800 * 801 * <p>This approach allows directories to be implemented lazily with respect 802 * to child nodes, while retaining efficiency when child nodes are accessed 803 * (since any incomplete nodes will be created and placed in the node cache 804 * when the parent was first returned to the user). 805 */ 806 private static final class Directory extends Node { 807 // Monotonic reference, will be set to the unmodifiable child list exactly once. 808 private List<Node> children = null; 809 810 private Directory(String name, BasicFileAttributes fileAttrs) { 811 super(name, fileAttrs); 812 } 813 814 @Override 815 boolean isCompleted() { 816 return children != null; 817 } 818 819 @Override 820 public boolean isDirectory() { 821 return true; 822 } 823 824 @Override 825 public Stream<String> getChildNames() { 826 if (children != null) { 827 return children.stream().map(Node::getName); 828 } 829 throw new IllegalStateException("Cannot get child nodes of an incomplete directory: " + getName()); 830 } 831 832 private void setChildren(List<Node> children) { 833 assert this.children == null : this + ": Cannot set child nodes twice!"; 834 this.children = Collections.unmodifiableList(children); 835 } 836 } 837 /** 838 * Resource node (e.g. a ".class" entry, or any other data resource). 839 * 840 * <p>Resources are leaf nodes referencing an underlying image location. They 841 * are lightweight, and do not cache their contents. 842 * 843 * <p>Unlike directories (where the node name matches the jimage path for the 844 * corresponding {@code ImageLocation}), resource node names are NOT the same 845 * as the corresponding jimage path. The difference is that node names for 846 * resources are prefixed with "/modules", which is missing from the 847 * equivalent jimage path. 848 */ 849 private static class Resource extends Node { 850 private final ImageLocation loc; 851 852 private Resource(String name, ImageLocation loc, BasicFileAttributes fileAttrs) { 853 super(name, fileAttrs); 854 this.loc = loc; 855 } 856 857 @Override 858 ImageLocation getLocation() { 859 return loc; 860 } 861 862 @Override 863 public boolean isResource() { 864 return true; 865 } 866 867 @Override 868 public long size() { 869 return loc.getUncompressedSize(); 870 } 871 872 @Override 873 public long compressedSize() { 874 return loc.getCompressedSize(); 875 } 876 877 @Override 878 public String extension() { 879 return loc.getExtension(); 880 } 881 } 882 883 /** 884 * Link node (a symbolic link to a top-level modules directory). 885 * 886 * <p>Link nodes resolve their target by invoking a given supplier, and do 887 * not cache the result. Since nodes are cached by the {@code ImageReader}, 888 * this means that only the first call to {@link #resolveLink(boolean)} 889 * could do any significant work. 890 */ 891 private static class LinkNode extends Node { 892 private final Supplier<Node> link; 893 894 private LinkNode(String name, Supplier<Node> link, BasicFileAttributes fileAttrs) { 895 super(name, fileAttrs); 896 this.link = link; 897 } 898 899 @Override 900 public Node resolveLink(boolean recursive) { 901 // No need to use or propagate the recursive flag, since the target 902 // cannot possibly be a link node (links only point to directories). 903 return link.get(); 904 } 905 906 @Override 907 public boolean isLink() { 908 return true; 909 } 910 } 911 }