8360037: Refactor ImageReader in preparation for Valhalla support

Reviewed-by: alanb, rriggs, jpai
This commit is contained in:
David Beaumont 2025-08-12 08:34:26 +00:00 committed by Jaikiran Pai
parent 5a442197d2
commit b81f4faed7
10 changed files with 880 additions and 626 deletions

View file

@ -119,9 +119,9 @@ class ExplodedImage extends SystemImage {
}
@Override
public List<Node> getChildren() {
public Stream<String> getChildNames() {
if (!isDirectory())
throw new IllegalArgumentException("not a directory: " + getNameString());
throw new IllegalArgumentException("not a directory: " + getName());
if (children == null) {
List<Node> list = new ArrayList<>();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(path)) {
@ -138,7 +138,7 @@ class ExplodedImage extends SystemImage {
}
children = list;
}
return children;
return children.stream().map(Node::getName);
}
@Override

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2016, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -49,7 +49,7 @@ final class JrtFileAttributes implements BasicFileAttributes {
//-------- basic attributes --------
@Override
public FileTime creationTime() {
return node.creationTime();
return node.getFileAttributes().creationTime();
}
@Override
@ -69,12 +69,12 @@ final class JrtFileAttributes implements BasicFileAttributes {
@Override
public FileTime lastAccessTime() {
return node.lastAccessTime();
return node.getFileAttributes().lastAccessTime();
}
@Override
public FileTime lastModifiedTime() {
return node.lastModifiedTime();
return node.getFileAttributes().lastModifiedTime();
}
@Override

View file

@ -54,7 +54,6 @@ import java.nio.file.attribute.FileAttribute;
import java.nio.file.attribute.FileTime;
import java.nio.file.attribute.UserPrincipalLookupService;
import java.nio.file.spi.FileSystemProvider;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
@ -64,7 +63,6 @@ import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;
import jdk.internal.jimage.ImageReader.Node;
import static java.util.stream.Collectors.toList;
/**
* jrt file system implementation built on System jimage files.
@ -225,19 +223,19 @@ class JrtFileSystem extends FileSystem {
throw new NotDirectoryException(path.getName());
}
if (filter == null) {
return node.getChildren()
.stream()
.map(child -> (Path)(path.resolve(new JrtPath(this, child.getNameString()).getFileName())))
.iterator();
return node.getChildNames()
.map(child -> (Path) (path.resolve(new JrtPath(this, child).getFileName())))
.iterator();
}
return node.getChildren()
.stream()
.map(child -> (Path)(path.resolve(new JrtPath(this, child.getNameString()).getFileName())))
.filter(p -> { try { return filter.accept(p);
} catch (IOException x) {}
return false;
})
.iterator();
return node.getChildNames()
.map(child -> (Path) (path.resolve(new JrtPath(this, child).getFileName())))
.filter(p -> {
try {
return filter.accept(p);
} catch (IOException x) {}
return false;
})
.iterator();
}
// returns the content of the file resource specified by the path

View file

@ -58,7 +58,6 @@ abstract class SystemImage {
if (modulesImageExists) {
// open a .jimage and build directory structure
final ImageReader image = ImageReader.open(moduleImageFile);
image.getRootDirectory();
return new SystemImage() {
@Override
Node findNode(String path) throws IOException {

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -34,7 +34,6 @@ import java.lang.module.ModuleReader;
import java.lang.module.ModuleReference;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.net.URLConnection;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
@ -54,7 +53,6 @@ import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import jdk.internal.jimage.ImageLocation;
import jdk.internal.jimage.ImageReader;
import jdk.internal.jimage.ImageReaderFactory;
import jdk.internal.access.JavaNetUriAccess;
@ -210,7 +208,7 @@ public final class SystemModuleFinders {
}
/**
* Parses the module-info.class of all module in the runtime image and
* Parses the {@code module-info.class} of all modules in the runtime image and
* returns a ModuleFinder to find the modules.
*
* @apiNote The returned ModuleFinder is thread safe.
@ -219,20 +217,16 @@ public final class SystemModuleFinders {
// parse the module-info.class in every module
Map<String, ModuleInfo.Attributes> nameToAttributes = new HashMap<>();
Map<String, byte[]> nameToHash = new HashMap<>();
ImageReader reader = SystemImage.reader();
for (String mn : reader.getModuleNames()) {
ImageLocation loc = reader.findLocation(mn, "module-info.class");
ModuleInfo.Attributes attrs
= ModuleInfo.read(reader.getResourceBuffer(loc), null);
nameToAttributes.put(mn, attrs);
allModuleAttributes().forEach(attrs -> {
nameToAttributes.put(attrs.descriptor().name(), attrs);
ModuleHashes hashes = attrs.recordedHashes();
if (hashes != null) {
for (String name : hashes.names()) {
nameToHash.computeIfAbsent(name, k -> hashes.hashFor(name));
}
}
}
});
// create a ModuleReference for each module
Set<ModuleReference> mrefs = new HashSet<>();
@ -253,6 +247,40 @@ public final class SystemModuleFinders {
return new SystemModuleFinder(mrefs, nameToModule);
}
/**
* Parses the {@code module-info.class} of all modules in the runtime image and
* returns a stream of {@link ModuleInfo.Attributes Attributes} for them. The
* returned attributes are in no specific order.
*/
private static Stream<ModuleInfo.Attributes> allModuleAttributes() {
// System-wide image reader.
ImageReader reader = SystemImage.reader();
try {
return reader.findNode("/modules")
.getChildNames()
.map(mn -> readModuleAttributes(reader, mn));
} catch (IOException e) {
throw new Error("Error reading root /modules entry", e);
}
}
/**
* Returns the module's "module-info", returning a holder for its class file
* attributes. Every module is required to have a valid {@code module-info.class}.
*/
private static ModuleInfo.Attributes readModuleAttributes(ImageReader reader, String moduleName) {
Exception err = null;
try {
ImageReader.Node node = reader.findNode(moduleName + "/module-info.class");
if (node != null && node.isResource()) {
return ModuleInfo.read(reader.getResourceBuffer(node), null);
}
} catch (IOException | UncheckedIOException e) {
err = e;
}
throw new Error("Missing or invalid module-info.class for module: " + moduleName, err);
}
/**
* A ModuleFinder that finds module in an array or set of modules.
*/
@ -382,34 +410,18 @@ public final class SystemModuleFinders {
this.module = module;
}
/**
* Returns the ImageLocation for the given resource, {@code null}
* if not found.
*/
private ImageLocation findImageLocation(String name) throws IOException {
Objects.requireNonNull(name);
if (closed)
throw new IOException("ModuleReader is closed");
ImageReader imageReader = SystemImage.reader();
if (imageReader != null) {
return imageReader.findLocation(module, name);
} else {
// not an images build
return null;
}
}
/**
* Returns {@code true} if the given resource exists, {@code false}
* if not found.
*/
private boolean containsImageLocation(String name) throws IOException {
Objects.requireNonNull(name);
private boolean containsResource(String resourcePath) throws IOException {
Objects.requireNonNull(resourcePath);
if (closed)
throw new IOException("ModuleReader is closed");
ImageReader imageReader = SystemImage.reader();
if (imageReader != null) {
return imageReader.verifyLocation(module, name);
ImageReader.Node node = imageReader.findNode("/modules" + resourcePath);
return node != null && node.isResource();
} else {
// not an images build
return false;
@ -418,8 +430,9 @@ public final class SystemModuleFinders {
@Override
public Optional<URI> find(String name) throws IOException {
if (containsImageLocation(name)) {
URI u = JNUA.create("jrt", "/" + module + "/" + name);
String resourcePath = "/" + module + "/" + name;
if (containsResource(resourcePath)) {
URI u = JNUA.create("jrt", resourcePath);
return Optional.of(u);
} else {
return Optional.empty();
@ -442,14 +455,25 @@ public final class SystemModuleFinders {
}
}
/**
* Returns the node for the given resource if found. If the name references
* a non-resource node, then {@code null} is returned.
*/
private ImageReader.Node findResource(ImageReader reader, String name) throws IOException {
Objects.requireNonNull(name);
if (closed) {
throw new IOException("ModuleReader is closed");
}
String nodeName = "/modules/" + module + "/" + name;
ImageReader.Node node = reader.findNode(nodeName);
return (node != null && node.isResource()) ? node : null;
}
@Override
public Optional<ByteBuffer> read(String name) throws IOException {
ImageLocation location = findImageLocation(name);
if (location != null) {
return Optional.of(SystemImage.reader().getResourceBuffer(location));
} else {
return Optional.empty();
}
ImageReader reader = SystemImage.reader();
return Optional.ofNullable(findResource(reader, name))
.map(reader::getResourceBuffer);
}
@Override
@ -481,7 +505,7 @@ public final class SystemModuleFinders {
private static class ModuleContentSpliterator implements Spliterator<String> {
final String moduleRoot;
final Deque<ImageReader.Node> stack;
Iterator<ImageReader.Node> iterator;
Iterator<String> iterator;
ModuleContentSpliterator(String module) throws IOException {
moduleRoot = "/modules/" + module;
@ -502,13 +526,10 @@ public final class SystemModuleFinders {
private String next() throws IOException {
for (;;) {
while (iterator.hasNext()) {
ImageReader.Node node = iterator.next();
String name = node.getName();
String name = iterator.next();
ImageReader.Node node = SystemImage.reader().findNode(name);
if (node.isDirectory()) {
// build node
ImageReader.Node dir = SystemImage.reader().findNode(name);
assert dir.isDirectory();
stack.push(dir);
stack.push(node);
} else {
// strip /modules/$MODULE/ prefix
return name.substring(moduleRoot.length() + 1);
@ -520,7 +541,7 @@ public final class SystemModuleFinders {
} else {
ImageReader.Node dir = stack.poll();
assert dir.isDirectory();
iterator = dir.getChildren().iterator();
iterator = dir.getChildNames().iterator();
}
}
}

View file

@ -0,0 +1,279 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import jdk.internal.jimage.ImageReader;
import jdk.internal.jimage.ImageReader.Node;
import jdk.test.lib.compiler.InMemoryJavaCompiler;
import jdk.test.lib.util.JarBuilder;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.opentest4j.TestSkippedException;
import tests.Helper;
import tests.JImageGenerator;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS;
/*
* @test
* @summary Tests for ImageReader.
* @modules java.base/jdk.internal.jimage
* jdk.jlink/jdk.tools.jimage
* @library /test/jdk/tools/lib
* /test/lib
* @build tests.*
* @run junit/othervm ImageReaderTest
*/
/// Using PER_CLASS lifecycle means the (expensive) image file is only build once.
/// There is no mutable test instance state to worry about.
@TestInstance(PER_CLASS)
public class ImageReaderTest {
private static final Map<String, List<String>> IMAGE_ENTRIES = Map.of(
"modfoo", Arrays.asList(
"com.foo.Alpha",
"com.foo.Beta",
"com.foo.bar.Gamma"),
"modbar", Arrays.asList(
"com.bar.One",
"com.bar.Two"));
private final Path image = buildJImage(IMAGE_ENTRIES);
@ParameterizedTest
@ValueSource(strings = {
"/",
"/modules",
"/modules/modfoo",
"/modules/modbar",
"/modules/modfoo/com",
"/modules/modfoo/com/foo",
"/modules/modfoo/com/foo/bar"})
public void testModuleDirectories_expected(String name) throws IOException {
try (ImageReader reader = ImageReader.open(image)) {
assertDir(reader, name);
}
}
@ParameterizedTest
@ValueSource(strings = {
"",
"//",
"/modules/",
"/modules/unknown",
"/modules/modbar/",
"/modules/modfoo//com",
"/modules/modfoo/com/"})
public void testModuleNodes_absent(String name) throws IOException {
try (ImageReader reader = ImageReader.open(image)) {
assertAbsent(reader, name);
}
}
@Test
public void testModuleResources() throws IOException {
try (ImageReader reader = ImageReader.open(image)) {
assertNode(reader, "/modules/modfoo/com/foo/Alpha.class");
assertNode(reader, "/modules/modbar/com/bar/One.class");
ImageClassLoader loader = new ImageClassLoader(reader, IMAGE_ENTRIES.keySet());
assertEquals("Class: com.foo.Alpha", loader.loadAndGetToString("modfoo", "com.foo.Alpha"));
assertEquals("Class: com.foo.Beta", loader.loadAndGetToString("modfoo", "com.foo.Beta"));
assertEquals("Class: com.foo.bar.Gamma", loader.loadAndGetToString("modfoo", "com.foo.bar.Gamma"));
assertEquals("Class: com.bar.One", loader.loadAndGetToString("modbar", "com.bar.One"));
}
}
@Test
public void testPackageDirectories() throws IOException {
try (ImageReader reader = ImageReader.open(image)) {
Node root = assertDir(reader, "/packages");
Set<String> pkgNames = root.getChildNames().collect(Collectors.toSet());
assertTrue(pkgNames.contains("/packages/com"));
assertTrue(pkgNames.contains("/packages/com.foo"));
assertTrue(pkgNames.contains("/packages/com.bar"));
// Even though no classes exist directly in the "com" package, it still
// creates a directory with links back to all the modules which contain it.
Set<String> comLinks = assertDir(reader, "/packages/com").getChildNames().collect(Collectors.toSet());
assertTrue(comLinks.contains("/packages/com/modfoo"));
assertTrue(comLinks.contains("/packages/com/modbar"));
}
}
@Test
public void testPackageLinks() throws IOException {
try (ImageReader reader = ImageReader.open(image)) {
Node moduleFoo = assertDir(reader, "/modules/modfoo");
Node moduleBar = assertDir(reader, "/modules/modbar");
assertSame(assertLink(reader, "/packages/com.foo/modfoo").resolveLink(), moduleFoo);
assertSame(assertLink(reader, "/packages/com.bar/modbar").resolveLink(), moduleBar);
}
}
private static ImageReader.Node assertNode(ImageReader reader, String name) throws IOException {
ImageReader.Node node = reader.findNode(name);
assertNotNull(node, "Could not find node: " + name);
return node;
}
private static ImageReader.Node assertDir(ImageReader reader, String name) throws IOException {
ImageReader.Node dir = assertNode(reader, name);
assertTrue(dir.isDirectory(), "Node was not a directory: " + name);
return dir;
}
private static ImageReader.Node assertLink(ImageReader reader, String name) throws IOException {
ImageReader.Node link = assertNode(reader, name);
assertTrue(link.isLink(), "Node was not a symbolic link: " + name);
return link;
}
private static void assertAbsent(ImageReader reader, String name) throws IOException {
assertNull(reader.findNode(name), "Should not be able to find node: " + name);
}
/// Builds a jimage file with the specified class entries. The classes in the built
/// image can be loaded and executed to return their names via `toString()` to confirm
/// the correct bytes were returned.
public static Path buildJImage(Map<String, List<String>> entries) {
Helper helper = getHelper();
Path outDir = helper.createNewImageDir("test");
JImageGenerator.JLinkTask jlink = JImageGenerator.getJLinkTask()
.modulePath(helper.defaultModulePath())
.output(outDir);
Path jarDir = helper.getJarDir();
entries.forEach((module, classes) -> {
JarBuilder jar = new JarBuilder(jarDir.resolve(module + ".jar").toString());
String moduleInfo = "module " + module + " {}";
jar.addEntry("module-info.class", InMemoryJavaCompiler.compile("module-info", moduleInfo));
classes.forEach(fqn -> {
int lastDot = fqn.lastIndexOf('.');
String pkg = fqn.substring(0, lastDot);
String cls = fqn.substring(lastDot + 1);
String path = fqn.replace('.', '/') + ".class";
String source = String.format(
"""
package %s;
public class %s {
public String toString() {
return "Class: %s";
}
}
""", pkg, cls, fqn);
jar.addEntry(path, InMemoryJavaCompiler.compile(fqn, source));
});
try {
jar.build();
} catch (IOException e) {
throw new RuntimeException(e);
}
jlink.addMods(module);
});
return jlink.call().assertSuccess().resolve("lib", "modules");
}
/// Returns the helper for building JAR and jimage files.
private static Helper getHelper() {
try {
Helper helper = Helper.newHelper();
if (helper == null) {
throw new TestSkippedException("Cannot create test helper (exploded image?)");
}
return helper;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/// Loads and performs actions on classes stored in a given `ImageReader`.
private static class ImageClassLoader extends ClassLoader {
private final ImageReader reader;
private final Set<String> testModules;
private ImageClassLoader(ImageReader reader, Set<String> testModules) {
this.reader = reader;
this.testModules = testModules;
}
@FunctionalInterface
public interface ClassAction<R, T extends Exception> {
R call(Class<?> cls) throws T;
}
String loadAndGetToString(String module, String fqn) {
return loadAndCall(module, fqn, c -> c.getDeclaredConstructor().newInstance().toString());
}
<R> R loadAndCall(String module, String fqn, ClassAction<R, ?> action) {
Class<?> cls = findClass(module, fqn);
assertNotNull(cls, "Could not load class: " + module + "/" + fqn);
try {
return action.call(cls);
} catch (Exception e) {
fail("Class loading failed", e);
return null;
}
}
@Override
protected Class<?> findClass(String module, String fqn) {
assumeTrue(testModules.contains(module), "Can only load classes in modules: " + testModules);
String name = "/modules/" + module + "/" + fqn.replace('.', '/') + ".class";
Class<?> cls = findLoadedClass(fqn);
if (cls == null) {
try {
ImageReader.Node node = reader.findNode(name);
if (node != null && node.isResource()) {
byte[] classBytes = reader.getResource(node);
cls = defineClass(fqn, classBytes, 0, classBytes.length);
resolveClass(cls);
return cls;
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
return null;
}
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2015, 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -49,6 +49,9 @@ import org.testng.annotations.Test;
import org.testng.Assert;
import org.testng.TestNG;
import static java.nio.ByteOrder.BIG_ENDIAN;
import static java.nio.ByteOrder.LITTLE_ENDIAN;
@Test
public class JImageReadTest {
@ -333,32 +336,21 @@ public class JImageReadTest {
*/
@Test
static void test5_imageReaderEndianness() throws IOException {
ImageReader nativeReader = ImageReader.open(imageFile);
Assert.assertEquals(nativeReader.getByteOrder(), ByteOrder.nativeOrder());
try {
ImageReader leReader = ImageReader.open(imageFile, ByteOrder.LITTLE_ENDIAN);
Assert.assertEquals(leReader.getByteOrder(), ByteOrder.LITTLE_ENDIAN);
leReader.close();
} catch (IOException io) {
// IOException expected if LITTLE_ENDIAN not the nativeOrder()
Assert.assertNotEquals(ByteOrder.nativeOrder(), ByteOrder.LITTLE_ENDIAN);
// Will be opened with native byte order.
try (ImageReader nativeReader = ImageReader.open(imageFile)) {
// Just ensure something works as expected.
Assert.assertNotNull(nativeReader.findNode("/"));
} catch (IOException expected) {
Assert.fail("Reader should be openable with native byte order.");
}
try {
ImageReader beReader = ImageReader.open(imageFile, ByteOrder.BIG_ENDIAN);
Assert.assertEquals(beReader.getByteOrder(), ByteOrder.BIG_ENDIAN);
beReader.close();
} catch (IOException io) {
// IOException expected if LITTLE_ENDIAN not the nativeOrder()
Assert.assertNotEquals(ByteOrder.nativeOrder(), ByteOrder.BIG_ENDIAN);
}
nativeReader.close();
// Reader should not be openable with the wrong byte order.
ByteOrder otherOrder = ByteOrder.nativeOrder() == BIG_ENDIAN ? LITTLE_ENDIAN : BIG_ENDIAN;
Assert.assertThrows(IOException.class, () -> ImageReader.open(imageFile, otherOrder));
}
// main method to run standalone from jtreg
@Test(enabled=false)
// main method to run standalone from jtreg
@Test(enabled = false)
@Parameters({"x"})
@SuppressWarnings("raw_types")
public static void main(@Optional String[] args) {

View file

@ -68,17 +68,17 @@ public class ImageReaderDuplicateChildNodesTest {
+ " in " + imagePath);
}
// now verify that the parent node which is a directory, doesn't have duplicate children
final List<ImageReader.Node> children = parent.getChildren();
if (children == null || children.isEmpty()) {
final List<String> childNames = parent.getChildNames().toList();
if (childNames.isEmpty()) {
throw new RuntimeException("ImageReader did not return any child resources under "
+ integersParentResource + " in " + imagePath);
}
final Set<ImageReader.Node> uniqueChildren = new HashSet<>();
for (final ImageReader.Node child : children) {
final boolean unique = uniqueChildren.add(child);
for (final String childName : childNames) {
final boolean unique = uniqueChildren.add(reader.findNode(childName));
if (!unique) {
throw new RuntimeException("ImageReader returned duplicate child resource "
+ child + " under " + parent + " from image " + imagePath);
+ childName + " under " + parent + " from image " + imagePath);
}
}
}

View file

@ -195,9 +195,9 @@ public class ImageReaderBenchmark {
static long countAllNodes(ImageReader reader, Node node) {
long count = 1;
if (node.isDirectory()) {
count += node.getChildren().stream().mapToLong(n -> {
count += node.getChildNames().mapToLong(n -> {
try {
return countAllNodes(reader, reader.findNode(n.getName()));
return countAllNodes(reader, reader.findNode(n));
} catch (IOException e) {
throw new RuntimeException(e);
}