From 852c8085ffb04d423623d27a90d2981fe4997d10 Mon Sep 17 00:00:00 2001 From: Peter Gafert Date: Sat, 1 Feb 2020 01:37:11 +0700 Subject: [PATCH] fix broken JRT URL handling for JDK >= 13 With JDK 13 some wrongly implemented behavior of JRT path handling was fixed. I.e. by JEP-220 (https://openjdk.java.net/jeps/220) while URIs possess the shape of `jrt:/module.name/some/clazz/Name.class` the respective (virtual) file system path for JRTs supposedly has the shape of `/modules/module.name/some/clazz/Name.class`. Unfortunately ArchUnit was relying on the wrongly implemented version of `JrtFileSystemProvider` which would return a path for JRTs of `/module.name/some/clazz/Name.class`. Thus with JDK >= 13 the URI handling for JRTs was broken. This will now be fixed by relying on pure JRT URI handling without relying on conversion to `java.nio.file.Path`. We simply split the path of a JRT URI on `/` and treat the first segment as module name, the rest of the segments as the resource/class name (e.g. `module.name` and `some/clazz/Name.class`). Note that we unfortunately cannot use `URI.getPath()` within `NormalizedUri` to retrieve the path without the schema, since `URI.getPath()` will return `null` for JAR URIs (i.e. `jar:file:/...`). Signed-off-by: Peter Gafert --- .../core/importer/ModuleLocationFactory.java | 19 ++++---- .../importer/ModuleLocationFactoryTest.java | 17 +++++-- .../archunit/core/importer/NormalizedUri.java | 19 +++++++- .../core/importer/NormalizedUriTest.java | 45 +++++++++++++++++++ .../maven-integration-test.gradle | 5 ++- 5 files changed, 89 insertions(+), 16 deletions(-) create mode 100644 archunit/src/test/java/com/tngtech/archunit/core/importer/NormalizedUriTest.java diff --git a/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleLocationFactory.java b/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleLocationFactory.java index 10a2f0476d..775e4d99c0 100644 --- a/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleLocationFactory.java +++ b/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleLocationFactory.java @@ -21,8 +21,6 @@ import java.lang.module.ModuleReader; import java.lang.module.ModuleReference; import java.net.URI; -import java.nio.file.Path; -import java.nio.file.Paths; import java.util.Collections; import java.util.Iterator; import java.util.Optional; @@ -63,9 +61,8 @@ private static class ModuleLocation extends Location { ModuleLocation(NormalizedUri uri) { super(uri); checkScheme(SCHEME, uri); - Path uriPath = Paths.get(uri.toURI()); - this.moduleReference = parseModuleName(uriPath, uri); - this.resourceName = parseResourceName(uriPath); + this.moduleReference = findModuleReference(uri); + this.resourceName = parseResourceName(uri); } ModuleLocation(ModuleReference moduleReference, NormalizedResourceName resourceName) { @@ -81,15 +78,15 @@ private static NormalizedUri createUri(ModuleReference moduleReference, Normaliz return NormalizedUri.from(moduleReference.location().get() + resourceName.toAbsolutePath()); } - private ModuleReference parseModuleName(Path uriPath, NormalizedUri uri) { - String moduleName = uriPath.getName(0).toString(); + private ModuleReference findModuleReference(NormalizedUri uri) { + String moduleName = uri.getFirstSegment(); Optional moduleReference = ModuleFinder.ofSystem().find(moduleName); checkState(moduleReference.isPresent(), "Couldn't find module %s of URI %s", moduleName, uri); return moduleReference.get(); } - private NormalizedResourceName parseResourceName(Path uriPath) { - return NormalizedResourceName.from(uriPath.getName(0).toAbsolutePath().relativize(uriPath).toString()); + private NormalizedResourceName parseResourceName(NormalizedUri uri) { + return NormalizedResourceName.from(uri.getTailSegments()); } @Override @@ -165,7 +162,9 @@ private static class ModuleClassFileLocation implements ClassFileLocation { @Override public InputStream openStream() { - return doWithModuleReader(moduleReference, moduleReader -> moduleReader.open(entry.toString()).get()); + return doWithModuleReader(moduleReference, moduleReader -> + moduleReader.open(entry.toString()).orElseThrow(() -> new IllegalStateException( + String.format("Entry %s parsed from JRT location %s could not be opened. This is most likely a bug.", entry, location)))); } @Override diff --git a/archunit/src/jdk9test/java/com/tngtech/archunit/core/importer/ModuleLocationFactoryTest.java b/archunit/src/jdk9test/java/com/tngtech/archunit/core/importer/ModuleLocationFactoryTest.java index e8cbb585eb..a8e402bc4e 100644 --- a/archunit/src/jdk9test/java/com/tngtech/archunit/core/importer/ModuleLocationFactoryTest.java +++ b/archunit/src/jdk9test/java/com/tngtech/archunit/core/importer/ModuleLocationFactoryTest.java @@ -2,8 +2,8 @@ import java.io.File; import java.io.FileReader; -import java.io.IOException; import java.lang.module.ModuleFinder; +import java.lang.module.ModuleReference; import java.net.URI; import java.net.URISyntaxException; import java.util.HashSet; @@ -20,6 +20,15 @@ public class ModuleLocationFactoryTest { private ModuleLocationFactory locationFactory = new ModuleLocationFactory(); + @Test + public void reads_single_entry_of_jrt() throws URISyntaxException { + URI jrtJavaIoFile = uriOf(File.class); + Location jrtJavaIo = locationFactory.create(jrtJavaIoFile); + + assertThat(jrtJavaIo.iterateEntries()) + .containsOnly(NormalizedResourceName.from(File.class.getName().replace('.', '/') + ".class")); + } + @Test public void iterates_package_of_jrt() throws URISyntaxException { URI jrtJavaIoFile = uriOf(File.class); @@ -53,9 +62,8 @@ public void respects_import_options() throws URISyntaxException { } @Test - @SuppressWarnings("ConstantConditions") - public void filters_out_module_infos() throws IOException { - URI jrtUri = ModuleFinder.ofSystem().find("java.base").get().location().get(); + public void filters_out_module_infos() { + URI jrtUri = ModuleFinder.ofSystem().find("java.base").flatMap(ModuleReference::location).get(); ClassFileSource source = Location.of(jrtUri).asClassFileSource(new ImportOptions()); @@ -67,6 +75,7 @@ public void filters_out_module_infos() throws IOException { .isFalse(); } + @SuppressWarnings("SameParameterValue") private URI createModuleUriContaining(Class clazz) throws URISyntaxException { URI someJrt = uriOf(clazz); String moduleUri = someJrt.toString().replaceAll("(jrt:/[^/]+).*", "$1"); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/NormalizedUri.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/NormalizedUri.java index 139866ff6d..c844c9e607 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/NormalizedUri.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/NormalizedUri.java @@ -16,16 +16,25 @@ package com.tngtech.archunit.core.importer; import java.net.URI; +import java.util.List; import java.util.Objects; +import com.google.common.base.Joiner; +import com.google.common.base.Splitter; + class NormalizedUri { private final URI uri; + private final String firstSegment; + private final String tailSegments; private NormalizedUri(URI uri) { - String uriString = uri.toString(); + String uriString = uri.normalize().toString(); uriString = uriString.replaceAll("://*", ":/"); // this is how getClass().getResource(..) returns URLs uriString = !uriString.endsWith("/") && !uriString.endsWith(".class") ? uriString + "/" : uriString; // we always want folders to end in '/' this.uri = URI.create(uriString); + List path = Splitter.on("/").omitEmptyStrings().splitToList(this.uri.toString().replaceAll("^.*:", "")); + firstSegment = path.get(0); + tailSegments = path.size() < 2 ? "" : Joiner.on("/").join(path.subList(1, path.size())); } URI toURI() { @@ -36,6 +45,14 @@ String getScheme() { return uri.getScheme(); } + public String getFirstSegment() { + return firstSegment; + } + + public String getTailSegments() { + return tailSegments; + } + @Override public int hashCode() { return Objects.hash(uri); diff --git a/archunit/src/test/java/com/tngtech/archunit/core/importer/NormalizedUriTest.java b/archunit/src/test/java/com/tngtech/archunit/core/importer/NormalizedUriTest.java new file mode 100644 index 0000000000..0432e7a53d --- /dev/null +++ b/archunit/src/test/java/com/tngtech/archunit/core/importer/NormalizedUriTest.java @@ -0,0 +1,45 @@ +package com.tngtech.archunit.core.importer; + +import java.net.URI; + +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class NormalizedUriTest { + @Test + public void normalizes_URI() { + NormalizedUri uri = NormalizedUri.from(URI.create("prot:/some/.././uri/.")); + + assertThat(uri.toURI()).isEqualTo(URI.create("prot:/uri/")); + } + + @Test + public void normalizes_URI_from_string() { + NormalizedUri uri = NormalizedUri.from("prot:/some/../uri/."); + + assertThat(uri.toURI()).isEqualTo(URI.create("prot:/uri/")); + } + + @Test + public void parses_first_segment() { + NormalizedUri uri = NormalizedUri.from("jrt:/java.base/java/io/File.class"); + + assertThat(uri.getFirstSegment()).isEqualTo("java.base"); + + uri = NormalizedUri.from("jrt:/java.base"); + + assertThat(uri.getFirstSegment()).isEqualTo("java.base"); + } + + @Test + public void parses_tail_segments() { + NormalizedUri uri = NormalizedUri.from("jrt:/java.base/java/io/File.class"); + + assertThat(uri.getTailSegments()).isEqualTo("java/io/File.class"); + + uri = NormalizedUri.from("jrt:/java.base"); + + assertThat(uri.getTailSegments()).isEmpty(); + } +} \ No newline at end of file diff --git a/build-steps/maven-integration-test/maven-integration-test.gradle b/build-steps/maven-integration-test/maven-integration-test.gradle index 5d5d036109..e638902783 100644 --- a/build-steps/maven-integration-test/maven-integration-test.gradle +++ b/build-steps/maven-integration-test/maven-integration-test.gradle @@ -185,7 +185,10 @@ def javaConfigs = [ [suffix: "java8", javaVersion: JavaVersion.VERSION_1_8, jdkProp: "java8Home"], [suffix: "java9", javaVersion: JavaVersion.VERSION_1_9, jdkProp: "java9Home"], [suffix: "java10", javaVersion: JavaVersion.VERSION_1_10, jdkProp: "java10Home"], - [suffix: "java11", javaVersion: JavaVersion.VERSION_11, jdkProp: "java11Home"] + [suffix: "java11", javaVersion: JavaVersion.VERSION_11, jdkProp: "java11Home"], + [suffix: "java12", javaVersion: JavaVersion.VERSION_HIGHER, jdkProp: "java12Home"], + [suffix: "java13", javaVersion: JavaVersion.VERSION_HIGHER, jdkProp: "java13Home"], + [suffix: "java14", javaVersion: JavaVersion.VERSION_HIGHER, jdkProp: "java14Home"] ] javaConfigs = javaConfigs.findAll { project.hasProperty(it.jdkProp) }