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");
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 * Returns the content of a resource node in a newly allocated byte buffer.
194 */
195 public ByteBuffer getResourceBuffer(Node node) {
196 requireOpen();
197 if (!node.isResource()) {
198 throw new IllegalArgumentException("Not a resource node: " + node);
199 }
200 return reader.getResourceBuffer(node.getLocation());
201 }
202
203 private static final class SharedImageReader extends BasicImageReader {
204 private static final Map<Path, SharedImageReader> OPEN_FILES = new HashMap<>();
205 private static final String MODULES_ROOT = "/modules";
206 private static final String PACKAGES_ROOT = "/packages";
207 // There are >30,000 nodes in a complete jimage tree, and even relatively
208 // common tasks (e.g. starting up javac) load somewhere in the region of
209 // 1000 classes. Thus, an initial capacity of 2000 is a reasonable guess.
210 private static final int INITIAL_NODE_CACHE_CAPACITY = 2000;
211
212 // List of openers for this shared image.
213 private final Set<ImageReader> openers = new HashSet<>();
214
215 // Attributes of the jimage file. The jimage file does not contain
216 // attributes for the individual resources (yet). We use attributes
217 // of the jimage file itself (creation, modification, access times).
218 private final BasicFileAttributes imageFileAttributes;
219
220 // Cache of all user visible nodes, guarded by synchronizing 'this' instance.
221 private final Map<String, Node> nodes;
222 // Used to classify ImageLocation instances without string comparison.
223 private final int modulesStringOffset;
224 private final int packagesStringOffset;
225
226 private SharedImageReader(Path imagePath, ByteOrder byteOrder) throws IOException {
227 super(imagePath, byteOrder);
228 this.imageFileAttributes = Files.readAttributes(imagePath, BasicFileAttributes.class);
229 this.nodes = new HashMap<>(INITIAL_NODE_CACHE_CAPACITY);
230 // Pick stable jimage names from which to extract string offsets (we cannot
231 // use "/modules" or "/packages", since those have a module offset of zero).
232 this.modulesStringOffset = getModuleOffset("/modules/java.base");
233 this.packagesStringOffset = getModuleOffset("/packages/java.lang");
234
235 // Node creation is very lazy, so we can just make the top-level directories
236 // now without the risk of triggering the building of lots of other nodes.
237 Directory packages = newDirectory(PACKAGES_ROOT);
238 nodes.put(packages.getName(), packages);
239 Directory modules = newDirectory(MODULES_ROOT);
240 nodes.put(modules.getName(), modules);
241
242 Directory root = newDirectory("/");
243 root.setChildren(Arrays.asList(packages, modules));
244 nodes.put(root.getName(), root);
245 }
246
247 /**
248 * Returns the offset of the string denoting the leading "module" segment in
249 * the given path (e.g. {@code <module>/<path>}). We can't just pass in the
250 * {@code /<module>} string here because that has a module offset of zero.
251 */
252 private int getModuleOffset(String path) {
253 ImageLocation location = findLocation(path);
254 assert location != null : "Cannot find expected jimage location: " + path;
255 int offset = location.getModuleOffset();
256 assert offset != 0 : "Invalid module offset for jimage location: " + path;
257 return offset;
258 }
259
260 private static ImageReader open(Path imagePath, ByteOrder byteOrder) throws IOException {
261 Objects.requireNonNull(imagePath);
262 Objects.requireNonNull(byteOrder);
263
264 synchronized (OPEN_FILES) {
265 SharedImageReader reader = OPEN_FILES.get(imagePath);
266
267 if (reader == null) {
268 // Will fail with an IOException if wrong byteOrder.
269 reader = new SharedImageReader(imagePath, byteOrder);
270 OPEN_FILES.put(imagePath, reader);
271 } else if (reader.getByteOrder() != byteOrder) {
272 throw new IOException("\"" + reader.getName() + "\" is not an image file");
273 }
274
275 ImageReader image = new ImageReader(reader);
276 reader.openers.add(image);
277
278 return image;
279 }
280 }
281
282 public void close(ImageReader image) throws IOException {
283 Objects.requireNonNull(image);
284
285 synchronized (OPEN_FILES) {
286 if (!openers.remove(image)) {
287 throw new IOException("image file already closed");
288 }
289
290 if (openers.isEmpty()) {
291 close();
292 nodes.clear();
293
294 if (!OPEN_FILES.remove(this.getImagePath(), this)) {
295 throw new IOException("image file not found in open list");
296 }
297 }
298 }
299 }
300
301 /**
302 * Returns a node with the given name, or null if no resource or directory of
303 * that name exists.
304 *
305 * <p>Note that there is no reentrant calling back to this method from within
306 * the node handling code.
307 *
308 * @param name an absolute, {@code /}-separated path string, prefixed with either
309 * "/modules" or "/packages".
310 */
311 synchronized Node findNode(String name) {
312 Node node = nodes.get(name);
313 if (node == null) {
314 // We cannot get the root paths ("/modules" or "/packages") here
315 // because those nodes are already in the nodes cache.
316 if (name.startsWith(MODULES_ROOT + "/")) {
317 // This may perform two lookups, one for a directory (in
318 // "/modules/...") and one for a non-prefixed resource
319 // (with "/modules" removed).
320 node = buildModulesNode(name);
321 } else if (name.startsWith(PACKAGES_ROOT + "/")) {
322 node = buildPackagesNode(name);
323 }
324 if (node != null) {
325 nodes.put(node.getName(), node);
326 }
327 } else if (!node.isCompleted()) {
328 // Only directories can be incomplete.
329 assert node instanceof Directory : "Invalid incomplete node: " + node;
330 completeDirectory((Directory) node);
331 }
332 assert node == null || node.isCompleted() : "Incomplete node: " + node;
333 return node;
334 }
335
336 /**
337 * Returns a resource node in the given module, or null if no resource of
338 * that name exists.
339 *
340 * <p>Note that there is no reentrant calling back to this method from within
341 * the node handling code.
342 */
343 Node findResourceNode(String moduleName, String resourcePath) {
344 // Unlike findNode(), this method makes only one lookup in the
345 // underlying jimage, but can only reliably return resource nodes.
346 if (moduleName.indexOf('/') >= 0) {
347 throw new IllegalArgumentException("invalid module name: " + moduleName);
348 }
349 String nodeName = MODULES_ROOT + "/" + moduleName + "/" + resourcePath;
350 // Synchronize as tightly as possible to reduce locking contention.
351 synchronized (this) {
352 Node node = nodes.get(nodeName);
353 if (node == null) {
354 ImageLocation loc = findLocation(moduleName, resourcePath);
355 if (loc != null && isResource(loc)) {
356 node = newResource(nodeName, loc);
357 nodes.put(node.getName(), node);
358 }
359 return node;
360 } else {
361 return node.isResource() ? node : null;
362 }
363 }
364 }
365
366 /**
367 * Returns whether a resource exists in the given module.
368 *
369 * <p>This method is expected to be called frequently for resources
370 * which do not exist in the given module (e.g. as part of classpath
371 * search). As such, it skips checking the nodes cache and only checks
372 * for an entry in the jimage file, as this is faster if the resource
373 * is not present. This also means it doesn't need synchronization.
374 */
375 boolean containsResource(String moduleName, String resourcePath) {
376 if (moduleName.indexOf('/') >= 0) {
377 throw new IllegalArgumentException("invalid module name: " + moduleName);
378 }
379 // If the given module name is 'modules', then 'isResource()'
380 // returns false to prevent false positives.
381 ImageLocation loc = findLocation(moduleName, resourcePath);
382 return loc != null && isResource(loc);
383 }
384
385 /**
386 * Builds a node in the "/modules/..." namespace.
387 *
388 * <p>Called by {@link #findNode(String)} if a {@code /modules/...} node
389 * is not present in the cache.
390 */
391 private Node buildModulesNode(String name) {
392 assert name.startsWith(MODULES_ROOT + "/") : "Invalid module node name: " + name;
393 // Returns null for non-directory resources, since the jimage name does not
394 // start with "/modules" (e.g. "/java.base/java/lang/Object.class").
395 ImageLocation loc = findLocation(name);
396 if (loc != null) {
397 assert name.equals(loc.getFullName()) : "Mismatched location for directory: " + name;
398 assert isModulesSubdirectory(loc) : "Invalid modules directory: " + name;
399 return completeModuleDirectory(newDirectory(name), loc);
400 }
401 // Now try the non-prefixed resource name, but be careful to avoid false
402 // positives for names like "/modules/modules/xxx" which could return a
403 // location of a directory entry.
404 loc = findLocation(name.substring(MODULES_ROOT.length()));
405 return loc != null && isResource(loc) ? newResource(name, loc) : null;
406 }
407
408 /**
409 * Builds a node in the "/packages/..." namespace.
410 *
411 * <p>Called by {@link #findNode(String)} if a {@code /packages/...} node
412 * is not present in the cache.
413 */
414 private Node buildPackagesNode(String name) {
415 // There are only locations for the root "/packages" or "/packages/xxx"
416 // directories, but not the symbolic links below them (the links can be
417 // entirely derived from the name information in the parent directory).
418 // However, unlike resources this means that we do not have a constant
419 // time lookup for link nodes when creating them.
420 int packageStart = PACKAGES_ROOT.length() + 1;
421 int packageEnd = name.indexOf('/', packageStart);
422 if (packageEnd == -1) {
423 ImageLocation loc = findLocation(name);
424 return loc != null ? completePackageDirectory(newDirectory(name), loc) : null;
425 } else {
426 // We cannot assume that the parent directory exists for a link node, since
427 // the given name is untrusted and could reference a non-existent link.
428 // However, if the parent directory is present, we can conclude that the
429 // given name was not a valid link (or else it would already be cached).
430 String dirName = name.substring(0, packageEnd);
431 if (!nodes.containsKey(dirName)) {
432 ImageLocation loc = findLocation(dirName);
433 // If the parent location doesn't exist, the link node cannot exist.
434 if (loc != null) {
435 nodes.put(dirName, completePackageDirectory(newDirectory(dirName), loc));
436 // When the parent is created its child nodes are created and cached,
437 // but this can still return null if given name wasn't a valid link.
438 return nodes.get(name);
439 }
440 }
441 }
442 return null;
443 }
444
445 /** Completes a directory by ensuring its child list is populated correctly. */
446 private void completeDirectory(Directory dir) {
447 String name = dir.getName();
448 // Since the node exists, we can assert that its name starts with
449 // either "/modules" or "/packages", making differentiation easy.
450 // It also means that the name is valid, so it must yield a location.
451 assert name.startsWith(MODULES_ROOT) || name.startsWith(PACKAGES_ROOT);
452 ImageLocation loc = findLocation(name);
453 assert loc != null && name.equals(loc.getFullName()) : "Invalid location for name: " + name;
454 // We cannot use 'isXxxSubdirectory()' methods here since we could
455 // be given a top-level directory (for which that test doesn't work).
456 // The string MUST start "/modules" or "/packages" here.
457 if (name.charAt(1) == 'm') {
458 completeModuleDirectory(dir, loc);
459 } else {
460 completePackageDirectory(dir, loc);
461 }
462 assert dir.isCompleted() : "Directory must be complete by now: " + dir;
463 }
464
465 /**
466 * Completes a modules directory by setting the list of child nodes.
467 *
468 * <p>The given directory can be the top level {@code /modules} directory,
469 * so it is NOT safe to use {@code isModulesSubdirectory(loc)} here.
470 */
471 private Directory completeModuleDirectory(Directory dir, ImageLocation loc) {
472 assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir;
473 List<Node> children = createChildNodes(loc, childLoc -> {
474 if (isModulesSubdirectory(childLoc)) {
475 return nodes.computeIfAbsent(childLoc.getFullName(), this::newDirectory);
476 } else {
477 // Add "/modules" prefix to image location paths to get node names.
478 String resourceName = childLoc.getFullName(true);
479 return nodes.computeIfAbsent(resourceName, n -> newResource(n, childLoc));
480 }
481 });
482 dir.setChildren(children);
483 return dir;
484 }
485
486 /**
487 * Completes a package directory by setting the list of child nodes.
488 *
489 * <p>The given directory can be the top level {@code /packages} directory,
490 * so it is NOT safe to use {@code isPackagesSubdirectory(loc)} here.
491 */
492 private Directory completePackageDirectory(Directory dir, ImageLocation loc) {
493 assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir;
494 // The only directories in the "/packages" namespace are "/packages" or
495 // "/packages/<package>". However, unlike "/modules" directories, the
496 // location offsets mean different things.
497 List<Node> children;
498 if (dir.getName().equals(PACKAGES_ROOT)) {
499 // Top-level directory just contains a list of subdirectories.
500 children = createChildNodes(loc, c -> nodes.computeIfAbsent(c.getFullName(), this::newDirectory));
501 } else {
502 // A package directory's content is array of offset PAIRS in the
503 // Strings table, but we only need the 2nd value of each pair.
504 IntBuffer intBuffer = getOffsetBuffer(loc);
505 int offsetCount = intBuffer.capacity();
506 assert (offsetCount & 0x1) == 0 : "Offset count must be even: " + offsetCount;
507 children = new ArrayList<>(offsetCount / 2);
508 // Iterate the 2nd offset in each pair (odd indices).
509 for (int i = 1; i < offsetCount; i += 2) {
510 String moduleName = getString(intBuffer.get(i));
511 children.add(nodes.computeIfAbsent(
512 dir.getName() + "/" + moduleName,
513 n -> newLinkNode(n, MODULES_ROOT + "/" + moduleName)));
514 }
515 }
516 // This only happens once and "completes" the directory.
517 dir.setChildren(children);
518 return dir;
519 }
520
521 /**
522 * Creates the list of child nodes for a {@code Directory} based on a given
523 *
524 * <p>Note: This cannot be used for package subdirectories as they have
525 * child offsets stored differently to other directories.
526 */
527 private List<Node> createChildNodes(ImageLocation loc, Function<ImageLocation, Node> newChildFn) {
528 IntBuffer offsets = getOffsetBuffer(loc);
529 int childCount = offsets.capacity();
530 List<Node> children = new ArrayList<>(childCount);
531 for (int i = 0; i < childCount; i++) {
532 children.add(newChildFn.apply(getLocation(offsets.get(i))));
533 }
534 return children;
535 }
536
537 /** Helper to extract the integer offset buffer from a directory location. */
538 private IntBuffer getOffsetBuffer(ImageLocation dir) {
539 assert !isResource(dir) : "Not a directory: " + dir.getFullName();
540 byte[] offsets = getResource(dir);
541 ByteBuffer buffer = ByteBuffer.wrap(offsets);
542 buffer.order(getByteOrder());
543 return buffer.asIntBuffer();
544 }
545
546 /**
547 * Efficiently determines if an image location is a resource.
548 *
549 * <p>A resource must have a valid module associated with it, so its
550 * module offset must be non-zero, and not equal to the offsets for
551 * "/modules/..." or "/packages/..." entries.
552 */
553 private boolean isResource(ImageLocation loc) {
554 int moduleOffset = loc.getModuleOffset();
555 return moduleOffset != 0
556 && moduleOffset != modulesStringOffset
557 && moduleOffset != packagesStringOffset;
558 }
559
560 /**
561 * Determines if an image location is a directory in the {@code /modules}
562 * namespace (if so, the location name is the node name).
563 *
564 * <p>In jimage, every {@code ImageLocation} under {@code /modules/} is a
565 * directory and has the same value for {@code getModule()}, and {@code
566 * getModuleOffset()}.
567 */
568 private boolean isModulesSubdirectory(ImageLocation loc) {
569 return loc.getModuleOffset() == modulesStringOffset;
570 }
571
572 /**
573 * Creates an "incomplete" directory node with no child nodes set.
574 * Directories need to be "completed" before they are returned by
575 * {@link #findNode(String)}.
576 */
577 private Directory newDirectory(String name) {
578 return new Directory(name, imageFileAttributes);
579 }
580
581 /**
582 * Creates a new resource from an image location. This is the only case
583 * where the image location name does not match the requested node name.
584 * In image files, resource locations are NOT prefixed by {@code /modules}.
585 */
586 private Resource newResource(String name, ImageLocation loc) {
587 assert name.equals(loc.getFullName(true)) : "Mismatched location for resource: " + name;
588 return new Resource(name, loc, imageFileAttributes);
589 }
590
591 /**
592 * Creates a new link node pointing at the given target name.
593 *
594 * <p>Note that target node is resolved each time {@code resolve()} is called,
595 * so if a link node is retained after its reader is closed, it will fail.
596 */
597 private LinkNode newLinkNode(String name, String targetName) {
598 return new LinkNode(name, () -> findNode(targetName), imageFileAttributes);
599 }
600
601 /** Returns the content of a resource node. */
602 private byte[] getResource(Node node) throws IOException {
603 // We could have been given a non-resource node here.
604 if (node.isResource()) {
605 return super.getResource(node.getLocation());
606 }
607 throw new IOException("Not a resource: " + node);
799 }
800
801 @Override
802 boolean isCompleted() {
803 return children != null;
804 }
805
806 @Override
807 public boolean isDirectory() {
808 return true;
809 }
810
811 @Override
812 public Stream<String> getChildNames() {
813 if (children != null) {
814 return children.stream().map(Node::getName);
815 }
816 throw new IllegalStateException("Cannot get child nodes of an incomplete directory: " + getName());
817 }
818
819 private void setChildren(List<Node> children) {
820 assert this.children == null : this + ": Cannot set child nodes twice!";
821 this.children = Collections.unmodifiableList(children);
822 }
823 }
824
825 /**
826 * Resource node (e.g. a ".class" entry, or any other data resource).
827 *
828 * <p>Resources are leaf nodes referencing an underlying image location. They
829 * are lightweight, and do not cache their contents.
830 *
831 * <p>Unlike directories (where the node name matches the jimage path for the
832 * corresponding {@code ImageLocation}), resource node names are NOT the same
833 * as the corresponding jimage path. The difference is that node names for
834 * resources are prefixed with "/modules", which is missing from the
835 * equivalent jimage path.
836 */
837 private static class Resource extends Node {
838 private final ImageLocation loc;
839
|
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 jdk.internal.jimage.ImageLocation.LocationType;
28
29 import java.io.IOException;
30 import java.nio.ByteBuffer;
31 import java.nio.ByteOrder;
32 import java.nio.IntBuffer;
33 import java.nio.file.Files;
34 import java.nio.file.Path;
35 import java.nio.file.attribute.BasicFileAttributes;
36 import java.util.ArrayList;
37 import java.util.Arrays;
38 import java.util.Collections;
39 import java.util.Comparator;
40 import java.util.HashMap;
41 import java.util.HashSet;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Objects;
45 import java.util.Set;
46 import java.util.TreeMap;
47 import java.util.function.Function;
48 import java.util.function.Supplier;
49 import java.util.stream.Stream;
50
51 import static jdk.internal.jimage.ImageLocation.LocationType.MODULES_DIR;
52 import static jdk.internal.jimage.ImageLocation.LocationType.MODULES_ROOT;
53 import static jdk.internal.jimage.ImageLocation.LocationType.PACKAGES_DIR;
54 import static jdk.internal.jimage.ImageLocation.LocationType.RESOURCE;
55 import static jdk.internal.jimage.ImageLocation.MODULES_PREFIX;
56 import static jdk.internal.jimage.ImageLocation.PACKAGES_PREFIX;
57 import static jdk.internal.jimage.ImageLocation.PREVIEW_INFIX;
58
59 /**
60 * A view over the entries of a jimage file with a unified namespace suitable
61 * for file system use. The jimage entries (resources, module and package
62 * information) are mapped into a unified hierarchy of named nodes, which serve
63 * as the underlying structure for {@code JrtFileSystem} and other utilities.
64 *
65 * <p>Entries in jimage are expressed as one of three {@link Node} types;
66 * resource nodes, directory nodes and link nodes.
67 *
68 * <p>When remapping jimage entries, jimage location names (e.g. {@code
69 * "/java.base/java/lang/Integer.class"}) are prefixed with {@code "/modules"}
70 * to form the names of resource nodes. This aligns with the naming of module
71 * entries in jimage (e.g. "/modules/java.base/java/lang"), which appear as
72 * directory nodes in {@code ImageReader}.
73 *
74 * <p>Package entries (e.g. {@code "/packages/java.lang"} appear as directory
75 * nodes containing link nodes, which resolve back to the root directory of the
76 * module in which that package exists (e.g. {@code "/modules/java.base"}).
77 * Unlike other nodes, the jimage file does not contain explicit entries for
78 * link nodes, and their existence is derived only from the contents of the
79 * parent directory.
80 *
81 * <p>While similar to {@code BasicImageReader}, this class is not a conceptual
82 * subtype of it, and deliberately hides types such as {@code ImageLocation} to
83 * give a focused API based only on nodes.
84 *
85 * @implNote This class needs to maintain JDK 8 source compatibility.
86 *
87 * It is used internally in the JDK to implement jimage/jrtfs access,
88 * but also compiled and delivered as part of the jrtfs.jar to support access
89 * to the jimage file provided by the shipped JDK by tools running on JDK 8.
90 */
91 public final class ImageReader implements AutoCloseable {
92
93 // For resource paths, there's no leading '/'.
94 private static final String PREVIEW_RESOURCE_PREFIX = PREVIEW_INFIX.substring(1);
95
96 private final SharedImageReader reader;
97
98 private volatile boolean closed;
99
100 private ImageReader(SharedImageReader reader) {
101 this.reader = reader;
102 }
103
104 /**
105 * Opens an image reader for a jimage file at the specified path.
106 *
107 * @param imagePath file system path of the jimage file.
108 * @param mode whether to return preview resources.
109 */
110 public static ImageReader open(Path imagePath, PreviewMode mode) throws IOException {
111 return open(imagePath, ByteOrder.nativeOrder(), mode);
112 }
113
114 /**
115 * Opens an image reader for a jimage file at the specified path.
116 *
117 * @param imagePath file system path of the jimage file.
118 * @param byteOrder the byte-order to be used when reading the jimage file.
119 * @param mode controls whether preview resources are visible.
120 */
121 public static ImageReader open(Path imagePath, ByteOrder byteOrder, PreviewMode mode)
122 throws IOException {
123 Objects.requireNonNull(imagePath);
124 Objects.requireNonNull(byteOrder);
125 return SharedImageReader.open(imagePath, byteOrder, mode.isPreviewModeEnabled());
126 }
127
128 @Override
129 public void close() throws IOException {
130 if (closed) {
131 throw new IOException("image file already closed");
132 }
133 reader.close(this);
134 closed = true;
135 }
136
137 private void ensureOpen() throws IOException {
138 if (closed) {
139 throw new IOException("image file closed");
140 }
141 }
142
143 private void requireOpen() {
144 if (closed) {
145 throw new IllegalStateException("image file closed");
204 *
205 * @throws IOException if the content cannot be returned (including if the
206 * given node is not a resource node).
207 */
208 public byte[] getResource(Node node) throws IOException {
209 ensureOpen();
210 return reader.getResource(node);
211 }
212
213 /**
214 * Returns the content of a resource node in a newly allocated byte buffer.
215 */
216 public ByteBuffer getResourceBuffer(Node node) {
217 requireOpen();
218 if (!node.isResource()) {
219 throw new IllegalArgumentException("Not a resource node: " + node);
220 }
221 return reader.getResourceBuffer(node.getLocation());
222 }
223
224 // Package protected for use only by SystemImageReader.
225 ResourceEntries getResourceEntries() {
226 return reader.getResourceEntries();
227 }
228
229 private static final class SharedImageReader extends BasicImageReader {
230 // There are >30,000 nodes in a complete jimage tree, and even relatively
231 // common tasks (e.g. starting up javac) load somewhere in the region of
232 // 1000 classes. Thus, an initial capacity of 2000 is a reasonable guess.
233 private static final int INITIAL_NODE_CACHE_CAPACITY = 2000;
234
235 static final class ReaderKey {
236 private final Path imagePath;
237 private final boolean previewMode;
238
239 public ReaderKey(Path imagePath, boolean previewMode) {
240 this.imagePath = imagePath;
241 this.previewMode = previewMode;
242 }
243
244 @Override
245 public boolean equals(Object obj) {
246 // No pattern variables here (Java 8 compatible source).
247 if (obj instanceof ReaderKey) {
248 ReaderKey other = (ReaderKey) obj;
249 return this.imagePath.equals(other.imagePath) && this.previewMode == other.previewMode;
250 }
251 return false;
252 }
253
254 @Override
255 public int hashCode() {
256 return imagePath.hashCode() ^ Boolean.hashCode(previewMode);
257 }
258 }
259
260 private static final Map<ReaderKey, SharedImageReader> OPEN_FILES = new HashMap<>();
261
262 // List of openers for this shared image.
263 private final Set<ImageReader> openers = new HashSet<>();
264
265 // Attributes of the jimage file. The jimage file does not contain
266 // attributes for the individual resources (yet). We use attributes
267 // of the jimage file itself (creation, modification, access times).
268 private final BasicFileAttributes imageFileAttributes;
269
270 // Cache of all user visible nodes, guarded by synchronizing 'this' instance.
271 private final Map<String, Node> nodes;
272
273 // Preview mode support.
274 private final boolean previewMode;
275 // A relativized mapping from non-preview name to directories containing
276 // preview-only nodes. This is used to add preview-only content to
277 // directories as they are completed.
278 private final HashMap<String, Directory> previewDirectoriesToMerge;
279
280 private SharedImageReader(Path imagePath, ByteOrder byteOrder, boolean previewMode) throws IOException {
281 super(imagePath, byteOrder);
282 this.imageFileAttributes = Files.readAttributes(imagePath, BasicFileAttributes.class);
283 this.nodes = new HashMap<>(INITIAL_NODE_CACHE_CAPACITY);
284 this.previewMode = previewMode;
285
286 // Node creation is very lazy, so we can just make the top-level directories
287 // now without the risk of triggering the building of lots of other nodes.
288 Directory packages = ensureCached(newDirectory(PACKAGES_PREFIX));
289 Directory modules = ensureCached(newDirectory(MODULES_PREFIX));
290
291 Directory root = newDirectory("/");
292 root.setChildren(Arrays.asList(packages, modules));
293 ensureCached(root);
294
295 // By scanning the /packages directory information early we can determine
296 // which module/package pairs have preview resources, and build the (small)
297 // set of preview nodes early. This also ensures that preview-only entries
298 // in the /packages directory are not present in non-preview mode.
299 this.previewDirectoriesToMerge = previewMode ? new HashMap<>() : null;
300 packages.setChildren(processPackagesDirectory(previewMode));
301 }
302
303 /**
304 * Process {@code "/packages/xxx"} entries to build the child nodes for the
305 * root {@code "/packages"} node. Preview-only entries will be skipped if
306 * {@code previewMode == false}.
307 *
308 * <p>If {@code previewMode == true}, this method also populates the {@link
309 * #previewDirectoriesToMerge} map with any preview-only nodes, to be merged
310 * into directories as they are completed. It also caches preview resources
311 * and preview-only directories for direct lookup.
312 */
313 private ArrayList<Node> processPackagesDirectory(boolean previewMode) {
314 ImageLocation pkgRoot = findLocation(PACKAGES_PREFIX);
315 assert pkgRoot != null : "Invalid jimage file";
316 IntBuffer offsets = getOffsetBuffer(pkgRoot);
317 ArrayList<Node> pkgDirs = new ArrayList<>(offsets.capacity());
318 // Package path to module map, sorted in reverse order so that
319 // longer child paths get processed first.
320 Map<String, List<String>> previewPackagesToModules =
321 new TreeMap<>(Comparator.reverseOrder());
322 for (int i = 0; i < offsets.capacity(); i++) {
323 ImageLocation pkgDir = getLocation(offsets.get(i));
324 int flags = pkgDir.getFlags();
325 // A package subdirectory is "preview only" if all the modules
326 // it references have that package marked as preview only.
327 // Skipping these entries avoids empty package subdirectories.
328 if (previewMode || !ImageLocation.isPreviewOnly(flags)) {
329 pkgDirs.add(ensureCached(newDirectory(pkgDir.getFullName())));
330 }
331 if (previewMode && ImageLocation.hasPreviewVersion(flags)) {
332 // Only do this in preview mode for the small set of packages with
333 // preview versions (the number of preview entries should be small).
334 List<String> moduleNames = new ArrayList<>();
335 ModuleReference.readNameOffsets(getOffsetBuffer(pkgDir), /*normal*/ false, /*preview*/ true)
336 .forEachRemaining(n -> moduleNames.add(getString(n)));
337 previewPackagesToModules.put(pkgDir.getBase().replace('.', '/'), moduleNames);
338 }
339 }
340 // Reverse sorted map means child directories are processed first.
341 previewPackagesToModules.forEach((pkgPath, modules) ->
342 modules.forEach(modName -> processPreviewDir(MODULES_PREFIX + "/" + modName, pkgPath)));
343 // We might have skipped some preview-only package entries.
344 pkgDirs.trimToSize();
345 return pkgDirs;
346 }
347
348 void processPreviewDir(String namePrefix, String pkgPath) {
349 String previewDirName = namePrefix + PREVIEW_INFIX + "/" + pkgPath;
350 ImageLocation previewLoc = findLocation(previewDirName);
351 assert previewLoc != null : "Missing preview directory location: " + previewDirName;
352 String nonPreviewDirName = namePrefix + "/" + pkgPath;
353 List<Node> previewOnlyChildren = createChildNodes(previewLoc, 0, childLoc -> {
354 String baseName = getBaseName(childLoc);
355 String nonPreviewChildName = nonPreviewDirName + "/" + baseName;
356 boolean isPreviewOnly = ImageLocation.isPreviewOnly(childLoc.getFlags());
357 LocationType type = childLoc.getType();
358 if (type == RESOURCE) {
359 // Preview resources are cached to override non-preview versions.
360 Node childNode = ensureCached(newResource(nonPreviewChildName, childLoc));
361 return isPreviewOnly ? childNode : null;
362 } else {
363 // Child directories are not cached here (they are either cached
364 // already or have been added to previewDirectoriesToMerge).
365 assert type == MODULES_DIR : "Invalid location type: " + childLoc;
366 Node childNode = nodes.get(nonPreviewChildName);
367 assert isPreviewOnly == (childNode != null) :
368 "Inconsistent child node: " + nonPreviewChildName;
369 return childNode;
370 }
371 });
372 Directory previewDir = newDirectory(nonPreviewDirName);
373 previewDir.setChildren(previewOnlyChildren);
374 if (ImageLocation.isPreviewOnly(previewLoc.getFlags())) {
375 // If we are preview-only, our children are also preview-only, so
376 // this directory is a complete hierarchy and should be cached.
377 assert !previewOnlyChildren.isEmpty() : "Invalid empty preview-only directory: " + nonPreviewDirName;
378 ensureCached(previewDir);
379 } else if (!previewOnlyChildren.isEmpty()) {
380 // A partial directory containing extra preview-only nodes
381 // to be merged when the non-preview directory is completed.
382 previewDirectoriesToMerge.put(nonPreviewDirName, previewDir);
383 }
384 }
385
386 // Adds a node to the cache, ensuring that no matching entry already existed.
387 private <T extends Node> T ensureCached(T node) {
388 Node existingNode = nodes.put(node.getName(), node);
389 assert existingNode == null : "Unexpected node already cached for: " + node;
390 return node;
391 }
392
393 private static ImageReader open(Path imagePath, ByteOrder byteOrder, boolean previewMode) throws IOException {
394 Objects.requireNonNull(imagePath);
395 Objects.requireNonNull(byteOrder);
396
397 synchronized (OPEN_FILES) {
398 ReaderKey key = new ReaderKey(imagePath, previewMode);
399 SharedImageReader reader = OPEN_FILES.get(key);
400
401 if (reader == null) {
402 // Will fail with an IOException if wrong byteOrder.
403 reader = new SharedImageReader(imagePath, byteOrder, previewMode);
404 OPEN_FILES.put(key, reader);
405 } else if (reader.getByteOrder() != byteOrder) {
406 throw new IOException("\"" + reader.getName() + "\" is not an image file");
407 }
408
409 ImageReader image = new ImageReader(reader);
410 reader.openers.add(image);
411
412 return image;
413 }
414 }
415
416 public void close(ImageReader image) throws IOException {
417 Objects.requireNonNull(image);
418
419 synchronized (OPEN_FILES) {
420 if (!openers.remove(image)) {
421 throw new IOException("image file already closed");
422 }
423
424 if (openers.isEmpty()) {
425 close();
426 nodes.clear();
427
428 if (!OPEN_FILES.remove(new ReaderKey(getImagePath(), previewMode), this)) {
429 throw new IOException("image file not found in open list");
430 }
431 }
432 }
433 }
434
435 /**
436 * Returns a node with the given name, or null if no resource or directory of
437 * that name exists.
438 *
439 * <p>Note that there is no reentrant calling back to this method from within
440 * the node handling code.
441 *
442 * @param name an absolute, {@code /}-separated path string, prefixed with either
443 * "/modules" or "/packages".
444 */
445 synchronized Node findNode(String name) {
446 // Root directories "/", "/modules" and "/packages", as well
447 // as all "/packages/xxx" subdirectories are already cached.
448 Node node = nodes.get(name);
449 if (node == null) {
450 if (name.startsWith(MODULES_PREFIX + "/")) {
451 node = buildAndCacheModulesNode(name);
452 } else if (name.startsWith(PACKAGES_PREFIX + "/")) {
453 node = buildAndCacheLinkNode(name);
454 }
455 } else if (!node.isCompleted()) {
456 // Only directories can be incomplete.
457 assert node instanceof Directory : "Invalid incomplete node: " + node;
458 completeDirectory((Directory) node);
459 }
460 assert node == null || node.isCompleted() : "Incomplete node: " + node;
461 return node;
462 }
463
464 /**
465 * Returns a resource node in the given module, or null if no resource of
466 * that name exists.
467 *
468 * <p>Note that there is no reentrant calling back to this method from within
469 * the node handling code.
470 */
471 Node findResourceNode(String moduleName, String resourcePath) {
472 // Unlike findNode(), this method makes only one lookup in the
473 // underlying jimage, but can only reliably return resource nodes.
474 if (moduleName.indexOf('/') >= 0) {
475 throw new IllegalArgumentException("invalid module name: " + moduleName);
476 }
477 if (resourcePath.startsWith(PREVIEW_RESOURCE_PREFIX)) {
478 return null;
479 }
480 String nodeName = MODULES_PREFIX + "/" + moduleName + "/" + resourcePath;
481 // Synchronize as tightly as possible to reduce locking contention.
482 synchronized (this) {
483 Node node = nodes.get(nodeName);
484 if (node == null) {
485 ImageLocation loc = null;
486 if (previewMode) {
487 // We must test preview location first (if in preview mode).
488 loc = findLocation(moduleName, PREVIEW_RESOURCE_PREFIX + resourcePath);
489 }
490 if (loc == null) {
491 loc = findLocation(moduleName, resourcePath);
492 }
493 if (loc != null && loc.getType() == RESOURCE) {
494 node = newResource(nodeName, loc);
495 nodes.put(node.getName(), node);
496 }
497 return node;
498 } else {
499 return node.isResource() ? node : null;
500 }
501 }
502 }
503
504 /**
505 * Returns whether a resource exists in the given module.
506 *
507 * <p>This method is expected to be called frequently for resources
508 * which do not exist in the given module (e.g. as part of classpath
509 * search). As such, it skips checking the nodes cache if possible, and
510 * only checks for an entry in the jimage file, as this is faster if the
511 * resource is not present. This also means it doesn't need
512 * synchronization most of the time.
513 */
514 boolean containsResource(String moduleName, String resourcePath) {
515 if (moduleName.indexOf('/') >= 0) {
516 throw new IllegalArgumentException("invalid module name: " + moduleName);
517 }
518 if (resourcePath.startsWith(PREVIEW_RESOURCE_PREFIX)) {
519 return false;
520 }
521 // In preview mode, preview-only resources are eagerly added to the
522 // cache, so we must check that first.
523 ImageLocation loc = null;
524 if (previewMode) {
525 String nodeName = MODULES_PREFIX + "/" + moduleName + "/" + resourcePath;
526 // Synchronize as tightly as possible to reduce locking contention.
527 synchronized (this) {
528 Node node = nodes.get(nodeName);
529 if (node != null) {
530 return node.isResource();
531 }
532 }
533 loc = findLocation(moduleName, PREVIEW_RESOURCE_PREFIX + resourcePath);
534 }
535 if (loc == null) {
536 loc = findLocation(moduleName, resourcePath);
537 }
538 return loc != null && loc.getType() == RESOURCE;
539 }
540
541 /**
542 * Builds a node in the "/modules/..." namespace.
543 *
544 * <p>Called by {@link #findNode(String)} if a {@code /modules/...} node
545 * is not present in the cache.
546 */
547 private Node buildAndCacheModulesNode(String name) {
548 assert name.startsWith(MODULES_PREFIX + "/") : "Invalid module node name: " + name;
549 if (isPreviewName(name)) {
550 return null;
551 }
552 // Returns null for non-directory resources, since the jimage name does not
553 // start with "/modules" (e.g. "/java.base/java/lang/Object.class").
554 ImageLocation loc = findLocation(name);
555 if (loc != null) {
556 assert name.equals(loc.getFullName()) : "Mismatched location for directory: " + name;
557 assert loc.getType() == MODULES_DIR : "Invalid modules directory: " + name;
558 return ensureCached(completeModuleDirectory(newDirectory(name), loc));
559 }
560 // Now try the non-prefixed resource name, but be careful to avoid false
561 // positives for names like "/modules/modules/xxx" which could return a
562 // location of a directory entry.
563 loc = findLocation(name.substring(MODULES_PREFIX.length()));
564 return loc != null && loc.getType() == RESOURCE
565 ? ensureCached(newResource(name, loc))
566 : null;
567 }
568
569 /**
570 * Returns whether a directory name in the "/modules/" directory could be referencing
571 * the "META-INF" directory".
572 */
573 private boolean isMetaInf(Directory dir) {
574 String name = dir.getName();
575 int pathStart = name.indexOf('/', MODULES_PREFIX.length() + 1);
576 return name.length() == pathStart + "/META-INF".length()
577 && name.endsWith("/META-INF");
578 }
579
580 /**
581 * Returns whether a node name in the "/modules/" directory could be referencing
582 * a preview resource or directory under "META-INF/preview".
583 */
584 private boolean isPreviewName(String name) {
585 int pathStart = name.indexOf('/', MODULES_PREFIX.length() + 1);
586 int previewEnd = pathStart + PREVIEW_INFIX.length();
587 return pathStart > 0
588 && name.regionMatches(pathStart, PREVIEW_INFIX, 0, PREVIEW_INFIX.length())
589 && (name.length() == previewEnd || name.charAt(previewEnd) == '/');
590 }
591
592 private String getBaseName(ImageLocation loc) {
593 // Matches logic in ImageLocation#getFullName() regarding extensions.
594 String trailingParts = loc.getBase()
595 + ((loc.getExtensionOffset() != 0) ? "." + loc.getExtension() : "");
596 return trailingParts.substring(trailingParts.lastIndexOf('/') + 1);
597 }
598
599 /**
600 * Builds a link node of the form "/packages/xxx/yyy".
601 *
602 * <p>Called by {@link #findNode(String)} if a {@code /packages/...}
603 * node is not present in the cache (the name is not trusted).
604 */
605 private Node buildAndCacheLinkNode(String name) {
606 // There are only locations for "/packages" or "/packages/xxx"
607 // directories, but not the symbolic links below them (links are
608 // derived from the name information in the parent directory).
609 int packageStart = PACKAGES_PREFIX.length() + 1;
610 int packageEnd = name.indexOf('/', packageStart);
611 // We already built the 2-level "/packages/xxx" directories,
612 // so if this is a 2-level name, it cannot reference a node.
613 if (packageEnd >= 0) {
614 String dirName = name.substring(0, packageEnd);
615 // If no parent exists here, the name cannot be valid.
616 Directory parent = (Directory) nodes.get(dirName);
617 if (parent != null) {
618 if (!parent.isCompleted()) {
619 // This caches all child links of the parent directory.
620 completePackageSubdirectory(parent, findLocation(dirName));
621 }
622 return nodes.get(name);
623 }
624 }
625 return null;
626 }
627
628 /** Completes a directory by ensuring its child list is populated correctly. */
629 private void completeDirectory(Directory dir) {
630 String name = dir.getName();
631 // Since the node exists, we can assert that its name starts with
632 // either "/modules" or "/packages", making differentiation easy.
633 // It also means that the name is valid, so it must yield a location.
634 assert name.startsWith(MODULES_PREFIX) || name.startsWith(PACKAGES_PREFIX);
635 ImageLocation loc = findLocation(name);
636 assert loc != null && name.equals(loc.getFullName()) : "Invalid location for name: " + name;
637 LocationType type = loc.getType();
638 if (type == MODULES_DIR || type == MODULES_ROOT) {
639 completeModuleDirectory(dir, loc);
640 } else {
641 assert type == PACKAGES_DIR : "Invalid location type: " + loc;
642 completePackageSubdirectory(dir, loc);
643 }
644 assert dir.isCompleted() : "Directory must be complete by now: " + dir;
645 }
646
647 /** Completes a modules directory by setting the list of child nodes. */
648 private Directory completeModuleDirectory(Directory dir, ImageLocation loc) {
649 assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir;
650 List<Node> previewOnlyNodes = getPreviewNodesToMerge(dir);
651 // We hide preview names from direct lookup, but must also prevent
652 // the preview directory from appearing in any META-INF directories.
653 boolean parentIsMetaInfDir = isMetaInf(dir);
654 List<Node> children = createChildNodes(loc, previewOnlyNodes.size(), childLoc -> {
655 LocationType type = childLoc.getType();
656 if (type == MODULES_DIR) {
657 String name = childLoc.getFullName();
658 return parentIsMetaInfDir && name.endsWith("/preview")
659 ? null
660 : nodes.computeIfAbsent(name, this::newDirectory);
661 } else {
662 assert type == RESOURCE : "Invalid location type: " + loc;
663 // Add "/modules" prefix to image location paths to get node names.
664 String resourceName = childLoc.getFullName(true);
665 return nodes.computeIfAbsent(resourceName, n -> newResource(n, childLoc));
666 }
667 });
668 children.addAll(previewOnlyNodes);
669 dir.setChildren(children);
670 return dir;
671 }
672
673 /** Completes a package directory by setting the list of child nodes. */
674 private void completePackageSubdirectory(Directory dir, ImageLocation loc) {
675 assert dir.getName().equals(loc.getFullName()) : "Mismatched location for directory: " + dir;
676 assert !dir.isCompleted() : "Directory already completed: " + dir;
677 assert loc.getType() == PACKAGES_DIR : "Invalid location type: " + loc.getType();
678
679 // In non-preview mode we might skip a very small number of preview-only
680 // entries, but it's not worth "right-sizing" the array for that.
681 IntBuffer offsets = getOffsetBuffer(loc);
682 List<Node> children = new ArrayList<>(offsets.capacity() / 2);
683 ModuleReference.readNameOffsets(offsets, /*normal*/ true, previewMode)
684 .forEachRemaining(n -> {
685 String modName = getString(n);
686 Node link = newLinkNode(dir.getName() + "/" + modName, MODULES_PREFIX + "/" + modName);
687 children.add(ensureCached(link));
688 });
689 // If the parent directory exists, there must be at least one child node.
690 assert !children.isEmpty() : "Invalid empty package directory: " + dir;
691 dir.setChildren(children);
692 }
693
694 /**
695 * Returns the list of child preview nodes to be merged into the given directory.
696 *
697 * <p>Because this is only called once per-directory (since the result is cached
698 * indefinitely) we can remove any entries we find from the cache. If ever the
699 * node cache allowed entries to expire, this would have to be changed so that
700 * directories could be completed more than once.
701 */
702 List<Node> getPreviewNodesToMerge(Directory dir) {
703 if (previewDirectoriesToMerge != null) {
704 Directory mergeDir = previewDirectoriesToMerge.remove(dir.getName());
705 if (mergeDir != null) {
706 return mergeDir.children;
707 }
708 }
709 return Collections.emptyList();
710 }
711
712 /**
713 * Creates the list of child nodes for a modules {@code Directory} from
714 * its parent location.
715 *
716 * <p>The {@code getChildFn} may return existing cached nodes rather
717 * than creating them, and if newly created nodes are to be cached,
718 * it is the job of {@code getChildFn}, or the caller of this method,
719 * to do that.
720 *
721 * @param loc a location relating to a "/modules" directory.
722 * @param extraNodesCount a known number of preview-only child nodes
723 * which will be merged onto the end of the returned list later.
724 * @param getChildFn a function to return a node for each child location
725 * (or null to skip putting anything in the list).
726 * @return the list of the non-null child nodes, returned by
727 * {@code getChildFn}, in the order of the locations entries.
728 */
729 private List<Node> createChildNodes(ImageLocation loc, int extraNodesCount, Function<ImageLocation, Node> getChildFn) {
730 LocationType type = loc.getType();
731 assert type == MODULES_DIR || type == MODULES_ROOT : "Invalid location type: " + loc;
732 IntBuffer offsets = getOffsetBuffer(loc);
733 int childCount = offsets.capacity();
734 List<Node> children = new ArrayList<>(childCount + extraNodesCount);
735 for (int i = 0; i < childCount; i++) {
736 Node childNode = getChildFn.apply(getLocation(offsets.get(i)));
737 if (childNode != null) {
738 children.add(childNode);
739 }
740 }
741 return children;
742 }
743
744 /** Helper to extract the integer offset buffer from a directory location. */
745 private IntBuffer getOffsetBuffer(ImageLocation dir) {
746 assert dir.getType() != RESOURCE : "Not a directory: " + dir.getFullName();
747 byte[] offsets = getResource(dir);
748 ByteBuffer buffer = ByteBuffer.wrap(offsets);
749 buffer.order(getByteOrder());
750 return buffer.asIntBuffer();
751 }
752
753 /**
754 * Creates an "incomplete" directory node with no child nodes set.
755 * Directories need to be "completed" before they are returned by
756 * {@link #findNode(String)}.
757 */
758 private Directory newDirectory(String name) {
759 return new Directory(name, imageFileAttributes);
760 }
761
762 /**
763 * Creates a new resource from an image location. This is the only case
764 * where the image location name does not match the requested node name.
765 * In image files, resource locations are NOT prefixed by {@code /modules}.
766 */
767 private Resource newResource(String name, ImageLocation loc) {
768 return new Resource(name, loc, imageFileAttributes);
769 }
770
771 /**
772 * Creates a new link node pointing at the given target name.
773 *
774 * <p>Note that target node is resolved each time {@code resolve()} is called,
775 * so if a link node is retained after its reader is closed, it will fail.
776 */
777 private LinkNode newLinkNode(String name, String targetName) {
778 return new LinkNode(name, () -> findNode(targetName), imageFileAttributes);
779 }
780
781 /** Returns the content of a resource node. */
782 private byte[] getResource(Node node) throws IOException {
783 // We could have been given a non-resource node here.
784 if (node.isResource()) {
785 return super.getResource(node.getLocation());
786 }
787 throw new IOException("Not a resource: " + node);
979 }
980
981 @Override
982 boolean isCompleted() {
983 return children != null;
984 }
985
986 @Override
987 public boolean isDirectory() {
988 return true;
989 }
990
991 @Override
992 public Stream<String> getChildNames() {
993 if (children != null) {
994 return children.stream().map(Node::getName);
995 }
996 throw new IllegalStateException("Cannot get child nodes of an incomplete directory: " + getName());
997 }
998
999 private void setChildren(List<? extends Node> children) {
1000 assert this.children == null : this + ": Cannot set child nodes twice!";
1001 this.children = Collections.unmodifiableList(children);
1002 }
1003 }
1004
1005 /**
1006 * Resource node (e.g. a ".class" entry, or any other data resource).
1007 *
1008 * <p>Resources are leaf nodes referencing an underlying image location. They
1009 * are lightweight, and do not cache their contents.
1010 *
1011 * <p>Unlike directories (where the node name matches the jimage path for the
1012 * corresponding {@code ImageLocation}), resource node names are NOT the same
1013 * as the corresponding jimage path. The difference is that node names for
1014 * resources are prefixed with "/modules", which is missing from the
1015 * equivalent jimage path.
1016 */
1017 private static class Resource extends Node {
1018 private final ImageLocation loc;
1019
|