From c0a0c4cbac333cdcde80526263a5d39d6c5af19f Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Wed, 28 Oct 2020 18:25:28 -0700 Subject: [PATCH] Show the source jar of a ClasspathResource Update `TextResourceOrigin` so that it shows the source jar file of a `ClasspathResource`. Closes gh-23019 --- .../springframework/boot/origin/JarUri.java | 82 +++++++++++++++++++ .../boot/origin/TextResourceOrigin.java | 32 +++++++- .../boot/origin/JarUriTests.java | 56 +++++++++++++ .../boot/origin/TextResourceOriginTests.java | 29 ++++++- 4 files changed, 195 insertions(+), 4 deletions(-) create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/origin/JarUri.java create mode 100644 spring-boot-project/spring-boot/src/test/java/org/springframework/boot/origin/JarUriTests.java diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/origin/JarUri.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/origin/JarUri.java new file mode 100644 index 0000000000..735a5ce019 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/origin/JarUri.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.origin; + +import java.net.URI; + +/** + * Simple class that understands Jar URLs can can provide short descriptions. + * + * @author Phillip Webb + */ +final class JarUri { + + private static final String JAR_SCHEME = "jar:"; + + private static final String JAR_EXTENSION = ".jar"; + + private final String uri; + + private final String description; + + private JarUri(String uri) { + this.uri = uri; + this.description = extractDescription(uri); + } + + private String extractDescription(String uri) { + uri = uri.substring(JAR_SCHEME.length()); + int firstDotJar = uri.indexOf(JAR_EXTENSION); + String firstJar = getFilename(uri.substring(0, firstDotJar + JAR_EXTENSION.length())); + uri = uri.substring(firstDotJar + JAR_EXTENSION.length()); + int lastDotJar = uri.lastIndexOf(JAR_EXTENSION); + if (lastDotJar == -1) { + return firstJar; + } + return firstJar + uri.substring(0, lastDotJar + JAR_EXTENSION.length()); + } + + private String getFilename(String string) { + int lastSlash = string.lastIndexOf('/'); + return (lastSlash == -1) ? string : string.substring(lastSlash + 1); + } + + String getDescription() { + return this.description; + } + + String getDescription(String existing) { + return existing + " from " + this.description; + } + + @Override + public String toString() { + return this.uri; + } + + static JarUri from(URI uri) { + return from(uri.toString()); + } + + static JarUri from(String uri) { + if (uri.startsWith(JAR_SCHEME) && uri.contains(JAR_EXTENSION)) { + return new JarUri(uri); + } + return null; + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/origin/TextResourceOrigin.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/origin/TextResourceOrigin.java index 9c1f422b27..f4cbffc037 100644 --- a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/origin/TextResourceOrigin.java +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/origin/TextResourceOrigin.java @@ -16,6 +16,9 @@ package org.springframework.boot.origin; +import java.io.IOException; + +import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.util.ObjectUtils; @@ -91,13 +94,38 @@ public class TextResourceOrigin implements Origin { @Override public String toString() { StringBuilder result = new StringBuilder(); - result.append((this.resource != null) ? this.resource.getDescription() : "unknown resource [?]"); + result.append(getResourceDescription(this.resource)); if (this.location != null) { - result.append(":").append(this.location); + result.append(" - ").append(this.location); } return result.toString(); } + private String getResourceDescription(Resource resource) { + if (resource instanceof OriginTrackedResource) { + return getResourceDescription(((OriginTrackedResource) resource).getResource()); + } + if (resource == null) { + return "unknown resource [?]"; + } + if (resource instanceof ClassPathResource) { + return getResourceDescription((ClassPathResource) resource); + } + return resource.getDescription(); + } + + private String getResourceDescription(ClassPathResource resource) { + try { + JarUri jarUri = JarUri.from(resource.getURI()); + if (jarUri != null) { + return jarUri.getDescription(resource.getDescription()); + } + } + catch (IOException ex) { + } + return resource.getDescription(); + } + /** * A location (line and column number) within the resource. */ diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/origin/JarUriTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/origin/JarUriTests.java new file mode 100644 index 0000000000..04fdf8965c --- /dev/null +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/origin/JarUriTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.origin; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author pwebb + */ +class JarUriTests { + + @Test + void describeBootInfClassesUri() { + JarUri uri = JarUri.from("jar:file:/home/user/project/target/project-0.0.1-SNAPSHOT.jar" + + "!/BOOT-INF/classes!/application.properties"); + assertThat(uri.getDescription()).isEqualTo("project-0.0.1-SNAPSHOT.jar"); + } + + @Test + void describeBootInfLibUri() { + JarUri uri = JarUri.from("jar:file:/home/user/project/target/project-0.0.1-SNAPSHOT.jar" + + "!/BOOT-INF/lib/nested.jar!/application.properties"); + assertThat(uri.getDescription()).isEqualTo("project-0.0.1-SNAPSHOT.jar!/BOOT-INF/lib/nested.jar"); + } + + @Test + void describeRegularJar() { + JarUri uri = JarUri + .from("jar:file:/home/user/project/target/project-0.0.1-SNAPSHOT.jar!/application.properties"); + assertThat(uri.getDescription()).isEqualTo("project-0.0.1-SNAPSHOT.jar"); + } + + @Test + void getDescriptionMergedWithExisting() { + JarUri uri = JarUri.from("jar:file:/project-0.0.1-SNAPSHOT.jar!/application.properties"); + assertThat(uri.getDescription("classpath: [application.properties]")) + .isEqualTo("classpath: [application.properties] from project-0.0.1-SNAPSHOT.jar"); + } + +} diff --git a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/origin/TextResourceOriginTests.java b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/origin/TextResourceOriginTests.java index 6f5827411e..59d87862c4 100644 --- a/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/origin/TextResourceOriginTests.java +++ b/spring-boot-project/spring-boot/src/test/java/org/springframework/boot/origin/TextResourceOriginTests.java @@ -16,6 +16,10 @@ package org.springframework.boot.origin; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; + import org.junit.jupiter.api.Test; import org.springframework.boot.origin.TextResourceOrigin.Location; @@ -95,14 +99,14 @@ class TextResourceOriginTests { ClassPathResource resource = new ClassPathResource("foo.txt"); Location location = new Location(1, 2); TextResourceOrigin origin = new TextResourceOrigin(resource, location); - assertThat(origin.toString()).isEqualTo("class path resource [foo.txt]:2:3"); + assertThat(origin.toString()).isEqualTo("class path resource [foo.txt] - 2:3"); } @Test void toStringWhenResourceIsNullReturnsNiceString() { Location location = new Location(1, 2); TextResourceOrigin origin = new TextResourceOrigin(null, location); - assertThat(origin.toString()).isEqualTo("unknown resource [?]:2:3"); + assertThat(origin.toString()).isEqualTo("unknown resource [?] - 2:3"); } @Test @@ -112,6 +116,27 @@ class TextResourceOriginTests { assertThat(origin.toString()).isEqualTo("class path resource [foo.txt]"); } + @Test + void toStringWhenResourceIsClasspathResourceReturnsToStringWithJar() { + ClassPathResource resource = new ClassPathResource("foo.txt") { + + @Override + public URI getURI() throws IOException { + try { + return new URI("jar:file:/home/user/project/target/project-0.0.1-SNAPSHOT.jar" + + "!/BOOT-INF/classes!/foo.txt"); + } + catch (URISyntaxException ex) { + throw new IllegalStateException(ex); + } + } + + }; + Location location = new Location(1, 2); + TextResourceOrigin origin = new TextResourceOrigin(resource, location); + assertThat(origin.toString()).isEqualTo("class path resource [foo.txt] from project-0.0.1-SNAPSHOT.jar - 2:3"); + } + @Test void locationEqualsAndHashCodeUsesLineAndColumn() { Location location1 = new Location(1, 2);