diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java index 654caf45ae..533d85f3ee 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.OutputStreamWriter; -import java.util.ArrayList; import java.util.Calendar; import java.util.Collection; import java.util.GregorianCalendar; @@ -31,7 +30,6 @@ import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; -import java.util.Properties; import java.util.Set; import java.util.function.Function; import java.util.regex.Pattern; @@ -59,6 +57,8 @@ import org.springframework.boot.loader.tools.JarModeLibrary; import org.springframework.boot.loader.tools.Layer; import org.springframework.boot.loader.tools.LayersIndex; import org.springframework.boot.loader.tools.LibraryCoordinates; +import org.springframework.boot.loader.tools.NativeImageArgFile; +import org.springframework.boot.loader.tools.ReachabilityMetadataProperties; import org.springframework.util.Assert; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; @@ -76,10 +76,9 @@ class BootZipCopyAction implements CopyAction { static final long CONSTANT_TIME_FOR_ZIP_ENTRIES = new GregorianCalendar(1980, Calendar.FEBRUARY, 1, 0, 0, 0) .getTimeInMillis(); - private static final String REACHABILITY_METADATA_PROPERTIES_LOCATION = "META-INF/native-image/%s/%s/%s/reachability-metadata.properties"; - private static final Pattern REACHABILITY_METADATA_PROPERTIES_LOCATION_PATTERN = Pattern - .compile(REACHABILITY_METADATA_PROPERTIES_LOCATION.formatted(".*", ".*", ".*")); + .compile(ReachabilityMetadataProperties.REACHABILITY_METADATA_PROPERTIES_LOCATION_TEMPLATE.formatted(".*", + ".*", ".*")); private final File output; @@ -355,32 +354,23 @@ class BootZipCopyAction implements CopyAction { DependencyDescriptor descriptor = BootZipCopyAction.this.resolvedDependencies .find(entry.getValue().getFile()); LibraryCoordinates coordinates = (descriptor != null) ? descriptor.getCoordinates() : null; - FileCopyDetails propertiesFile = (coordinates != null) - ? this.reachabilityMetadataProperties.get(REACHABILITY_METADATA_PROPERTIES_LOCATION.formatted( - coordinates.getGroupId(), coordinates.getArtifactId(), coordinates.getVersion())) - : null; + FileCopyDetails propertiesFile = (coordinates != null) ? this.reachabilityMetadataProperties + .get(ReachabilityMetadataProperties.getLocation(coordinates)) : null; if (propertiesFile != null) { try (InputStream inputStream = propertiesFile.open()) { - Properties properties = new Properties(); - properties.load(inputStream); - if (Boolean.parseBoolean(properties.getProperty("override"))) { + ReachabilityMetadataProperties properties = ReachabilityMetadataProperties + .fromInputStream(inputStream); + if (properties.isOverridden()) { excludes.add(entry.getKey()); } } } } - if (excludes != null) { - List args = new ArrayList<>(); - for (String exclude : excludes) { - int lastSlash = exclude.lastIndexOf('/'); - String jar = (lastSlash != -1) ? exclude.substring(lastSlash + 1) : exclude; - args.add("--exclude-config"); - args.add(Pattern.quote(jar)); - args.add("^/META-INF/native-image/.*"); - } - ZipEntryContentWriter writer = ZipEntryContentWriter.fromLines(BootZipCopyAction.this.encoding, args); - writeEntry("META-INF/native-image/argfile", writer, true); - } + NativeImageArgFile argFile = new NativeImageArgFile(excludes); + argFile.writeIfNecessary((lines) -> { + ZipEntryContentWriter writer = ZipEntryContentWriter.fromLines(BootZipCopyAction.this.encoding, lines); + writeEntry(NativeImageArgFile.LOCATION, writer, true); + }); } private void writeLayersIndexIfNecessary() throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java index a14dd78893..67bba89e90 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarTests.java @@ -201,6 +201,13 @@ class BootJarTests extends AbstractBootArchiveTests { } } + @Test + void nativeImageArgFileIsNotWrittenWhenExcludesAreEmpty() throws IOException { + try (JarFile jarFile = new JarFile(createLayeredJar(false))) { + assertThat(jarFile.getEntry("META-INF/native-image/argfile")).isNull(); + } + } + @Test void javaVersionIsWrittenToManifest() throws IOException { try (JarFile jarFile = new JarFile(createPopulatedJar())) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/NativeImageArgFile.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/NativeImageArgFile.java new file mode 100644 index 0000000000..1a2379927e --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/NativeImageArgFile.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2022 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.loader.tools; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Pattern; + +import org.springframework.util.function.ThrowingConsumer; + +/** + * Class to work with the native-image argfile. + * + * @author Moritz Halbritter + * @author Phil Webb + * @since 3.0.0 + */ +public final class NativeImageArgFile { + + /** + * Location of the argfile. + */ + public static final String LOCATION = "META-INF/native-image/argfile"; + + private final List excludes; + + /** + * Constructs a new instance with the given excludes. + * @param excludes dependencies for which the reachability metadata should be excluded + */ + public NativeImageArgFile(Collection excludes) { + this.excludes = List.copyOf(excludes); + } + + /** + * Write the arguments file if it is necessary. + * @param writer consumer that should write the contents + */ + public void writeIfNecessary(ThrowingConsumer> writer) { + if (this.excludes.isEmpty()) { + return; + } + List lines = new ArrayList<>(); + for (String exclude : this.excludes) { + int lastSlash = exclude.lastIndexOf('/'); + String jar = (lastSlash != -1) ? exclude.substring(lastSlash + 1) : exclude; + lines.add("--exclude-config"); + lines.add(Pattern.quote(jar)); + lines.add("^/META-INF/native-image/.*"); + } + writer.accept(lines); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java index 3304565f3e..61ec3eb5fc 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/Packager.java @@ -28,7 +28,6 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; -import java.util.Properties; import java.util.Set; import java.util.TreeMap; import java.util.concurrent.TimeUnit; @@ -37,8 +36,6 @@ import java.util.jar.Attributes; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.Manifest; -import java.util.regex.Pattern; -import java.util.stream.Collectors; import java.util.zip.ZipEntry; import org.apache.commons.compress.archivers.jar.JarArchiveEntry; @@ -61,8 +58,6 @@ import org.springframework.util.StringUtils; */ public abstract class Packager { - private static final String REACHABILITY_METADATA_PROPERTIES_LOCATION = "META-INF/native-image/%s/%s/%s/reachability-metadata.properties"; - private static final String MAIN_CLASS_ATTRIBUTE = "Main-Class"; private static final String START_CLASS_ATTRIBUTE = "Start-Class"; @@ -229,31 +224,24 @@ public abstract class Packager { Set excludes = new LinkedHashSet<>(); for (Map.Entry entry : writtenLibraries.entrySet()) { LibraryCoordinates coordinates = entry.getValue().getCoordinates(); - ZipEntry zipEntry = (coordinates != null) ? sourceJar.getEntry(REACHABILITY_METADATA_PROPERTIES_LOCATION - .formatted(coordinates.getGroupId(), coordinates.getArtifactId(), coordinates.getVersion())) : null; + ZipEntry zipEntry = (coordinates != null) + ? sourceJar.getEntry(ReachabilityMetadataProperties.getLocation(coordinates)) : null; if (zipEntry != null) { try (InputStream inputStream = sourceJar.getInputStream(zipEntry)) { - Properties properties = new Properties(); - properties.load(inputStream); - if (Boolean.parseBoolean(properties.getProperty("override"))) { + ReachabilityMetadataProperties properties = ReachabilityMetadataProperties + .fromInputStream(inputStream); + if (properties.isOverridden()) { excludes.add(entry.getKey()); } } } } - if (!excludes.isEmpty()) { - List args = new ArrayList<>(); - for (String exclude : excludes) { - int lastSlash = exclude.lastIndexOf('/'); - String jar = (lastSlash != -1) ? exclude.substring(lastSlash + 1) : exclude; - args.add("--exclude-config"); - args.add(Pattern.quote(jar)); - args.add("^/META-INF/native-image/.*"); - } - String contents = args.stream().collect(Collectors.joining("\n")) + "\n"; - writer.writeEntry("META-INF/native-image/argfile", + NativeImageArgFile argFile = new NativeImageArgFile(excludes); + argFile.writeIfNecessary((lines) -> { + String contents = String.join("\n", lines) + "\n"; + writer.writeEntry(NativeImageArgFile.LOCATION, new ByteArrayInputStream(contents.getBytes(StandardCharsets.UTF_8))); - } + }); } private void writeLayerIndex(AbstractJarWriter writer) throws IOException { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/ReachabilityMetadataProperties.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/ReachabilityMetadataProperties.java new file mode 100644 index 0000000000..082bbe9043 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/ReachabilityMetadataProperties.java @@ -0,0 +1,75 @@ +/* + * Copyright 2012-2022 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.loader.tools; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +/** + * Class to work with {@code reachability-metadata.properties}. + * + * @author Moritz Halbritter + * @since 3.0.0 + */ +public final class ReachabilityMetadataProperties { + + /** + * Location of the properties file. Must be formatted using + * {@link String#format(String, Object...)} with the group id, artifact id and version + * of the dependency. + */ + public static final String REACHABILITY_METADATA_PROPERTIES_LOCATION_TEMPLATE = "META-INF/native-image/%s/%s/%s/reachability-metadata.properties"; + + private final Properties properties; + + private ReachabilityMetadataProperties(Properties properties) { + this.properties = properties; + } + + /** + * Returns if the dependency has been overridden. + * @return true if the dependency has been overridden + */ + public boolean isOverridden() { + return Boolean.parseBoolean(this.properties.getProperty("override")); + } + + /** + * Constructs a new instance from the given {@code InputStream}. + * @param inputStream {@code InputStream} to load the properties from + * @return loaded properties + * @throws IOException if loading from the {@code InputStream} went wrong + */ + public static ReachabilityMetadataProperties fromInputStream(InputStream inputStream) throws IOException { + Properties properties = new Properties(); + properties.load(inputStream); + return new ReachabilityMetadataProperties(properties); + } + + /** + * Returns the location of the properties for the given coordinates. + * @param coordinates library coordinates for which the property file location should + * be returned + * @return location of the properties + */ + public static String getLocation(LibraryCoordinates coordinates) { + return REACHABILITY_METADATA_PROPERTIES_LOCATION_TEMPLATE.formatted(coordinates.getGroupId(), + coordinates.getArtifactId(), coordinates.getVersion()); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/NativeImageArgFileTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/NativeImageArgFileTests.java new file mode 100644 index 0000000000..13aaadccda --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/NativeImageArgFileTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2022 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.loader.tools; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * Tests for @{link NativeImageArgFile}. + * + * @author Moritz Halbritter + */ +class NativeImageArgFileTests { + + @Test + void writeIfNecessaryWhenHasExcludesWritesLines() { + NativeImageArgFile argFile = new NativeImageArgFile(List.of("path/to/dependency-1.jar", "dependency-2.jar")); + List lines = new ArrayList<>(); + argFile.writeIfNecessary(lines::addAll); + assertThat(lines).containsExactly("--exclude-config", "\\Qdependency-1.jar\\E", "^/META-INF/native-image/.*", + "--exclude-config", "\\Qdependency-2.jar\\E", "^/META-INF/native-image/.*"); + } + + @Test + void writeIfNecessaryWhenHasNothingDoesNotCallConsumer() { + NativeImageArgFile argFile = new NativeImageArgFile(Collections.emptyList()); + argFile.writeIfNecessary((lines) -> fail("Should not be called")); + } + +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ReachabilityMetadataPropertiesTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ReachabilityMetadataPropertiesTests.java new file mode 100644 index 0000000000..f7e4b16dba --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/ReachabilityMetadataPropertiesTests.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2022 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.loader.tools; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ReachabilityMetadataProperties}. + * + * @author Moritz Halbritter + */ +class ReachabilityMetadataPropertiesTests { + + @Test + void shouldReadFromInputStream() throws IOException { + String propertiesContent = "override=true\n"; + ReachabilityMetadataProperties properties = ReachabilityMetadataProperties + .fromInputStream(new ByteArrayInputStream(propertiesContent.getBytes(StandardCharsets.UTF_8))); + assertThat(properties.isOverridden()).isTrue(); + } + + @Test + void shouldFormatLocation() { + String location = ReachabilityMetadataProperties + .getLocation(LibraryCoordinates.of("group-id", "artifact-id", "1.0.0")); + assertThat(location) + .isEqualTo("META-INF/native-image/group-id/artifact-id/1.0.0/reachability-metadata.properties"); + } + +}