Browse Source

Implement extract and list-layers command

Adds a new jarmode called 'tools'. This provides two commands,
'extract' and 'list-layers'. list-layers is the same as list from
the layertools.

extract is able to extract the JAR in four different modes:

- CDS compatible extraction with libraries in a lib folder and a runner
.jar
- CDS compatible as above, but with layers
- Launcher based
- Launcher based with layers. This is essentially the same as extract
  from the layertools

The commands in layertools have been deprecated in favor of the commands
in 'tools'.

This also changes the behavior of layers.enabled from the Gradle and
Maven plugin: before this commit, layers.enabled prevents the inclusion
of the layer index file as well as the layertools JAR.
After this commit, layers.enabled only prevents the inclusion of the
layer index file.

layer.includeLayerTools have been deprecated in favor of includeTools,
and the layertools JAR has been renamed to tools.

Closes gh-38276
pull/39856/head
Moritz Halbritter 2 years ago
parent
commit
793aca60d2
  1. 2
      settings.gradle
  2. 2
      spring-boot-project/spring-boot-dependencies/build.gradle
  3. 12
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java
  4. 6
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java
  5. 21
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java
  6. 21
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java
  7. 10
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java
  8. 4
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java
  9. 23
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java
  10. 32
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java
  11. 6
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java
  12. 8
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle
  13. 4
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchive.gradle
  14. 4
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle
  15. 4
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle
  16. 8
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle
  17. 4
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-jarTypeFilteringIsApplied.gradle
  18. 8
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-layersWithCustomSourceSet.gradle
  19. 8
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle
  20. 8
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle
  21. 9
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle
  22. 4
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle
  23. 4
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle
  24. 8
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle
  25. 4
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle
  26. 4
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle
  27. 8
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle
  28. 4
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle
  29. 8
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle
  30. 8
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle
  31. 8
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle
  32. 10
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle
  33. 4
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle
  34. 4
      spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle
  35. 119
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java
  36. 116
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/LayerToolsJarMode.java
  37. 3
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/resources/META-INF/spring.factories
  38. 103
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java
  39. 7
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-extract-output.txt
  40. 2
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/build.gradle
  41. 93
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Command.java
  42. 4
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Context.java
  43. 436
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java
  44. 69
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractLayersCommand.java
  45. 64
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/HelpCommand.java
  46. 159
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java
  47. 25
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java
  48. 78
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/JarStructure.java
  49. 54
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/LayerToolsJarMode.java
  50. 33
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java
  51. 27
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListCommand.java
  52. 59
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListLayersCommand.java
  53. 4
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/MissingValueException.java
  54. 94
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Runner.java
  55. 64
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ToolsJarMode.java
  56. 4
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/UnknownOptionException.java
  57. 4
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/package-info.java
  58. 4
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/resources/META-INF/spring.factories
  59. 154
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/AbstractTests.java
  60. 27
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/CommandTests.java
  61. 4
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ContextTests.java
  62. 303
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java
  63. 35
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java
  64. 66
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/HelpCommandTests.java
  65. 173
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java
  66. 18
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java
  67. 20
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/LayerToolsJarModeTests.java
  68. 18
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListCommandTests.java
  69. 51
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListLayersCommandTests.java
  70. 40
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestCommand.java
  71. 6
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestPrintStream.java
  72. 102
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ToolsJarModeTests.java
  73. 0
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/JarLauncher
  74. 1
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/application.properties
  75. 3
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/classpath.idx
  76. 0
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-1
  77. 0
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-2
  78. 0
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-3-SNAPSHOT
  79. 12
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/layers.idx
  80. 2
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/ExtractCommand-printErrorIfLayersAreNotEnabled.txt
  81. 6
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-output.txt
  82. 8
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-test-output.txt
  83. 3
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt
  84. 2
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt
  85. 2
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt
  86. 3
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt
  87. 5
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-list-output.txt
  88. 2
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output-layers-disabled.txt
  89. 4
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output.txt
  90. 0
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-output-without-deprecation.txt
  91. 0
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-layers.idx
  92. 0
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-manifest.MF
  93. 0
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-war-layers.idx
  94. 0
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-war-manifest.MF
  95. 9
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-command-unknown-output.txt
  96. 13
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-missing-value-output.txt
  97. 13
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-unknown-output.txt
  98. 11
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-extract-output.txt
  99. 4
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-help-output.txt
  100. 4
      spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-list-layers-output.txt
  101. Some files were not shown because too many files have changed in this diff Show More

2
settings.gradle

@ -57,7 +57,7 @@ include "spring-boot-project:spring-boot-tools:spring-boot-configuration-metadat @@ -57,7 +57,7 @@ include "spring-boot-project:spring-boot-tools:spring-boot-configuration-metadat
include "spring-boot-project:spring-boot-tools:spring-boot-configuration-processor"
include "spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin"
include "spring-boot-project:spring-boot-tools:spring-boot-gradle-test-support"
include "spring-boot-project:spring-boot-tools:spring-boot-jarmode-layertools"
include "spring-boot-project:spring-boot-tools:spring-boot-jarmode-tools"
include "spring-boot-project:spring-boot-tools:spring-boot-loader"
include "spring-boot-project:spring-boot-tools:spring-boot-loader-classic"
include "spring-boot-project:spring-boot-tools:spring-boot-loader-tools"

2
spring-boot-project/spring-boot-dependencies/build.gradle

@ -1612,7 +1612,7 @@ bom { @@ -1612,7 +1612,7 @@ bom {
"spring-boot-configuration-processor",
"spring-boot-devtools",
"spring-boot-docker-compose",
"spring-boot-jarmode-layertools",
"spring-boot-jarmode-tools",
"spring-boot-loader",
"spring-boot-loader-classic",
"spring-boot-loader-tools",

12
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchive.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -39,6 +39,7 @@ import org.springframework.boot.loader.tools.LoaderImplementation; @@ -39,6 +39,7 @@ import org.springframework.boot.loader.tools.LoaderImplementation;
* A Spring Boot "fat" archive task.
*
* @author Andy Wilkinson
* @author Moritz Halbritter
* @since 2.0.0
*/
public interface BootArchive extends Task {
@ -144,4 +145,13 @@ public interface BootArchive extends Task { @@ -144,4 +145,13 @@ public interface BootArchive extends Task {
@Optional
Property<LoaderImplementation> getLoaderImplementation();
/**
* Returns whether the JAR tools should be included as a dependency in the layered
* archive.
* @return whether the JAR tools should be included
* @since 3.3.0
*/
@Input
Property<Boolean> getIncludeTools();
}

6
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootArchiveSupport.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -129,7 +129,7 @@ class BootArchiveSupport { @@ -129,7 +129,7 @@ class BootArchiveSupport {
CopyAction createCopyAction(Jar jar, ResolvedDependencies resolvedDependencies,
LoaderImplementation loaderImplementation, boolean supportsSignatureFile, LayerResolver layerResolver,
String layerToolsLocation) {
String jarmodeToolsLocation) {
File output = jar.getArchiveFile().get().getAsFile();
Manifest manifest = jar.getManifest();
boolean preserveFileTimestamps = jar.isPreserveFileTimestamps();
@ -143,7 +143,7 @@ class BootArchiveSupport { @@ -143,7 +143,7 @@ class BootArchiveSupport {
Function<FileCopyDetails, ZipCompression> compressionResolver = this.compressionResolver;
String encoding = jar.getMetadataCharset();
CopyAction action = new BootZipCopyAction(output, manifest, preserveFileTimestamps, dirMode, fileMode,
includeDefaultLoader, layerToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec,
includeDefaultLoader, jarmodeToolsLocation, requiresUnpack, exclusions, launchScript, librarySpec,
compressionResolver, encoding, resolvedDependencies, supportsSignatureFile, layerResolver,
loaderImplementation);
return jar.isReproducibleFileOrder() ? new ReproducibleOrderingCopyAction(action) : action;

21
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootJar.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -88,6 +88,7 @@ public abstract class BootJar extends Jar implements BootArchive { @@ -88,6 +88,7 @@ public abstract class BootJar extends Jar implements BootArchive {
this.projectName = project.provider(project::getName);
this.projectVersion = project.provider(project::getVersion);
this.resolvedDependencies = new ResolvedDependencies(project);
getIncludeTools().convention(true);
}
private void configureBootInfSpec(CopySpec bootInfSpec) {
@ -144,13 +145,21 @@ public abstract class BootJar extends Jar implements BootArchive { @@ -144,13 +145,21 @@ public abstract class BootJar extends Jar implements BootArchive {
@Override
protected CopyAction createCopyAction() {
LoaderImplementation loaderImplementation = getLoaderImplementation().getOrElse(LoaderImplementation.DEFAULT);
LayerResolver layerResolver = null;
if (!isLayeredDisabled()) {
LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
String layerToolsLocation = this.layered.getIncludeLayerTools().get() ? LIB_DIRECTORY : null;
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, true,
layerResolver, layerToolsLocation);
layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
}
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, true);
String jarmodeToolsLocation = isIncludeJarmodeTools() ? LIB_DIRECTORY : null;
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, true, layerResolver,
jarmodeToolsLocation);
}
@SuppressWarnings("removal")
private boolean isIncludeJarmodeTools() {
if (!this.getIncludeTools().get()) {
return false;
}
return this.layered.getIncludeLayerTools().get();
}
@Override

21
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootWar.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -87,6 +87,7 @@ public abstract class BootWar extends War implements BootArchive { @@ -87,6 +87,7 @@ public abstract class BootWar extends War implements BootArchive {
this.projectName = project.provider(project::getName);
this.projectVersion = project.provider(project::getVersion);
this.resolvedDependencies = new ResolvedDependencies(project);
getIncludeTools().convention(true);
}
private Object getProvidedLibFiles() {
@ -118,13 +119,21 @@ public abstract class BootWar extends War implements BootArchive { @@ -118,13 +119,21 @@ public abstract class BootWar extends War implements BootArchive {
@Override
protected CopyAction createCopyAction() {
LoaderImplementation loaderImplementation = getLoaderImplementation().getOrElse(LoaderImplementation.DEFAULT);
LayerResolver layerResolver = null;
if (!isLayeredDisabled()) {
LayerResolver layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
String layerToolsLocation = this.layered.getIncludeLayerTools().get() ? LIB_DIRECTORY : null;
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, false,
layerResolver, layerToolsLocation);
layerResolver = new LayerResolver(this.resolvedDependencies, this.layered, this::isLibrary);
}
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, false);
String jarmodeToolsLocation = isIncludeJarmodeTools() ? LIB_DIRECTORY : null;
return this.support.createCopyAction(this, this.resolvedDependencies, loaderImplementation, false,
layerResolver, jarmodeToolsLocation);
}
@SuppressWarnings("removal")
private boolean isIncludeJarmodeTools() {
if (!this.getIncludeTools().get()) {
return false;
}
return this.layered.getIncludeLayerTools().get();
}
@Override

10
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/BootZipCopyAction.java

@ -96,7 +96,7 @@ class BootZipCopyAction implements CopyAction { @@ -96,7 +96,7 @@ class BootZipCopyAction implements CopyAction {
private final boolean includeDefaultLoader;
private final String layerToolsLocation;
private final String jarmodeToolsLocation;
private final Spec<FileTreeElement> requiresUnpack;
@ -119,7 +119,7 @@ class BootZipCopyAction implements CopyAction { @@ -119,7 +119,7 @@ class BootZipCopyAction implements CopyAction {
private final LoaderImplementation loaderImplementation;
BootZipCopyAction(File output, Manifest manifest, boolean preserveFileTimestamps, Integer dirMode, Integer fileMode,
boolean includeDefaultLoader, String layerToolsLocation, Spec<FileTreeElement> requiresUnpack,
boolean includeDefaultLoader, String jarmodeToolsLocation, Spec<FileTreeElement> requiresUnpack,
Spec<FileTreeElement> exclusions, LaunchScriptConfiguration launchScript, Spec<FileCopyDetails> librarySpec,
Function<FileCopyDetails, ZipCompression> compressionResolver, String encoding,
ResolvedDependencies resolvedDependencies, boolean supportsSignatureFile, LayerResolver layerResolver,
@ -130,7 +130,7 @@ class BootZipCopyAction implements CopyAction { @@ -130,7 +130,7 @@ class BootZipCopyAction implements CopyAction {
this.dirMode = dirMode;
this.fileMode = fileMode;
this.includeDefaultLoader = includeDefaultLoader;
this.layerToolsLocation = layerToolsLocation;
this.jarmodeToolsLocation = jarmodeToolsLocation;
this.requiresUnpack = requiresUnpack;
this.exclusions = exclusions;
this.launchScript = launchScript;
@ -342,8 +342,8 @@ class BootZipCopyAction implements CopyAction { @@ -342,8 +342,8 @@ class BootZipCopyAction implements CopyAction {
}
private void writeJarToolsIfNecessary() throws IOException {
if (BootZipCopyAction.this.layerToolsLocation != null) {
writeJarModeLibrary(BootZipCopyAction.this.layerToolsLocation, JarModeLibrary.LAYER_TOOLS);
if (BootZipCopyAction.this.jarmodeToolsLocation != null) {
writeJarModeLibrary(BootZipCopyAction.this.jarmodeToolsLocation, JarModeLibrary.TOOLS);
}
}

4
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/tasks/bundling/LayeredSpec.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -72,8 +72,10 @@ public abstract class LayeredSpec { @@ -72,8 +72,10 @@ public abstract class LayeredSpec {
* archive.
* @return whether the layer tools should be included
* @since 3.0.0
* @deprecated since 3.3.0 for removal in 3.5.0 in favor of {@code includeTools}.
*/
@Input
@Deprecated(since = "3.3.0", forRemoval = true)
public abstract Property<Boolean> getIncludeLayerTools();
/**

23
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -66,6 +66,7 @@ import static org.assertj.core.api.Assertions.assertThat; @@ -66,6 +66,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Andy Wilkinson
* @author Madhura Bhave
* @author Scott Frederick
* @author Moritz Halbritter
*/
abstract class AbstractBootArchiveIntegrationTests {
@ -332,6 +333,18 @@ abstract class AbstractBootArchiveIntegrationTests { @@ -332,6 +333,18 @@ abstract class AbstractBootArchiveIntegrationTests {
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
}
@TestTemplate
void notUpToDateWhenBuiltWithToolsAndThenWithoutTools() {
assertThat(this.gradleBuild.scriptProperty("includeTools", "")
.build(this.taskName)
.task(":" + this.taskName)
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
assertThat(this.gradleBuild.scriptProperty("includeTools", "includeTools = false")
.build(this.taskName)
.task(":" + this.taskName)
.getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
}
@TestTemplate
void layersWithCustomSourceSet() {
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome())
@ -345,7 +358,7 @@ abstract class AbstractBootArchiveIntegrationTests { @@ -345,7 +358,7 @@ abstract class AbstractBootArchiveIntegrationTests {
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome())
.isEqualTo(TaskOutcome.SUCCESS);
Map<String, List<String>> indexedLayers;
String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName();
String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName();
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
assertThat(jarFile.getEntry(layerToolsJar)).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull();
@ -397,7 +410,7 @@ abstract class AbstractBootArchiveIntegrationTests { @@ -397,7 +410,7 @@ abstract class AbstractBootArchiveIntegrationTests {
assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome())
.isEqualTo(TaskOutcome.SUCCESS);
Map<String, List<String>> indexedLayers;
String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName();
String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName();
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
assertThat(jarFile.getEntry(layerToolsJar)).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "alpha-1.2.3.jar")).isNotNull();
@ -443,7 +456,7 @@ abstract class AbstractBootArchiveIntegrationTests { @@ -443,7 +456,7 @@ abstract class AbstractBootArchiveIntegrationTests {
BuildResult build = this.gradleBuild.build(this.taskName);
assertThat(build.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
Map<String, List<String>> indexedLayers;
String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName();
String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName();
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
assertThat(jarFile.getEntry(layerToolsJar)).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "commons-lang3-3.9.jar")).isNotNull();
@ -490,7 +503,7 @@ abstract class AbstractBootArchiveIntegrationTests { @@ -490,7 +503,7 @@ abstract class AbstractBootArchiveIntegrationTests {
BuildResult build = this.gradleBuild.build(this.taskName);
assertThat(build.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS);
Map<String, List<String>> indexedLayers;
String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName();
String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName();
try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) {
assertThat(jarFile.getEntry(layerToolsJar)).isNotNull();
assertThat(jarFile.getEntry(this.libPath + "alpha-1.2.3.jar")).isNotNull();

32
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -80,6 +80,7 @@ import static org.mockito.Mockito.mock; @@ -80,6 +80,7 @@ import static org.mockito.Mockito.mock;
* @param <T> the type of the concrete BootArchive implementation
* @author Andy Wilkinson
* @author Scott Frederick
* @author Moritz Halbritter
*/
abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
@ -496,7 +497,7 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> { @@ -496,7 +497,7 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Lib")).isEqualTo(this.libPath);
assertThat(jarFile.getManifest().getMainAttributes().getValue("Spring-Boot-Layers-Index"))
.isEqualTo(this.indexPath + "layers.idx");
assertThat(getEntryNames(jarFile)).contains(this.libPath + JarModeLibrary.LAYER_TOOLS.getName());
assertThat(getEntryNames(jarFile)).contains(this.libPath + JarModeLibrary.TOOLS.getName());
}
}
@ -530,7 +531,7 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> { @@ -530,7 +531,7 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
List<String> index = entryLines(jarFile, this.indexPath + "layers.idx");
assertThat(getLayerNames(index)).containsExactly("dependencies", "spring-boot-loader",
"snapshot-dependencies", "application");
String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName();
String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName();
List<String> expected = new ArrayList<>();
expected.add("- \"dependencies\":");
expected.add(" - \"" + this.libPath + "first-library.jar\"");
@ -584,7 +585,7 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> { @@ -584,7 +585,7 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
List<String> index = entryLines(jarFile, this.indexPath + "layers.idx");
assertThat(getLayerNames(index)).containsExactly("my-deps", "my-internal-deps", "my-snapshot-deps",
"resources", "application");
String layerToolsJar = this.libPath + JarModeLibrary.LAYER_TOOLS.getName();
String layerToolsJar = this.libPath + JarModeLibrary.TOOLS.getName();
List<String> expected = new ArrayList<>();
expected.add("- \"my-deps\":");
expected.add(" - \"" + layerToolsJar + "\"");
@ -614,15 +615,32 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> { @@ -614,15 +615,32 @@ abstract class AbstractBootArchiveTests<T extends Jar & BootArchive> {
@Test
void whenArchiveIsLayeredThenLayerToolsAreAddedToTheJar() throws IOException {
List<String> entryNames = getEntryNames(createLayeredJar());
assertThat(entryNames).contains(this.libPath + JarModeLibrary.LAYER_TOOLS.getName());
assertThat(entryNames).contains(this.libPath + JarModeLibrary.TOOLS.getName());
}
@Test
void shouldAddToolsToTheJar() throws IOException {
this.task.getMainClass().set("com.example.Main");
executeTask();
List<String> entryNames = getEntryNames(this.task.getArchiveFile().get().getAsFile());
assertThat(entryNames).isNotEmpty().contains(this.libPath + JarModeLibrary.TOOLS.getName());
}
@Test
@SuppressWarnings("removal")
void whenArchiveIsLayeredAndIncludeLayerToolsIsFalseThenLayerToolsAreNotAddedToTheJar() throws IOException {
List<String> entryNames = getEntryNames(
createLayeredJar((configuration) -> configuration.getIncludeLayerTools().set(false)));
assertThat(entryNames).isNotEmpty()
.doesNotContain(this.indexPath + "layers/dependencies/lib/spring-boot-jarmode-layertools.jar");
assertThat(entryNames).isNotEmpty().doesNotContain(this.libPath + JarModeLibrary.TOOLS.getName());
}
@Test
void whenIncludeToolsIsFalseThenToolsAreNotAddedToTheJar() throws IOException {
this.task.getIncludeTools().set(false);
this.task.getMainClass().set("com.example.Main");
executeTask();
List<String> entryNames = getEntryNames(this.task.getArchiveFile().get().getAsFile());
assertThat(entryNames).isNotEmpty().doesNotContain(this.libPath + JarModeLibrary.TOOLS.getName());
}
protected File jarFile(String name) throws IOException {

6
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -67,7 +67,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { @@ -67,7 +67,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
assertThat(output).containsPattern("1\\. .*classes");
assertThat(output).containsPattern("2\\. .*library-1.0-SNAPSHOT.jar");
assertThat(output).containsPattern("3\\. .*commons-lang3-3.9.jar");
assertThat(output).containsPattern("4\\. .*spring-boot-jarmode-layertools.*.jar");
assertThat(output).containsPattern("4\\. .*spring-boot-jarmode-tools.*.jar");
assertThat(output).doesNotContain("5. ");
}
@ -77,7 +77,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests { @@ -77,7 +77,7 @@ class BootJarIntegrationTests extends AbstractBootArchiveIntegrationTests {
BuildResult result = this.gradleBuild.build("launch");
String output = result.getOutput();
assertThat(output).containsPattern("1\\. .*classes");
assertThat(output).containsPattern("2\\. .*spring-boot-jarmode-layertools.*.jar");
assertThat(output).containsPattern("2\\. .*spring-boot-jarmode-tools.*.jar");
assertThat(output).containsPattern("3\\. .*library-1.0-SNAPSHOT.jar");
assertThat(output).containsPattern("4\\. .*commons-lang3-3.9.jar");
assertThat(output).doesNotContain("5. ");

8
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-customLayers.gradle

@ -38,12 +38,12 @@ dependencies { @@ -38,12 +38,12 @@ dependencies {
task listLayers(type: JavaExec) {
classpath = bootJar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
systemProperties = [ "jarmode": "tools" ]
args "list-layers"
}
task extractLayers(type: JavaExec) {
classpath = bootJar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
systemProperties = [ "jarmode": "tools" ]
args "extract", "--layers", "--launcher"
}

4
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchive.gradle

@ -17,7 +17,5 @@ dependencies { @@ -17,7 +17,5 @@ dependencies {
}
bootJar {
layered {
enabled = false
}
includeTools = false
}

4
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle

@ -18,7 +18,5 @@ dependencies { @@ -18,7 +18,5 @@ dependencies {
}
bootJar {
layered {
enabled = false
}
includeTools = false
}

4
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle

@ -21,7 +21,5 @@ bootJar { @@ -21,7 +21,5 @@ bootJar {
}
bootJar {
layered {
enabled = false
}
includeTools = false
}

8
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-implicitLayers.gradle

@ -21,12 +21,12 @@ dependencies { @@ -21,12 +21,12 @@ dependencies {
task listLayers(type: JavaExec) {
classpath = bootJar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
systemProperties = [ "jarmode": "tools" ]
args "list-layers"
}
task extractLayers(type: JavaExec) {
classpath = bootJar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
systemProperties = [ "jarmode": "tools" ]
args "extract", "--layers", "--launcher"
}

4
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-jarTypeFilteringIsApplied.gradle

@ -19,7 +19,5 @@ dependencies { @@ -19,7 +19,5 @@ dependencies {
}
bootJar {
layered {
enabled = false
}
includeTools = false
}

8
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-layersWithCustomSourceSet.gradle

@ -24,12 +24,12 @@ dependencies { @@ -24,12 +24,12 @@ dependencies {
task listLayers(type: JavaExec) {
classpath = bootJar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
systemProperties = [ "jarmode": "tools" ]
args "list-layers"
}
task extractLayers(type: JavaExec) {
classpath = bootJar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
systemProperties = [ "jarmode": "tools" ]
args "extract", "--layers", "--launcher"
}

8
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleCustomLayers.gradle

@ -55,12 +55,12 @@ dependencies { @@ -55,12 +55,12 @@ dependencies {
task listLayers(type: JavaExec) {
classpath = bootJar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
systemProperties = [ "jarmode": "tools" ]
args "list-layers"
}
task extractLayers(type: JavaExec) {
classpath = bootJar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
systemProperties = [ "jarmode": "tools" ]
args "extract", "--layers", "--launcher"
}

8
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-multiModuleImplicitLayers.gradle

@ -33,12 +33,12 @@ dependencies { @@ -33,12 +33,12 @@ dependencies {
task listLayers(type: JavaExec) {
classpath = bootJar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
systemProperties = [ "jarmode": "tools" ]
args "list-layers"
}
task extractLayers(type: JavaExec) {
classpath = bootJar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
systemProperties = [ "jarmode": "tools" ]
args "extract", "--layers", "--launcher"
}

9
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
}
bootJar {
mainClass = 'com.example.Application'
{includeTools}
}

4
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle

@ -18,7 +18,5 @@ dependencies { @@ -18,7 +18,5 @@ dependencies {
}
bootJar {
layered {
enabled = false
}
includeTools = false
}

4
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle

@ -21,7 +21,5 @@ bootJar { @@ -21,7 +21,5 @@ bootJar {
}
bootJar {
layered {
enabled = false
}
includeTools = false
}

8
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-customLayers.gradle

@ -39,12 +39,12 @@ dependencies { @@ -39,12 +39,12 @@ dependencies {
task listLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
systemProperties = [ "jarmode": "tools" ]
args "list-layers"
}
task extractLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
systemProperties = [ "jarmode": "tools" ]
args "extract", "--layers", "--launcher"
}

4
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle

@ -18,7 +18,5 @@ dependencies { @@ -18,7 +18,5 @@ dependencies {
}
bootWar {
layered {
enabled = false
}
includeTools = false
}

4
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-developmentOnlyDependenciesCanBeIncludedInTheArchive.gradle

@ -21,7 +21,5 @@ bootWar { @@ -21,7 +21,5 @@ bootWar {
}
bootWar {
layered {
enabled = false
}
includeTools = false
}

8
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-implicitLayers.gradle

@ -22,12 +22,12 @@ dependencies { @@ -22,12 +22,12 @@ dependencies {
task listLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
systemProperties = [ "jarmode": "tools" ]
args "list-layers"
}
task extractLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
systemProperties = [ "jarmode": "tools" ]
args "extract", "--layers", "--launcher"
}

4
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-jarTypeFilteringIsApplied.gradle

@ -19,7 +19,5 @@ dependencies { @@ -19,7 +19,5 @@ dependencies {
}
bootWar {
layered {
enabled = false
}
includeTools = false
}

8
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-layersWithCustomSourceSet.gradle

@ -25,12 +25,12 @@ dependencies { @@ -25,12 +25,12 @@ dependencies {
task listLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
systemProperties = [ "jarmode": "tools" ]
args "list-layers"
}
task extractLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
systemProperties = [ "jarmode": "tools" ]
args "extract", "--layers", "--launcher"
}

8
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleCustomLayers.gradle

@ -56,12 +56,12 @@ dependencies { @@ -56,12 +56,12 @@ dependencies {
task listLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
systemProperties = [ "jarmode": "tools" ]
args "list-layers"
}
task extractLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
systemProperties = [ "jarmode": "tools" ]
args "extract", "--layers", "--launcher"
}

8
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-multiModuleImplicitLayers.gradle

@ -34,12 +34,12 @@ dependencies { @@ -34,12 +34,12 @@ dependencies {
task listLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "list"
systemProperties = [ "jarmode": "tools" ]
args "list-layers"
}
task extractLayers(type: JavaExec) {
classpath = bootWar.outputs.files
systemProperties = [ "jarmode": "layertools" ]
args "extract"
systemProperties = [ "jarmode": "tools" ]
args "extract", "--layers", "--launcher"
}

10
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-notUpToDateWhenBuiltWithToolsAndThenWithoutTools.gradle

@ -0,0 +1,10 @@ @@ -0,0 +1,10 @@
plugins {
id 'java'
id 'org.springframework.boot' version '{version}'
id 'war'
}
bootWar {
mainClass = 'com.example.Application'
{includeTools}
}

4
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault.gradle

@ -18,7 +18,5 @@ dependencies { @@ -18,7 +18,5 @@ dependencies {
}
bootWar {
layered {
enabled = false
}
includeTools = false
}

4
spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-testAndDevelopmentOnlyDependenciesCanBeIncludedInTheArchive.gradle

@ -21,7 +21,5 @@ bootWar { @@ -21,7 +21,5 @@ bootWar {
}
bootWar {
layered {
enabled = false
}
includeTools = false
}

119
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ExtractCommand.java

@ -1,119 +0,0 @@ @@ -1,119 +0,0 @@
/*
* Copyright 2012-2023 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.jarmode.layertools;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributeView;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
/**
* The {@code 'extract'} tools command.
*
* @author Phillip Webb
*/
class ExtractCommand extends Command {
static final Option DESTINATION_OPTION = Option.of("destination", "string", "The destination to extract files to");
private final Context context;
private final Layers layers;
ExtractCommand(Context context) {
this(context, Layers.get(context));
}
ExtractCommand(Context context, Layers layers) {
super("extract", "Extracts layers from the jar for image creation", Options.of(DESTINATION_OPTION),
Parameters.of("[<layer>...]"));
this.context = context;
this.layers = layers;
}
@Override
protected void run(Map<Option, String> options, List<String> parameters) {
try {
File destination = options.containsKey(DESTINATION_OPTION) ? new File(options.get(DESTINATION_OPTION))
: this.context.getWorkingDir();
for (String layer : this.layers) {
if (parameters.isEmpty() || parameters.contains(layer)) {
mkDirs(new File(destination, layer));
}
}
try (ZipInputStream zip = new ZipInputStream(new FileInputStream(this.context.getArchiveFile()))) {
ZipEntry entry = zip.getNextEntry();
Assert.state(entry != null, "File '" + this.context.getArchiveFile().toString()
+ "' is not compatible with layertools; ensure jar file is valid and launch script is not enabled");
while (entry != null) {
if (!entry.isDirectory()) {
String layer = this.layers.getLayer(entry);
if (parameters.isEmpty() || parameters.contains(layer)) {
write(zip, entry, new File(destination, layer));
}
}
entry = zip.getNextEntry();
}
}
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private void write(ZipInputStream zip, ZipEntry entry, File destination) throws IOException {
String canonicalOutputPath = destination.getCanonicalPath() + File.separator;
File file = new File(destination, entry.getName());
String canonicalEntryPath = file.getCanonicalPath();
Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath),
() -> "Entry '" + entry.getName() + "' would be written to '" + canonicalEntryPath
+ "'. This is outside the output location of '" + canonicalOutputPath
+ "'. Verify the contents of your archive.");
mkParentDirs(file);
try (OutputStream out = new FileOutputStream(file)) {
StreamUtils.copy(zip, out);
}
try {
Files.getFileAttributeView(file.toPath(), BasicFileAttributeView.class)
.setTimes(entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime());
}
catch (IOException ex) {
// File system does not support setting time attributes. Continue.
}
}
private void mkParentDirs(File file) throws IOException {
mkDirs(file.getParentFile());
}
private void mkDirs(File file) throws IOException {
if (!file.exists() && !file.mkdirs()) {
throw new IOException("Unable to create directory " + file);
}
}
}

116
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/LayerToolsJarMode.java

@ -1,116 +0,0 @@ @@ -1,116 +0,0 @@
/*
* 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.jarmode.layertools;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import org.springframework.boot.loader.jarmode.JarMode;
/**
* {@link JarMode} providing {@code "layertools"} support.
*
* @author Phillip Webb
* @author Scott Frederick
* @since 2.3.0
*/
public class LayerToolsJarMode implements JarMode {
@Override
public boolean accepts(String mode) {
return "layertools".equalsIgnoreCase(mode);
}
@Override
public void run(String mode, String[] args) {
try {
new Runner().run(args);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
static class Runner {
static Context contextOverride;
private final List<Command> commands;
private final HelpCommand help;
Runner() {
Context context = (contextOverride != null) ? contextOverride : new Context();
this.commands = getCommands(context);
this.help = new HelpCommand(context, this.commands);
}
private void run(String[] args) {
run(dequeOf(args));
}
private void run(Deque<String> args) {
if (!args.isEmpty()) {
String commandName = args.removeFirst();
Command command = Command.find(this.commands, commandName);
if (command != null) {
runCommand(command, args);
return;
}
printError("Unknown command \"" + commandName + "\"");
}
this.help.run(args);
}
private void runCommand(Command command, Deque<String> args) {
try {
command.run(args);
}
catch (UnknownOptionException ex) {
printError("Unknown option \"" + ex.getMessage() + "\" for the " + command.getName() + " command");
this.help.run(dequeOf(command.getName()));
}
catch (MissingValueException ex) {
printError("Option \"" + ex.getMessage() + "\" for the " + command.getName()
+ " command requires a value");
this.help.run(dequeOf(command.getName()));
}
}
private void printError(String errorMessage) {
System.out.println("Error: " + errorMessage);
System.out.println();
}
private Deque<String> dequeOf(String... args) {
return new ArrayDeque<>(Arrays.asList(args));
}
static List<Command> getCommands(Context context) {
List<Command> commands = new ArrayList<>();
commands.add(new ListCommand(context));
commands.add(new ExtractCommand(context));
return Collections.unmodifiableList(commands);
}
}
}

3
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/resources/META-INF/spring.factories

@ -1,3 +0,0 @@ @@ -1,3 +0,0 @@
# Jar Modes
org.springframework.boot.loader.jarmode.JarMode=\
org.springframework.boot.jarmode.layertools.LayerToolsJarMode

103
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/HelpCommandTests.java

@ -1,103 +0,0 @@ @@ -1,103 +0,0 @@
/*
* Copyright 2012-2023 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.jarmode.layertools;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import java.util.jar.JarEntry;
import java.util.zip.ZipOutputStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link HelpCommand}.
*
* @author Phillip Webb
*/
class HelpCommandTests {
private HelpCommand command;
private TestPrintStream out;
@TempDir
File temp;
@BeforeEach
void setup() throws Exception {
Context context = mock(Context.class);
given(context.getArchiveFile()).willReturn(createJarFile("test.jar"));
this.command = new HelpCommand(context, LayerToolsJarMode.Runner.getCommands(context));
this.out = new TestPrintStream(this);
}
@Test
void runWhenHasNoParametersPrintsUsage() {
this.command.run(this.out, Collections.emptyList());
assertThat(this.out).hasSameContentAsResource("help-output.txt");
}
@Test
void runWhenHasNoCommandParameterPrintsUsage() {
this.command.run(this.out, Arrays.asList("extract"));
System.out.println(this.out);
assertThat(this.out).hasSameContentAsResource("help-extract-output.txt");
}
private File createJarFile(String name) throws Exception {
File file = new File(this.temp, name);
try (ZipOutputStream jarOutputStream = new ZipOutputStream(new FileOutputStream(file))) {
jarOutputStream.putNextEntry(new JarEntry("META-INF/MANIFEST.MF"));
jarOutputStream.write(getFile("test-manifest.MF").getBytes());
jarOutputStream.closeEntry();
JarEntry indexEntry = new JarEntry("BOOT-INF/layers.idx");
jarOutputStream.putNextEntry(indexEntry);
Writer writer = new OutputStreamWriter(jarOutputStream, StandardCharsets.UTF_8);
writer.write("- \"0001\":\n");
writer.write(" - \"BOOT-INF/lib/a.jar\"\n");
writer.write(" - \"BOOT-INF/lib/b.jar\"\n");
writer.write("- \"0002\":\n");
writer.write(" - \"BOOT-INF/lib/c.jar\"\n");
writer.write("- \"0003\":\n");
writer.write(" - \"BOOT-INF/lib/d.jar\"\n");
writer.flush();
}
return file;
}
private String getFile(String fileName) throws Exception {
ClassPathResource resource = new ClassPathResource(fileName, getClass());
InputStreamReader reader = new InputStreamReader(resource.getInputStream());
return FileCopyUtils.copyToString(reader);
}
}

7
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-extract-output.txt

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
Extracts layers from the jar for image creation
Usage:
java -Djarmode=layertools -jar test.jar extract [options] [<layer>...]
Options:
--destination string The destination to extract files to

2
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/build.gradle → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/build.gradle

@ -4,7 +4,7 @@ plugins { @@ -4,7 +4,7 @@ plugins {
id "org.springframework.boot.deployed"
}
description = "Spring Boot Layers Tools"
description = "Spring Boot Jarmode Tools"
dependencies {
implementation(project(":spring-boot-project:spring-boot-tools:spring-boot-loader-classic"))

93
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Command.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Command.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2024 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.
@ -14,8 +14,9 @@ @@ -14,8 +14,9 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
@ -24,13 +25,15 @@ import java.util.Deque; @@ -24,13 +25,15 @@ import java.util.Deque;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.stream.Stream;
/**
* A command that can be launched from the layertools jarmode.
* A command that can be launched.
*
* @author Phillip Webb
* @author Scott Frederick
* @author Moritz Halbritter
*/
abstract class Command {
@ -90,9 +93,10 @@ abstract class Command { @@ -90,9 +93,10 @@ abstract class Command {
/**
* Run the command by processing the remaining arguments.
* @param out stream for command output
* @param args a mutable deque of the remaining arguments
*/
final void run(Deque<String> args) {
final void run(PrintStream out, Deque<String> args) {
List<String> parameters = new ArrayList<>();
Map<Option, String> options = new HashMap<>();
while (!args.isEmpty()) {
@ -105,15 +109,32 @@ abstract class Command { @@ -105,15 +109,32 @@ abstract class Command {
parameters.add(arg);
}
}
run(options, parameters);
run(out, options, parameters);
}
/**
* Run the actual command.
* @param out stream for command output
* @param options any options extracted from the arguments
* @param parameters any parameters extracted from the arguments
*/
protected abstract void run(Map<Option, String> options, List<String> parameters);
abstract void run(PrintStream out, Map<Option, String> options, List<String> parameters);
/**
* Whether the command is deprecated.
* @return whether the command is deprecated
*/
boolean isDeprecated() {
return false;
}
/**
* Returns the deprecation message.
* @return the deprecation message
*/
String getDeprecationMessage() {
return null;
}
/**
* Static method that can be used to find a single command from a collection.
@ -133,7 +154,7 @@ abstract class Command { @@ -133,7 +154,7 @@ abstract class Command {
/**
* Parameters that the command accepts.
*/
protected static final class Parameters {
static final class Parameters {
private final List<String> descriptions;
@ -158,7 +179,7 @@ abstract class Command { @@ -158,7 +179,7 @@ abstract class Command {
* Factory method used if there are no expected parameters.
* @return a new {@link Parameters} instance
*/
protected static Parameters none() {
static Parameters none() {
return of();
}
@ -168,7 +189,7 @@ abstract class Command { @@ -168,7 +189,7 @@ abstract class Command {
* @param descriptions the parameter descriptions
* @return a new {@link Parameters} instance with the given descriptions
*/
protected static Parameters of(String... descriptions) {
static Parameters of(String... descriptions) {
return new Parameters(descriptions);
}
@ -177,7 +198,7 @@ abstract class Command { @@ -177,7 +198,7 @@ abstract class Command {
/**
* Options that the command accepts.
*/
protected static final class Options {
static final class Options {
private final Option[] values;
@ -218,7 +239,7 @@ abstract class Command { @@ -218,7 +239,7 @@ abstract class Command {
* Factory method used if there are no expected options.
* @return a new {@link Options} instance
*/
protected static Options none() {
static Options none() {
return of();
}
@ -228,7 +249,7 @@ abstract class Command { @@ -228,7 +249,7 @@ abstract class Command {
* @param values the option values
* @return a new {@link Options} instance with the given values
*/
protected static Options of(Option... values) {
static Options of(Option... values) {
return new Options(values);
}
@ -237,9 +258,9 @@ abstract class Command { @@ -237,9 +258,9 @@ abstract class Command {
/**
* An individual option that the command can accepts. Can either be an option with a
* value (e.g. {@literal --log debug}) or a flag (e.g. {@literal
* --verbose}).
* --verbose}). It also can be both if the value is marked as optional.
*/
protected static final class Option {
static final class Option {
private final String name;
@ -247,10 +268,13 @@ abstract class Command { @@ -247,10 +268,13 @@ abstract class Command {
private final String description;
private Option(String name, String valueDescription, String description) {
private final boolean optionalValue;
private Option(String name, String valueDescription, String description, boolean optionalValue) {
this.name = name;
this.description = description;
this.valueDescription = valueDescription;
this.optionalValue = optionalValue;
}
/**
@ -287,13 +311,24 @@ abstract class Command { @@ -287,13 +311,24 @@ abstract class Command {
}
private String claimArg(Deque<String> args) {
if (this.valueDescription != null) {
if (args.isEmpty()) {
throw new MissingValueException(this.name);
if (this.valueDescription == null) {
return null;
}
if (this.optionalValue) {
String nextArg = args.peek();
if (nextArg == null || nextArg.startsWith("--")) {
return null;
}
return args.removeFirst();
}
return null;
else {
try {
return args.removeFirst();
}
catch (NoSuchElementException ex) {
throw new MissingValueException(this.name);
}
}
}
@Override
@ -323,8 +358,19 @@ abstract class Command { @@ -323,8 +358,19 @@ abstract class Command {
* @param description a description of the option
* @return a new {@link Option} instance
*/
protected static Option flag(String name, String description) {
return new Option(name, null, description);
static Option flag(String name, String description) {
return new Option(name, null, description, false);
}
/**
* Factory method to create value option.
* @param name the name of the option
* @param valueDescription a description of the expected value
* @param description a description of the option
* @return a new {@link Option} instance
*/
static Option of(String name, String valueDescription, String description) {
return new Option(name, valueDescription, description, false);
}
/**
@ -332,10 +378,11 @@ abstract class Command { @@ -332,10 +378,11 @@ abstract class Command {
* @param name the name of the option
* @param valueDescription a description of the expected value
* @param description a description of the option
* @param optionalValue whether the value is optional
* @return a new {@link Option} instance
*/
protected static Option of(String name, String valueDescription, String description) {
return new Option(name, valueDescription, description);
static Option of(String name, String valueDescription, String description, boolean optionalValue) {
return new Option(name, valueDescription, description, optionalValue);
}
}

4
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Context.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Context.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2024 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.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
import java.io.File;
import java.io.IOException;

436
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractCommand.java

@ -0,0 +1,436 @@ @@ -0,0 +1,436 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.FileTime;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.springframework.boot.jarmode.tools.JarStructure.Entry;
import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type;
import org.springframework.boot.jarmode.tools.Layers.LayersNotEnabledException;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
* The {@code 'extract'} tools command.
*
* @author Moritz Halbritter
*/
class ExtractCommand extends Command {
/**
* Option to create a launcher.
*/
static final Option LAUNCHER_OPTION = Option.of("launcher", null, "Whether to extract the Spring Boot launcher");
/**
* Option to extract layers.
*/
static final Option LAYERS_OPTION = Option.of("layers", "string list", "Layers to extract", true);
/**
* Option to specify the destination to write to.
*/
static final Option DESTINATION_OPTION = Option.of("destination", "string",
"Directory to extract files to. Defaults to the current working directory");
private static final Option LIBRARIES_DIRECTORY_OPTION = Option.of("libraries", "string",
"Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/");
private static final Option RUNNER_FILENAME_OPTION = Option.of("runner-filename", "string",
"Name of the runner JAR file. Only applicable when not using --launcher. Defaults to runner.jar");
private final Context context;
private final Layers layers;
ExtractCommand(Context context) {
this(context, null);
}
ExtractCommand(Context context, Layers layers) {
super("extract", "Extract the contents from the jar", Options.of(LAUNCHER_OPTION, LAYERS_OPTION,
DESTINATION_OPTION, LIBRARIES_DIRECTORY_OPTION, RUNNER_FILENAME_OPTION), Parameters.none());
this.context = context;
this.layers = layers;
}
@Override
void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
try {
checkJarCompatibility();
File destination = getWorkingDirectory(options);
FileResolver fileResolver = getFileResolver(destination, options);
fileResolver.createDirectories();
if (options.containsKey(LAUNCHER_OPTION)) {
extractArchive(fileResolver);
}
else {
JarStructure jarStructure = getJarStructure();
extractLibraries(fileResolver, jarStructure, options);
createRunner(jarStructure, fileResolver, options);
}
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
catch (LayersNotEnabledException ex) {
printError(out, "Layers are not enabled");
}
}
private void checkJarCompatibility() throws IOException {
File file = this.context.getArchiveFile();
try (ZipInputStream stream = new ZipInputStream(new FileInputStream(file))) {
ZipEntry entry = stream.getNextEntry();
Assert.state(entry != null,
() -> "File '%s' is not compatible; ensure jar file is valid and launch script is not enabled"
.formatted(file));
}
}
private void printError(PrintStream out, String message) {
out.println("Error: " + message);
out.println();
}
private void extractLibraries(FileResolver fileResolver, JarStructure jarStructure, Map<Option, String> options)
throws IOException {
String librariesDirectory = getLibrariesDirectory(options);
extractArchive(fileResolver, (zipEntry) -> {
Entry entry = jarStructure.resolve(zipEntry);
if (isType(entry, Type.LIBRARY)) {
return librariesDirectory + entry.location();
}
return null;
});
}
private static String getLibrariesDirectory(Map<Option, String> options) {
if (options.containsKey(LIBRARIES_DIRECTORY_OPTION)) {
String value = options.get(LIBRARIES_DIRECTORY_OPTION);
if (value.endsWith("/")) {
return value;
}
return value + "/";
}
return "lib/";
}
private FileResolver getFileResolver(File destination, Map<Option, String> options) {
String runnerFilename = getRunnerFilename(options);
if (!options.containsKey(LAYERS_OPTION)) {
return new NoLayersFileResolver(destination, runnerFilename);
}
Layers layers = getLayers();
Set<String> layersToExtract = StringUtils.commaDelimitedListToSet(options.get(LAYERS_OPTION));
return new LayersFileResolver(destination, layers, layersToExtract, runnerFilename);
}
private File getWorkingDirectory(Map<Option, String> options) {
if (options.containsKey(DESTINATION_OPTION)) {
return new File(options.get(DESTINATION_OPTION));
}
return this.context.getWorkingDir();
}
private JarStructure getJarStructure() {
IndexedJarStructure jarStructure = IndexedJarStructure.get(this.context.getArchiveFile());
Assert.state(jarStructure != null, "Couldn't read classpath index");
return jarStructure;
}
private void extractArchive(FileResolver fileResolver) throws IOException {
extractArchive(fileResolver, ZipEntry::getName);
}
private void extractArchive(FileResolver fileResolver, EntryNameTransformer entryNameTransformer)
throws IOException {
withZipEntries(this.context.getArchiveFile(), (stream, zipEntry) -> {
if (zipEntry.isDirectory()) {
return;
}
String name = entryNameTransformer.getName(zipEntry);
if (name == null) {
return;
}
File file = fileResolver.resolve(zipEntry, name);
if (file != null) {
extractEntry(stream, zipEntry, file);
}
});
}
private Layers getLayers() {
if (this.layers != null) {
return this.layers;
}
return Layers.get(this.context);
}
private void createRunner(JarStructure jarStructure, FileResolver fileResolver, Map<Option, String> options)
throws IOException {
File file = fileResolver.resolveRunner();
if (file == null) {
return;
}
String librariesDirectory = getLibrariesDirectory(options);
Manifest manifest = jarStructure.createLauncherManifest((library) -> librariesDirectory + library);
mkDirs(file.getParentFile());
try (JarOutputStream output = new JarOutputStream(new FileOutputStream(file), manifest)) {
withZipEntries(this.context.getArchiveFile(), ((stream, zipEntry) -> {
Entry entry = jarStructure.resolve(zipEntry);
if (isType(entry, Type.APPLICATION_CLASS_OR_RESOURCE) && StringUtils.hasLength(entry.location())) {
JarEntry jarEntry = createJarEntry(entry.location(), zipEntry);
output.putNextEntry(jarEntry);
StreamUtils.copy(stream, output);
output.closeEntry();
}
}));
}
}
private String getRunnerFilename(Map<Option, String> options) {
if (options.containsKey(RUNNER_FILENAME_OPTION)) {
return options.get(RUNNER_FILENAME_OPTION);
}
return "runner.jar";
}
private static boolean isType(Entry entry, Type type) {
if (entry == null) {
return false;
}
return entry.type() == type;
}
private static void extractEntry(ZipInputStream zip, ZipEntry entry, File file) throws IOException {
mkDirs(file.getParentFile());
try (OutputStream out = new FileOutputStream(file)) {
StreamUtils.copy(zip, out);
}
try {
Files.getFileAttributeView(file.toPath(), BasicFileAttributeView.class)
.setTimes(entry.getLastModifiedTime(), entry.getLastAccessTime(), entry.getCreationTime());
}
catch (IOException ex) {
// File system does not support setting time attributes. Continue.
}
}
private static void mkDirs(File file) throws IOException {
if (!file.exists() && !file.mkdirs()) {
throw new IOException("Unable to create directory " + file);
}
}
private static JarEntry createJarEntry(String location, ZipEntry originalEntry) {
JarEntry jarEntry = new JarEntry(location);
FileTime lastModifiedTime = originalEntry.getLastModifiedTime();
if (lastModifiedTime != null) {
jarEntry.setLastModifiedTime(lastModifiedTime);
}
FileTime lastAccessTime = originalEntry.getLastAccessTime();
if (lastAccessTime != null) {
jarEntry.setLastAccessTime(lastAccessTime);
}
FileTime creationTime = originalEntry.getCreationTime();
if (creationTime != null) {
jarEntry.setCreationTime(creationTime);
}
return jarEntry;
}
private static void withZipEntries(File file, ThrowingConsumer callback) throws IOException {
try (ZipInputStream stream = new ZipInputStream(new FileInputStream(file))) {
ZipEntry entry = stream.getNextEntry();
while (entry != null) {
if (StringUtils.hasLength(entry.getName())) {
callback.accept(stream, entry);
}
entry = stream.getNextEntry();
}
}
}
private static File assertFileIsContainedInDirectory(File directory, File file, String name) throws IOException {
String canonicalOutputPath = directory.getCanonicalPath() + File.separator;
String canonicalEntryPath = file.getCanonicalPath();
Assert.state(canonicalEntryPath.startsWith(canonicalOutputPath),
() -> "Entry '%s' would be written to '%s'. This is outside the output location of '%s'. Verify the contents of your archive."
.formatted(name, canonicalEntryPath, canonicalOutputPath));
return file;
}
@FunctionalInterface
private interface EntryNameTransformer {
String getName(ZipEntry entry);
}
@FunctionalInterface
private interface ThrowingConsumer {
void accept(ZipInputStream stream, ZipEntry entry) throws IOException;
}
private interface FileResolver {
/**
* Creates needed directories.
* @throws IOException if something went wrong
*/
void createDirectories() throws IOException;
/**
* Resolves the given {@link ZipEntry} to a file.
* @param entry the zip entry
* @param newName the new name of the file
* @return file where the contents should be written or {@code null} if this entry
* should be skipped
* @throws IOException if something went wrong
*/
default File resolve(ZipEntry entry, String newName) throws IOException {
return resolve(entry.getName(), newName);
}
/**
* Resolves the given name to a file.
* @param originalName the original name of the file
* @param newName the new name of the file
* @return file where the contents should be written or {@code null} if this name
* should be skipped
* @throws IOException if something went wrong
*/
File resolve(String originalName, String newName) throws IOException;
/**
* Resolves the file for the runner.
* @return the file for the runner or {@code null} if the runner should be skipped
* @throws IOException if something went wrong
*/
File resolveRunner() throws IOException;
}
private static final class NoLayersFileResolver implements FileResolver {
private final File directory;
private final String runnerFilename;
private NoLayersFileResolver(File directory, String runnerFilename) {
this.directory = directory;
this.runnerFilename = runnerFilename;
}
@Override
public void createDirectories() {
}
@Override
public File resolve(String originalName, String newName) throws IOException {
return assertFileIsContainedInDirectory(this.directory, new File(this.directory, newName), newName);
}
@Override
public File resolveRunner() throws IOException {
return resolve(this.runnerFilename, this.runnerFilename);
}
}
private static final class LayersFileResolver implements FileResolver {
private final Layers layers;
private final Set<String> layersToExtract;
private final File directory;
private final String runnerFilename;
LayersFileResolver(File directory, Layers layers, Set<String> layersToExtract, String runnerFilename) {
this.layers = layers;
this.layersToExtract = layersToExtract;
this.directory = directory;
this.runnerFilename = runnerFilename;
}
@Override
public void createDirectories() throws IOException {
for (String layer : this.layers) {
if (shouldExtractLayer(layer)) {
mkDirs(getLayerDirectory(layer));
}
}
}
@Override
public File resolve(String originalName, String newName) throws IOException {
String layer = this.layers.getLayer(originalName);
if (shouldExtractLayer(layer)) {
File directory = getLayerDirectory(layer);
return assertFileIsContainedInDirectory(directory, new File(directory, newName), newName);
}
return null;
}
@Override
public File resolveRunner() throws IOException {
String layer = this.layers.getApplicationLayerName();
if (shouldExtractLayer(layer)) {
File directory = getLayerDirectory(layer);
return assertFileIsContainedInDirectory(directory, new File(directory, this.runnerFilename),
this.runnerFilename);
}
return null;
}
private File getLayerDirectory(String layer) {
return new File(this.directory, layer);
}
private boolean shouldExtractLayer(String layer) {
if (this.layersToExtract.isEmpty()) {
return true;
}
return this.layersToExtract.contains(layer);
}
}
}

69
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ExtractLayersCommand.java

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.io.PrintStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.util.StringUtils;
/**
* The {@code 'extract'} tools command.
*
* @author Phillip Webb
*/
class ExtractLayersCommand extends Command {
static final Option DESTINATION_OPTION = Option.of("destination", "string", "The destination to extract files to");
private final ExtractCommand delegate;
ExtractLayersCommand(Context context) {
this(context, null);
}
ExtractLayersCommand(Context context, Layers layers) {
super("extract", "Extracts layers from the jar for image creation", Options.of(DESTINATION_OPTION),
Parameters.of("[<layer>...]"));
this.delegate = new ExtractCommand(context, layers);
}
@Override
boolean isDeprecated() {
return true;
}
@Override
String getDeprecationMessage() {
return "Use '-Djarmode=tools extract --layers --launcher' instead.";
}
@Override
void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
Map<Option, String> rewrittenOptions = new HashMap<>();
if (options.containsKey(DESTINATION_OPTION)) {
rewrittenOptions.put(ExtractCommand.DESTINATION_OPTION, options.get(DESTINATION_OPTION));
}
rewrittenOptions.put(ExtractCommand.LAYERS_OPTION, StringUtils.collectionToCommaDelimitedString(parameters));
rewrittenOptions.put(ExtractCommand.LAUNCHER_OPTION, null);
this.delegate.run(out, rewrittenOptions, Collections.emptyList());
}
}

64
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/HelpCommand.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/HelpCommand.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
import java.io.PrintStream;
import java.util.List;
@ -25,6 +25,7 @@ import java.util.stream.Stream; @@ -25,6 +25,7 @@ import java.util.stream.Stream;
* Implicit {@code 'help'} command.
*
* @author Phillip Webb
* @author Moritz Halbritter
*/
class HelpCommand extends Command {
@ -32,27 +33,47 @@ class HelpCommand extends Command { @@ -32,27 +33,47 @@ class HelpCommand extends Command {
private final List<Command> commands;
private final String jarMode;
HelpCommand(Context context, List<Command> commands) {
super("help", "Help about any command", Options.none(), Parameters.of("[<command]"));
this(context, commands, System.getProperty("jarmode"));
}
HelpCommand(Context context, List<Command> commands, String jarMode) {
super("help", "Help about any command", Options.none(), Parameters.of("[<command>]"));
this.context = context;
this.commands = commands;
this.jarMode = (jarMode != null) ? jarMode : "tools";
}
@Override
protected void run(Map<Option, String> options, List<String> parameters) {
run(System.out, parameters);
void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
run(out, parameters);
}
void run(PrintStream out, List<String> parameters) {
Command command = (!parameters.isEmpty()) ? Command.find(this.commands, parameters.get(0)) : null;
if (command != null) {
printCommandHelp(out, command);
String commandName = (parameters.isEmpty()) ? null : parameters.get(0);
if (commandName == null) {
printUsageAndCommands(out);
return;
}
if (getName().equals(commandName)) {
printCommandHelp(out, this, true);
return;
}
Command command = Command.find(this.commands, commandName);
if (command == null) {
printError(out, "Unknown command \"%s\"".formatted(commandName));
printUsageAndCommands(out);
return;
}
printUsageAndCommands(out);
printCommandHelp(out, command, true);
}
private void printCommandHelp(PrintStream out, Command command) {
void printCommandHelp(PrintStream out, Command command, boolean printDeprecationWarning) {
if (command.isDeprecated() && printDeprecationWarning) {
printWarning(out, "This command is deprecated. " + command.getDeprecationMessage());
}
out.println(command.getDescription());
out.println();
out.println("Usage:");
@ -85,8 +106,17 @@ class HelpCommand extends Command { @@ -85,8 +106,17 @@ class HelpCommand extends Command {
out.println();
out.println("Available commands:");
int maxNameLength = getMaxLength(getName().length(), this.commands.stream().map(Command::getName));
this.commands.forEach((command) -> printCommandSummary(out, command, maxNameLength));
this.commands.stream()
.filter((command) -> !command.isDeprecated())
.forEach((command) -> printCommandSummary(out, command, maxNameLength));
printCommandSummary(out, this, maxNameLength);
List<Command> deprecatedCommands = this.commands.stream().filter(Command::isDeprecated).toList();
if (!deprecatedCommands.isEmpty()) {
out.println("Deprecated commands:");
for (Command command : deprecatedCommands) {
printCommandSummary(out, command, maxNameLength);
}
}
}
private int getMaxLength(int minimum, Stream<String> strings) {
@ -98,7 +128,17 @@ class HelpCommand extends Command { @@ -98,7 +128,17 @@ class HelpCommand extends Command {
}
private String getJavaCommand() {
return "java -Djarmode=layertools -jar " + this.context.getArchiveFile().getName();
return "java -Djarmode=" + this.jarMode + " -jar " + this.context.getArchiveFile().getName();
}
private void printError(PrintStream out, String errorMessage) {
out.println("Error: " + errorMessage);
out.println();
}
private void printWarning(PrintStream out, String errorMessage) {
out.println("Warning: " + errorMessage);
out.println();
}
}

159
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedJarStructure.java

@ -0,0 +1,159 @@ @@ -0,0 +1,159 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.NoSuchFileException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.UnaryOperator;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
/**
* {@link JarStructure} implementation backed by a {@code classpath.idx} file.
*
* @author Stephane Nicoll
* @author Moritz Halbritter
*/
class IndexedJarStructure implements JarStructure {
private static final List<String> MANIFEST_DENY_LIST = List.of("Start-Class", "Spring-Boot-Classes",
"Spring-Boot-Lib", "Spring-Boot-Classpath-Index", "Spring-Boot-Layers-Index");
private final Manifest originalManifest;
private final String libLocation;
private final String classesLocation;
private final List<String> classpathEntries;
IndexedJarStructure(Manifest originalManifest, String indexFile) {
this.originalManifest = originalManifest;
this.libLocation = getLocation(originalManifest, "Spring-Boot-Lib");
this.classesLocation = getLocation(originalManifest, "Spring-Boot-Classes");
this.classpathEntries = readIndexFile(indexFile);
}
private static String getLocation(Manifest manifest, String attribute) {
String location = getMandatoryAttribute(manifest, attribute);
if (!location.endsWith("/")) {
location = location + "/";
}
return location;
}
private static List<String> readIndexFile(String indexFile) {
String[] lines = Arrays.stream(indexFile.split("\n"))
.map((line) -> line.replace("\r", ""))
.filter(StringUtils::hasText)
.toArray(String[]::new);
List<String> classpathEntries = new ArrayList<>();
for (String line : lines) {
if (line.startsWith("- ")) {
classpathEntries.add(line.substring(3, line.length() - 1));
}
else {
throw new IllegalStateException("Classpath index file is malformed");
}
}
Assert.state(!classpathEntries.isEmpty(), "Empty classpath index file loaded");
return classpathEntries;
}
@Override
public String getClassesLocation() {
return this.classesLocation;
}
@Override
public Entry resolve(String name) {
if (this.classpathEntries.contains(name)) {
return new Entry(name, toStructureDependency(name), Type.LIBRARY);
}
else if (name.startsWith(this.classesLocation)) {
return new Entry(name, name.substring(this.classesLocation.length()), Type.APPLICATION_CLASS_OR_RESOURCE);
}
else if (name.startsWith("org/springframework/boot/loader")) {
return new Entry(name, name, Type.LOADER);
}
return null;
}
@Override
public Manifest createLauncherManifest(UnaryOperator<String> libraryTransformer) {
Manifest manifest = new Manifest(this.originalManifest);
Attributes attributes = manifest.getMainAttributes();
for (String denied : MANIFEST_DENY_LIST) {
attributes.remove(new Name(denied));
}
attributes.put(Name.MAIN_CLASS, getMandatoryAttribute(this.originalManifest, "Start-Class"));
attributes.put(Name.CLASS_PATH,
this.classpathEntries.stream()
.map(this::toStructureDependency)
.map(libraryTransformer)
.collect(Collectors.joining(" ")));
return manifest;
}
private String toStructureDependency(String libEntryName) {
Assert.state(libEntryName.startsWith(this.libLocation), "Invalid library location " + libEntryName);
return libEntryName.substring(this.libLocation.length());
}
private static String getMandatoryAttribute(Manifest manifest, String attribute) {
String value = manifest.getMainAttributes().getValue(attribute);
Assert.state(value != null, "Manifest attribute '" + attribute + "' is mandatory");
return value;
}
static IndexedJarStructure get(File file) {
try {
try (JarFile jarFile = new JarFile(file)) {
Manifest manifest = jarFile.getManifest();
String location = getMandatoryAttribute(manifest, "Spring-Boot-Classpath-Index");
ZipEntry entry = jarFile.getEntry(location);
if (entry != null) {
String indexFile = StreamUtils.copyToString(jarFile.getInputStream(entry), StandardCharsets.UTF_8);
return new IndexedJarStructure(manifest, indexFile);
}
}
return null;
}
catch (FileNotFoundException | NoSuchFileException ex) {
return null;
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
}

25
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/IndexedLayers.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/IndexedLayers.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
import java.io.FileNotFoundException;
import java.io.IOException;
@ -39,12 +39,16 @@ import org.springframework.util.StringUtils; @@ -39,12 +39,16 @@ import org.springframework.util.StringUtils;
*
* @author Phillip Webb
* @author Madhura Bhave
* @author Moritz Halbritter
*/
class IndexedLayers implements Layers {
private final Map<String, List<String>> layers = new LinkedHashMap<>();
IndexedLayers(String indexFile) {
private final String classesLocation;
IndexedLayers(String indexFile, String classesLocation) {
this.classesLocation = classesLocation;
String[] lines = Arrays.stream(indexFile.split("\n"))
.map((line) -> line.replace("\r", ""))
.filter(StringUtils::hasText)
@ -56,6 +60,7 @@ class IndexedLayers implements Layers { @@ -56,6 +60,7 @@ class IndexedLayers implements Layers {
this.layers.put(line.substring(3, line.length() - 2), contents);
}
else if (line.startsWith(" - ")) {
Assert.notNull(contents, "Contents must not be null. Check if the index file is malformed!");
contents.add(line.substring(5, line.length() - 1));
}
else {
@ -66,16 +71,17 @@ class IndexedLayers implements Layers { @@ -66,16 +71,17 @@ class IndexedLayers implements Layers {
}
@Override
public Iterator<String> iterator() {
return this.layers.keySet().iterator();
public String getApplicationLayerName() {
return getLayer(this.classesLocation);
}
@Override
public String getLayer(ZipEntry entry) {
return getLayer(entry.getName());
public Iterator<String> iterator() {
return this.layers.keySet().iterator();
}
private String getLayer(String name) {
@Override
public String getLayer(String name) {
for (Map.Entry<String, List<String>> entry : this.layers.entrySet()) {
for (String candidate : entry.getValue()) {
if (candidate.equals(name) || (candidate.endsWith("/") && name.startsWith(candidate))) {
@ -100,7 +106,8 @@ class IndexedLayers implements Layers { @@ -100,7 +106,8 @@ class IndexedLayers implements Layers {
ZipEntry entry = (location != null) ? jarFile.getEntry(location) : null;
if (entry != null) {
String indexFile = StreamUtils.copyToString(jarFile.getInputStream(entry), StandardCharsets.UTF_8);
return new IndexedLayers(indexFile);
String classesLocation = manifest.getMainAttributes().getValue("Spring-Boot-Classes");
return new IndexedLayers(indexFile, classesLocation);
}
}
return null;

78
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/JarStructure.java

@ -0,0 +1,78 @@ @@ -0,0 +1,78 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.util.function.UnaryOperator;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
/**
* Provide information about a fat jar structure that is meant to be extracted.
*
* @author Stephane Nicoll
* @author Moritz Halbritter
*/
interface JarStructure {
/**
* Resolve the specified {@link ZipEntry}, return {@code null} if the entry should not
* be handled.
* @param entry the entry to handle
* @return the resolved {@link Entry}
*/
default Entry resolve(ZipEntry entry) {
return resolve(entry.getName());
}
/**
* Resolve the entry with the specified name, return {@code null} if the entry should
* not be handled.
* @param name the name of the entry to handle
* @return the resolved {@link Entry}
*/
Entry resolve(String name);
/**
* Create the {@link Manifest} for the launcher jar, applying the specified operator
* on each classpath entry.
* @param libraryTransformer the operator to apply on each classpath entry
* @return the manifest to use for the launcher jar
*/
Manifest createLauncherManifest(UnaryOperator<String> libraryTransformer);
/**
* Return the location of the application classes.
* @return the location of the application classes
*/
String getClassesLocation();
/**
* An entry to handle in the exploded structure.
*
* @param originalLocation the original location
* @param location the relative location
* @param type of the entry
*/
record Entry(String originalLocation, String location, Type type) {
enum Type {
LIBRARY, APPLICATION_CLASS_OR_RESOURCE, LOADER
}
}
}

54
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/LayerToolsJarMode.java

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.util.List;
import org.springframework.boot.loader.jarmode.JarMode;
/**
* {@link JarMode} providing {@code "layertools"} support.
*
* @author Phillip Webb
* @author Scott Frederick
* @since 2.3.0
*/
public class LayerToolsJarMode implements JarMode {
static Context contextOverride;
@Override
public boolean accepts(String mode) {
return "layertools".equalsIgnoreCase(mode);
}
@Override
public void run(String mode, String[] args) {
try {
Context context = (contextOverride != null) ? contextOverride : new Context();
new Runner(System.out, context, getCommands(context)).run(args);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
static List<Command> getCommands(Context context) {
return List.of(new ListCommand(context), new ExtractLayersCommand(context));
}
}

33
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/Layers.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Layers.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2024 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.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
import java.util.Iterator;
import java.util.zip.ZipEntry;
@ -23,6 +23,7 @@ import java.util.zip.ZipEntry; @@ -23,6 +23,7 @@ import java.util.zip.ZipEntry;
* Provides information about the jar layers.
*
* @author Phillip Webb
* @author Moritz Halbritter
* @see ExtractCommand
* @see ListCommand
*/
@ -40,19 +41,43 @@ interface Layers extends Iterable<String> { @@ -40,19 +41,43 @@ interface Layers extends Iterable<String> {
* @param entry the entry to check
* @return the layer that the entry is in
*/
String getLayer(ZipEntry entry);
default String getLayer(ZipEntry entry) {
return getLayer(entry.getName());
}
/**
* Return the layer that the entry with the given name is in.
* @param entryName the name of the entry to check
* @return the layer that the entry is in
*/
String getLayer(String entryName);
/**
* Return the name of the application layer.
* @return the name of the application layer
*/
String getApplicationLayerName();
/**
* Return a {@link Layers} instance for the currently running application.
* @param context the command context
* @return a new layers instance
* @throws LayersNotEnabledException if layers are not enabled
*/
static Layers get(Context context) {
IndexedLayers indexedLayers = IndexedLayers.get(context);
if (indexedLayers == null) {
throw new IllegalStateException("Failed to load layers.idx which is required by layertools");
throw new LayersNotEnabledException();
}
return indexedLayers;
}
final class LayersNotEnabledException extends RuntimeException {
LayersNotEnabledException() {
super("Layers not enabled: Failed to load layer index file");
}
}
}

27
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/ListCommand.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListCommand.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 the original author or authors.
* Copyright 2012-2024 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.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
import java.io.PrintStream;
import java.util.List;
@ -23,24 +23,37 @@ import java.util.Map; @@ -23,24 +23,37 @@ import java.util.Map;
/**
* The {@code 'list'} tools command.
*
* Delegates the actual work to {@link ListLayersCommand}.
*
* @author Phillip Webb
* @author Moritz Halbritter
*/
class ListCommand extends Command {
private final Context context;
private final ListLayersCommand delegate;
ListCommand(Context context) {
super("list", "List layers from the jar that can be extracted", Options.none(), Parameters.none());
this.context = context;
this.delegate = new ListLayersCommand(context);
}
@Override
boolean isDeprecated() {
return true;
}
@Override
String getDeprecationMessage() {
return "Use '-Djarmode=tools list-layers' instead.";
}
@Override
protected void run(Map<Option, String> options, List<String> parameters) {
printLayers(Layers.get(this.context), System.out);
void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
this.delegate.run(out, options, parameters);
}
void printLayers(Layers layers, PrintStream out) {
layers.forEach(out::println);
this.delegate.printLayers(out, layers);
}
}

59
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ListLayersCommand.java

@ -0,0 +1,59 @@ @@ -0,0 +1,59 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.io.PrintStream;
import java.util.List;
import java.util.Map;
import org.springframework.boot.jarmode.tools.Layers.LayersNotEnabledException;
/**
* The {@code 'list-layers'} tools command.
*
* @author Moritz Halbritter
*/
class ListLayersCommand extends Command {
private final Context context;
ListLayersCommand(Context context) {
super("list-layers", "List layers from the jar that can be extracted", Options.none(), Parameters.none());
this.context = context;
}
@Override
void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
try {
Layers layers = Layers.get(this.context);
printLayers(out, layers);
}
catch (LayersNotEnabledException ex) {
printError(out, "Layers are not enabled");
}
}
void printLayers(PrintStream out, Layers layers) {
layers.forEach(out::println);
}
private void printError(PrintStream out, String message) {
out.println("Error: " + message);
out.println();
}
}

4
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/MissingValueException.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/MissingValueException.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2024 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.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
/**
* Exception thrown when a required value is not provided for an option.

94
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/Runner.java

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.io.PrintStream;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.List;
/**
* Runs commands.
*
* @author Moritz Halbritter
*/
class Runner {
private final PrintStream out;
private final List<Command> commands = new ArrayList<>();
private final HelpCommand help;
Runner(PrintStream out, Context context, List<Command> commands) {
this.out = out;
this.commands.addAll(commands);
this.help = new HelpCommand(context, commands);
this.commands.add(this.help);
}
void run(String... args) {
run(dequeOf(args));
}
private void run(Deque<String> args) {
if (!args.isEmpty()) {
String commandName = args.removeFirst();
Command command = Command.find(this.commands, commandName);
if (command != null) {
runCommand(command, args);
return;
}
printError("Unknown command \"" + commandName + "\"");
}
this.help.run(this.out, args);
}
private void runCommand(Command command, Deque<String> args) {
if (command.isDeprecated()) {
printWarning("This command is deprecated. " + command.getDeprecationMessage());
}
try {
command.run(this.out, args);
}
catch (UnknownOptionException ex) {
printError("Unknown option \"" + ex.getMessage() + "\" for the " + command.getName() + " command");
this.help.printCommandHelp(this.out, command, false);
}
catch (MissingValueException ex) {
printError("Option \"" + ex.getMessage() + "\" for the " + command.getName() + " command requires a value");
this.help.printCommandHelp(this.out, command, false);
}
}
private void printWarning(String message) {
this.out.println("Warning: " + message);
this.out.println();
}
private void printError(String message) {
this.out.println("Error: " + message);
this.out.println();
}
private Deque<String> dequeOf(String... args) {
return new ArrayDeque<>(Arrays.asList(args));
}
}

64
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/ToolsJarMode.java

@ -0,0 +1,64 @@ @@ -0,0 +1,64 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.io.PrintStream;
import java.util.List;
import org.springframework.boot.loader.jarmode.JarMode;
/**
* {@link JarMode} providing {@code "tools"} support.
*
* @author Moritz Halbritter
* @since 3.3.0
*/
public class ToolsJarMode implements JarMode {
private final Context context;
private final PrintStream out;
public ToolsJarMode() {
this(null, null);
}
public ToolsJarMode(Context context, PrintStream out) {
this.context = (context != null) ? context : new Context();
this.out = (out != null) ? out : System.out;
}
@Override
public boolean accepts(String mode) {
return "tools".equalsIgnoreCase(mode);
}
@Override
public void run(String mode, String[] args) {
try {
new Runner(this.out, this.context, getCommands(this.context)).run(args);
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
}
static List<Command> getCommands(Context context) {
return List.of(new ExtractCommand(context), new ListLayersCommand(context));
}
}

4
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/UnknownOptionException.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/UnknownOptionException.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2024 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.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
/**
* Exception thrown when an unrecognized option is encountered.

4
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/main/java/org/springframework/boot/jarmode/layertools/package-info.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/java/org/springframework/boot/jarmode/tools/package-info.java

@ -15,6 +15,6 @@ @@ -15,6 +15,6 @@
*/
/**
* JarMode support for layertools.
* JarMode support for layertools and tools.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;

4
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/main/resources/META-INF/spring.factories

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
# Jar Modes
org.springframework.boot.loader.jarmode.JarMode=\
org.springframework.boot.jarmode.tools.LayerToolsJarMode,\
org.springframework.boot.jarmode.tools.ToolsJarMode

154
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/AbstractTests.java

@ -0,0 +1,154 @@ @@ -0,0 +1,154 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.FileTime;
import java.time.Instant;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.util.Assert;
import org.springframework.util.StreamUtils;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Moritz Halbritter
*/
abstract class AbstractTests {
@TempDir
File tempDir;
Manifest createManifest(String... entries) {
Manifest manifest = new Manifest();
manifest.getMainAttributes().putValue("Manifest-Version", "1.0");
for (String entry : entries) {
int colon = entry.indexOf(':');
Assert.state(colon > -1, () -> "Colon not found in %s".formatted(entry));
String key = entry.substring(0, colon).trim();
String value = entry.substring(colon + 1).trim();
manifest.getMainAttributes().putValue(key, value);
}
return manifest;
}
File createArchive(String... entries) throws IOException {
return createArchive(createManifest(), entries);
}
File createArchive(Manifest manifest, String... entries) throws IOException {
return createArchive(manifest, null, null, null, entries);
}
File createArchive(Manifest manifest, Instant creationTime, Instant lastModifiedTime, Instant lastAccessTime,
String... entries) throws IOException {
Assert.state(entries.length % 2 == 0, "Entries must be key value pairs");
File file = new File(this.tempDir, "test.jar");
try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(file), manifest)) {
for (int i = 0; i < entries.length; i += 2) {
ZipEntry entry = new ZipEntry(entries[i]);
if (creationTime != null) {
entry.setCreationTime(FileTime.from(creationTime));
}
if (lastModifiedTime != null) {
entry.setLastModifiedTime(FileTime.from(lastModifiedTime));
}
if (lastAccessTime != null) {
entry.setLastAccessTime(FileTime.from(lastAccessTime));
}
jar.putNextEntry(entry);
String resource = entries[i + 1];
if (resource != null) {
try (InputStream content = ListLayersCommandTests.class.getResourceAsStream(resource)) {
assertThat(content).as("Resource " + resource).isNotNull();
StreamUtils.copy(content, jar);
}
}
jar.closeEntry();
}
}
return file;
}
TestPrintStream runCommand(CommandFactory<?> commandFactory, File archive, String... arguments) {
Context context = new Context(archive, this.tempDir);
Command command = commandFactory.create(context);
TestPrintStream out = new TestPrintStream(this);
command.run(out, new ArrayDeque<>(Arrays.asList(arguments)));
return out;
}
Manifest getJarManifest(File jar) throws IOException {
try (JarFile jarFile = new JarFile(jar)) {
return jarFile.getManifest();
}
}
Map<String, String> getJarManifestAttributes(File jar) throws IOException {
assertThat(jar).exists();
Manifest manifest = getJarManifest(jar);
Map<String, String> result = new HashMap<>();
manifest.getMainAttributes().forEach((key, value) -> result.put(key.toString(), value.toString()));
return result;
}
List<String> getJarEntryNames(File jar) throws IOException {
assertThat(jar).exists();
try (JarFile jarFile = new JarFile(jar)) {
return jarFile.stream().map(ZipEntry::getName).toList();
}
}
List<String> listFilenames() throws IOException {
return listFilenames(this.tempDir);
}
List<String> listFilenames(File directory) throws IOException {
try (Stream<Path> stream = Files.walk(directory.toPath())) {
int substring = directory.getAbsolutePath().length() + 1;
return stream.map((file) -> file.toAbsolutePath().toString())
.map((file) -> (file.length() >= substring) ? file.substring(substring) : "")
.filter(StringUtils::hasLength)
.toList();
}
}
interface CommandFactory<T extends Command> {
T create(Context context);
}
}

27
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/CommandTests.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/CommandTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -14,8 +14,9 @@ @@ -14,8 +14,9 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
import java.io.PrintStream;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.List;
@ -24,9 +25,9 @@ import java.util.Map; @@ -24,9 +25,9 @@ import java.util.Map;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.springframework.boot.jarmode.layertools.Command.Option;
import org.springframework.boot.jarmode.layertools.Command.Options;
import org.springframework.boot.jarmode.layertools.Command.Parameters;
import org.springframework.boot.jarmode.tools.Command.Option;
import org.springframework.boot.jarmode.tools.Command.Options;
import org.springframework.boot.jarmode.tools.Command.Parameters;
import static org.assertj.core.api.Assertions.as;
import static org.assertj.core.api.Assertions.assertThat;
@ -37,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -37,6 +38,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
*
* @author Phillip Webb
* @author Scott Frederick
* @author Moritz Halbritter
*/
class CommandTests {
@ -44,6 +46,9 @@ class CommandTests { @@ -44,6 +46,9 @@ class CommandTests {
private static final Option LOG_LEVEL_OPTION = Option.of("log-level", "Logging level (debug or info)", "string");
private static final Option LAYERS_OPTION = Option.of("layers", "Layers (leave empty for all)", "string list",
true);
@Test
void getNameReturnsName() {
TestCommand command = new TestCommand("test");
@ -146,8 +151,16 @@ class CommandTests { @@ -146,8 +151,16 @@ class CommandTests {
assertThat(option.getValueDescription()).isEqualTo("value description");
}
@Test
void shouldNotParseFollowingOptionAsValue() {
TestCommand command = new TestCommand("test", LAYERS_OPTION, LOG_LEVEL_OPTION);
run(command, "--layers", "--log-level", "debug");
assertThat(command.getRunOptions()).containsEntry(LAYERS_OPTION, null);
assertThat(command.getRunOptions()).containsEntry(LOG_LEVEL_OPTION, "debug");
}
private void run(TestCommand command, String... args) {
command.run(new ArrayDeque<>(Arrays.asList(args)));
command.run(System.out, new ArrayDeque<>(Arrays.asList(args)));
}
static class TestCommand extends Command {
@ -165,7 +178,7 @@ class CommandTests { @@ -165,7 +178,7 @@ class CommandTests {
}
@Override
protected void run(Map<Option, String> options, List<String> parameters) {
protected void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
this.runOptions = options;
this.runParameters = parameters;
}

4
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ContextTests.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ContextTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
import java.io.File;
import java.io.IOException;

303
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractCommandTests.java

@ -0,0 +1,303 @@ @@ -0,0 +1,303 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.Runtime.Version;
import java.nio.file.Files;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.jar.Manifest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.condition.OS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link ExtractCommand}.
*
* @author Moritz Halbritter
*/
class ExtractCommandTests extends AbstractTests {
private static final Instant NOW = Instant.now();
private static final Instant CREATION_TIME = NOW.minus(3, ChronoUnit.DAYS);
private static final Instant LAST_MODIFIED_TIME = NOW.minus(2, ChronoUnit.DAYS);
private static final Instant LAST_ACCESS_TIME = NOW.minus(1, ChronoUnit.DAYS);
private File archive;
@BeforeEach
void setUp() throws IOException {
Manifest manifest = createManifest("Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx",
"Spring-Boot-Lib: BOOT-INF/lib/", "Spring-Boot-Classes: BOOT-INF/classes/",
"Start-Class: org.example.Main", "Spring-Boot-Layers-Index: BOOT-INF/layers.idx",
"Some-Attribute: Some-Value");
this.archive = createArchive(manifest, CREATION_TIME, LAST_MODIFIED_TIME, LAST_ACCESS_TIME,
"BOOT-INF/classpath.idx", "/jar-contents/classpath.idx", "BOOT-INF/layers.idx",
"/jar-contents/layers.idx", "BOOT-INF/lib/dependency-1.jar", "/jar-contents/dependency-1",
"BOOT-INF/lib/dependency-2.jar", "/jar-contents/dependency-2", "BOOT-INF/lib/dependency-3-SNAPSHOT.jar",
"/jar-contents/dependency-3-SNAPSHOT", "org/springframework/boot/loader/launch/JarLauncher.class",
"/jar-contents/JarLauncher", "BOOT-INF/classes/application.properties",
"/jar-contents/application.properties");
}
private File file(String name) {
return new File(this.tempDir, name);
}
private TestPrintStream run(File archive, String... args) {
return runCommand(ExtractCommand::new, archive, args);
}
private void timeAttributes(File file) {
try {
BasicFileAttributes basicAttributes = Files
.getFileAttributeView(file.toPath(), BasicFileAttributeView.class)
.readAttributes();
assertThat(basicAttributes.lastModifiedTime().toInstant().truncatedTo(ChronoUnit.SECONDS))
.isEqualTo(LAST_MODIFIED_TIME.truncatedTo(ChronoUnit.SECONDS));
Instant expectedCreationTime = expectedCreationTime();
if (expectedCreationTime != null) {
assertThat(basicAttributes.creationTime().toInstant().truncatedTo(ChronoUnit.SECONDS))
.isEqualTo(expectedCreationTime.truncatedTo(ChronoUnit.SECONDS));
}
assertThat(basicAttributes.lastAccessTime().toInstant().truncatedTo(ChronoUnit.SECONDS))
.isEqualTo(LAST_ACCESS_TIME.truncatedTo(ChronoUnit.SECONDS));
}
catch (IOException ex) {
throw new RuntimeException(ex);
}
}
private Instant expectedCreationTime() {
// macOS uses last modified time until Java 20 where it uses creation time.
// https://github.com/openjdk/jdk21u-dev/commit/6397d564a5dab07f81bf4c69b116ebfabb2446ba
if (OS.MAC.isCurrentOs()) {
return (EnumSet.range(JRE.JAVA_17, JRE.JAVA_19).contains(JRE.currentVersion())) ? LAST_MODIFIED_TIME
: CREATION_TIME;
}
if (OS.LINUX.isCurrentOs()) {
// Linux uses the modified time until Java 21.0.2 where a bug means that it
// uses the birth time which it has not set, preventing us from verifying it.
// https://github.com/openjdk/jdk21u-dev/commit/4cf572e3b99b675418e456e7815fb6fd79245e30
return (Runtime.version().compareTo(Version.parse("21.0.2")) >= 0) ? null : LAST_MODIFIED_TIME;
}
return CREATION_TIME;
}
@Nested
class Extract {
@Test
void extractLibrariesAndCreatesRunner() throws IOException {
run(ExtractCommandTests.this.archive);
List<String> filenames = listFilenames();
assertThat(filenames).contains("lib/dependency-1.jar")
.contains("lib/dependency-2.jar")
.contains("lib/dependency-3-SNAPSHOT.jar")
.contains("runner.jar")
.doesNotContain("org/springframework/boot/loader/launch/JarLauncher.class");
}
@Test
void extractLibrariesAndCreatesRunnerInDestination() throws IOException {
run(ExtractCommandTests.this.archive, "--destination", file("out").getAbsolutePath());
List<String> filenames = listFilenames();
assertThat(filenames).contains("out/lib/dependency-1.jar")
.contains("out/lib/dependency-2.jar")
.contains("out/lib/dependency-3-SNAPSHOT.jar")
.contains("out/runner.jar");
}
@Test
void runnerNameAndLibrariesDirectoriesCanBeCustomized() throws IOException {
run(ExtractCommandTests.this.archive, "--runner-filename", "runner-customized.jar", "--libraries",
"dependencies");
List<String> filenames = listFilenames();
assertThat(filenames).contains("dependencies/dependency-1.jar")
.contains("dependencies/dependency-2.jar")
.contains("dependencies/dependency-3-SNAPSHOT.jar");
File runner = file("runner-customized.jar");
assertThat(runner).exists();
Map<String, String> attributes = getJarManifestAttributes(runner);
assertThat(attributes).containsEntry("Class-Path",
"dependencies/dependency-1.jar dependencies/dependency-2.jar dependencies/dependency-3-SNAPSHOT.jar");
}
@Test
void runnerContainsManifestEntries() throws IOException {
run(ExtractCommandTests.this.archive);
File runner = file("runner.jar");
Map<String, String> attributes = getJarManifestAttributes(runner);
assertThat(attributes).containsEntry("Main-Class", "org.example.Main")
.containsEntry("Class-Path", "lib/dependency-1.jar lib/dependency-2.jar lib/dependency-3-SNAPSHOT.jar")
.containsEntry("Some-Attribute", "Some-Value")
.doesNotContainKeys("Start-Class", "Spring-Boot-Classes", "Spring-Boot-Lib",
"Spring-Boot-Classpath-Index", "Spring-Boot-Layers-Index");
}
@Test
void runnerContainsApplicationClassesAndResources() throws IOException {
run(ExtractCommandTests.this.archive);
File runner = file("runner.jar");
List<String> entryNames = getJarEntryNames(runner);
assertThat(entryNames).contains("application.properties");
}
@Test
void appliesFileTimes() {
run(ExtractCommandTests.this.archive);
assertThat(file("lib/dependency-1.jar")).exists().satisfies(ExtractCommandTests.this::timeAttributes);
assertThat(file("lib/dependency-2.jar")).exists().satisfies(ExtractCommandTests.this::timeAttributes);
assertThat(file("lib/dependency-3-SNAPSHOT.jar")).exists()
.satisfies(ExtractCommandTests.this::timeAttributes);
}
@Test
void runnerDoesntContainLibraries() throws IOException {
run(ExtractCommandTests.this.archive);
File runner = file("runner.jar");
List<String> entryNames = getJarEntryNames(runner);
assertThat(entryNames).doesNotContain("BOOT-INF/lib/dependency-1.jar", "BOOT-INF/lib/dependency-2.jar");
}
@Test
void failsOnIncompatibleJar() throws IOException {
File file = file("empty.jar");
try (FileWriter writer = new FileWriter(file)) {
writer.write("text");
}
assertThatIllegalStateException().isThrownBy(() -> run(file)).withMessageContaining("not compatible");
}
}
@Nested
class ExtractWithLayers {
@Test
void extractLibrariesAndCreatesRunner() throws IOException {
run(ExtractCommandTests.this.archive, "--layers");
List<String> filenames = listFilenames();
assertThat(filenames).contains("dependencies/lib/dependency-1.jar")
.contains("dependencies/lib/dependency-2.jar")
.contains("snapshot-dependencies/lib/dependency-3-SNAPSHOT.jar")
.contains("application/runner.jar");
}
@Test
void extractsOnlySelectedLayers() throws IOException {
run(ExtractCommandTests.this.archive, "--layers", "dependencies");
List<String> filenames = listFilenames();
assertThat(filenames).contains("dependencies/lib/dependency-1.jar")
.contains("dependencies/lib/dependency-2.jar")
.doesNotContain("snapshot-dependencies/lib/dependency-3-SNAPSHOT.jar")
.doesNotContain("application/runner.jar");
}
@Test
void printErrorIfLayersAreNotEnabled() throws IOException {
File archive = createArchive();
TestPrintStream out = run(archive, "--layers");
assertThat(out).hasSameContentAsResource("ExtractCommand-printErrorIfLayersAreNotEnabled.txt");
}
}
@Nested
class ExtractLauncher {
@Test
void extract() throws IOException {
run(ExtractCommandTests.this.archive, "--launcher");
List<String> filenames = listFilenames();
assertThat(filenames).contains("META-INF/MANIFEST.MF")
.contains("BOOT-INF/classpath.idx")
.contains("BOOT-INF/layers.idx")
.contains("BOOT-INF/lib/dependency-1.jar")
.contains("BOOT-INF/lib/dependency-2.jar")
.contains("BOOT-INF/lib/dependency-3-SNAPSHOT.jar")
.contains("BOOT-INF/classes/application.properties")
.contains("org/springframework/boot/loader/launch/JarLauncher.class");
}
@Test
void runWithJarFileThatWouldWriteEntriesOutsideDestinationFails() throws Exception {
File file = createArchive("e/../../e.jar", null);
assertThatIllegalStateException().isThrownBy(() -> run(file, "--launcher"))
.withMessageContaining("Entry 'e/../../e.jar' would be written");
}
}
@Nested
class ExtractLauncherWithLayers {
@Test
void extract() throws IOException {
run(ExtractCommandTests.this.archive, "--launcher", "--layers");
List<String> filenames = listFilenames();
assertThat(filenames).contains("application/META-INF/MANIFEST.MF")
.contains("application/BOOT-INF/classpath.idx")
.contains("application/BOOT-INF/layers.idx")
.contains("dependencies/BOOT-INF/lib/dependency-1.jar")
.contains("dependencies/BOOT-INF/lib/dependency-2.jar")
.contains("snapshot-dependencies/BOOT-INF/lib/dependency-3-SNAPSHOT.jar")
.contains("application/BOOT-INF/classes/application.properties")
.contains("spring-boot-loader/org/springframework/boot/loader/launch/JarLauncher.class");
}
@Test
void printErrorIfLayersAreNotEnabled() throws IOException {
File archive = createArchive();
TestPrintStream out = run(archive, "--launcher", "--layers");
assertThat(out).hasSameContentAsResource("ExtractCommand-printErrorIfLayersAreNotEnabled.txt");
}
@Test
void extractsOnlySelectedLayers() throws IOException {
run(ExtractCommandTests.this.archive, "--launcher", "--layers", "dependencies");
List<String> filenames = listFilenames();
assertThat(filenames).doesNotContain("application/META-INF/MANIFEST.MF")
.doesNotContain("application/BOOT-INF/classpath.idx")
.doesNotContain("application/BOOT-INF/layers.idx")
.contains("dependencies/BOOT-INF/lib/dependency-1.jar")
.contains("dependencies/BOOT-INF/lib/dependency-2.jar")
.doesNotContain("snapshot-dependencies/BOOT-INF/lib/dependency-3-SNAPSHOT.jar")
.doesNotContain("application/BOOT-INF/classes/application.properties")
.doesNotContain("spring-boot-loader/org/springframework/boot/loader/launch/JarLauncher.class");
}
}
}

35
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ExtractCommandTests.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ExtractLayersCommandTests.java

@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
import java.io.File;
import java.io.FileOutputStream;
@ -54,13 +54,13 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -54,13 +54,13 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.BDDMockito.given;
/**
* Tests for {@link ExtractCommand}.
* Tests for {@link ExtractLayersCommand}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
@ExtendWith(MockitoExtension.class)
class ExtractCommandTests {
class ExtractLayersCommandTests {
private static final Instant NOW = Instant.now();
@ -82,21 +82,21 @@ class ExtractCommandTests { @@ -82,21 +82,21 @@ class ExtractCommandTests {
private final Layers layers = new TestLayers();
private ExtractCommand command;
private ExtractLayersCommand command;
@BeforeEach
void setup() throws Exception {
this.jarFile = createJarFile("test.jar");
this.extract = new File(this.temp, "extract");
this.extract.mkdir();
this.command = new ExtractCommand(this.context, this.layers);
this.command = new ExtractLayersCommand(this.context, this.layers);
}
@Test
void runExtractsLayers() {
given(this.context.getArchiveFile()).willReturn(this.jarFile);
given(this.context.getWorkingDir()).willReturn(this.extract);
this.command.run(Collections.emptyMap(), Collections.emptyList());
this.command.run(System.out, Collections.emptyMap(), Collections.emptyList());
assertThat(this.extract.list()).containsOnly("a", "b", "c", "d");
assertThat(new File(this.extract, "a/a/a.jar")).exists().satisfies(this::timeAttributes);
assertThat(new File(this.extract, "b/b/b.jar")).exists().satisfies(this::timeAttributes);
@ -145,7 +145,8 @@ class ExtractCommandTests { @@ -145,7 +145,8 @@ class ExtractCommandTests {
void runWhenHasDestinationOptionExtractsLayers() {
given(this.context.getArchiveFile()).willReturn(this.jarFile);
File out = new File(this.extract, "out");
this.command.run(Collections.singletonMap(ExtractCommand.DESTINATION_OPTION, out.getAbsolutePath()),
this.command.run(System.out,
Collections.singletonMap(ExtractLayersCommand.DESTINATION_OPTION, out.getAbsolutePath()),
Collections.emptyList());
assertThat(this.extract.list()).containsOnly("out");
assertThat(new File(this.extract, "out/a/a/a.jar")).exists().satisfies(this::timeAttributes);
@ -157,7 +158,7 @@ class ExtractCommandTests { @@ -157,7 +158,7 @@ class ExtractCommandTests {
void runWhenHasLayerParamsExtractsLimitedLayers() {
given(this.context.getArchiveFile()).willReturn(this.jarFile);
given(this.context.getWorkingDir()).willReturn(this.extract);
this.command.run(Collections.emptyMap(), Arrays.asList("a", "c"));
this.command.run(System.out, Collections.emptyMap(), Arrays.asList("a", "c"));
assertThat(this.extract.list()).containsOnly("a", "c");
assertThat(new File(this.extract, "a/a/a.jar")).exists().satisfies(this::timeAttributes);
assertThat(new File(this.extract, "c/c/c.jar")).exists().satisfies(this::timeAttributes);
@ -171,10 +172,9 @@ class ExtractCommandTests { @@ -171,10 +172,9 @@ class ExtractCommandTests {
writer.write("text");
}
given(this.context.getArchiveFile()).willReturn(file);
given(this.context.getWorkingDir()).willReturn(this.extract);
assertThatIllegalStateException()
.isThrownBy(() -> this.command.run(Collections.emptyMap(), Collections.emptyList()))
.withMessageContaining("not compatible with layertools");
.isThrownBy(() -> this.command.run(System.out, Collections.emptyMap(), Collections.emptyList()))
.withMessageContaining("not compatible");
}
@Test
@ -191,7 +191,7 @@ class ExtractCommandTests { @@ -191,7 +191,7 @@ class ExtractCommandTests {
given(this.context.getArchiveFile()).willReturn(this.jarFile);
given(this.context.getWorkingDir()).willReturn(this.extract);
assertThatIllegalStateException()
.isThrownBy(() -> this.command.run(Collections.emptyMap(), Collections.emptyList()))
.isThrownBy(() -> this.command.run(System.out, Collections.emptyMap(), Collections.emptyList()))
.withMessageContaining("Entry 'e/../../e.jar' would be written");
}
@ -247,16 +247,21 @@ class ExtractCommandTests { @@ -247,16 +247,21 @@ class ExtractCommandTests {
}
@Override
public String getLayer(ZipEntry entry) {
if (entry.getName().startsWith("a")) {
public String getLayer(String entryName) {
if (entryName.startsWith("a")) {
return "a";
}
if (entry.getName().startsWith("b")) {
if (entryName.startsWith("b")) {
return "b";
}
return "c";
}
@Override
public String getApplicationLayerName() {
return "application";
}
}
}

66
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/HelpCommandTests.java

@ -0,0 +1,66 @@ @@ -0,0 +1,66 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.nio.file.Path;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mockito;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
/**
* Tests for {@link HelpCommand}.
*
* @author Phillip Webb
*/
class HelpCommandTests {
private HelpCommand command;
private TestPrintStream out;
@TempDir
Path temp;
@BeforeEach
void setup() {
Context context = Mockito.mock(Context.class);
given(context.getArchiveFile()).willReturn(this.temp.resolve("test.jar").toFile());
this.command = new HelpCommand(context, List.of(new TestCommand()), "tools");
this.out = new TestPrintStream(this);
}
@Test
void shouldPrintAllCommands() {
this.command.run(this.out, Collections.emptyList());
assertThat(this.out).hasSameContentAsResource("help-output.txt");
}
@Test
void shouldPrintCommandSpecificHelp() {
this.command.run(this.out, List.of("test"));
System.out.println(this.out);
assertThat(this.out).hasSameContentAsResource("help-test-output.txt");
}
}

173
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedJarStructureTests.java

@ -0,0 +1,173 @@ @@ -0,0 +1,173 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.function.UnaryOperator;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.jarmode.tools.JarStructure.Entry;
import org.springframework.boot.jarmode.tools.JarStructure.Entry.Type;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link IndexedJarStructure}.
*
* @author Moritz Halbritter
*/
class IndexedJarStructureTests {
@Test
void shouldResolveLibraryEntry() throws IOException {
IndexedJarStructure structure = createStructure();
Entry entry = structure.resolve("BOOT-INF/lib/spring-webmvc-6.1.4.jar");
assertThat(entry.location()).isEqualTo("spring-webmvc-6.1.4.jar");
assertThat(entry.originalLocation()).isEqualTo("BOOT-INF/lib/spring-webmvc-6.1.4.jar");
assertThat(entry.type()).isEqualTo(Type.LIBRARY);
}
@Test
void shouldResolveApplicationEntry() throws IOException {
IndexedJarStructure structure = createStructure();
Entry entry = structure.resolve("BOOT-INF/classes/application.properties");
assertThat(entry.location()).isEqualTo("application.properties");
assertThat(entry.originalLocation()).isEqualTo("BOOT-INF/classes/application.properties");
assertThat(entry.type()).isEqualTo(Type.APPLICATION_CLASS_OR_RESOURCE);
}
@Test
void shouldResolveLoaderEntry() throws IOException {
IndexedJarStructure structure = createStructure();
Entry entry = structure.resolve("org/springframework/boot/loader/launch/JarLauncher");
assertThat(entry.location()).isEqualTo("org/springframework/boot/loader/launch/JarLauncher");
assertThat(entry.originalLocation()).isEqualTo("org/springframework/boot/loader/launch/JarLauncher");
assertThat(entry.type()).isEqualTo(Type.LOADER);
}
@Test
void shouldNotResolveNonExistingLibs() throws IOException {
IndexedJarStructure structure = createStructure();
Entry entry = structure.resolve("BOOT-INF/lib/doesnt-exists.jar");
assertThat(entry).isNull();
}
@Test
void shouldCreateLauncherManifest() throws IOException {
IndexedJarStructure structure = createStructure();
Manifest manifest = structure.createLauncherManifest(UnaryOperator.identity());
Map<String, String> attributes = getAttributes(manifest);
assertThat(attributes).containsEntry("Manifest-Version", "1.0")
.containsEntry("Implementation-Title", "IndexedJarStructureTests")
.containsEntry("Spring-Boot-Version", "3.3.0-SNAPSHOT")
.containsEntry("Implementation-Version", "0.0.1-SNAPSHOT")
.containsEntry("Build-Jdk-Spec", "17")
.containsEntry("Class-Path",
"spring-webmvc-6.1.4.jar spring-web-6.1.4.jar spring-boot-autoconfigure-3.3.0-SNAPSHOT.jar spring-boot-3.3.0-SNAPSHOT.jar jakarta.annotation-api-2.1.1.jar spring-context-6.1.4.jar spring-aop-6.1.4.jar spring-beans-6.1.4.jar spring-expression-6.1.4.jar spring-core-6.1.4.jar snakeyaml-2.2.jar jackson-datatype-jdk8-2.16.1.jar jackson-datatype-jsr310-2.16.1.jar jackson-module-parameter-names-2.16.1.jar jackson-databind-2.16.1.jar tomcat-embed-websocket-10.1.19.jar tomcat-embed-core-10.1.19.jar tomcat-embed-el-10.1.19.jar micrometer-observation-1.13.0-M1.jar logback-classic-1.4.14.jar log4j-to-slf4j-2.23.0.jar jul-to-slf4j-2.0.12.jar spring-jcl-6.1.4.jar jackson-annotations-2.16.1.jar jackson-core-2.16.1.jar micrometer-commons-1.13.0-M1.jar logback-core-1.4.14.jar slf4j-api-2.0.12.jar log4j-api-2.23.0.jar")
.containsEntry("Main-Class", "org.springframework.boot.jarmode.tools.IndexedJarStructureTests")
.doesNotContainKeys("Start-Class", "Spring-Boot-Classes", "Spring-Boot-Lib", "Spring-Boot-Classpath-Index",
"Spring-Boot-Layers-Index");
}
@Test
void shouldLoadFromFile(@TempDir File tempDir) throws IOException {
File jarFile = new File(tempDir, "test.jar");
try (JarOutputStream outputStream = new JarOutputStream(new FileOutputStream(jarFile), createManifest())) {
outputStream.putNextEntry(new ZipEntry("BOOT-INF/classpath.idx"));
outputStream.write(createIndexFile().getBytes(StandardCharsets.UTF_8));
outputStream.closeEntry();
}
IndexedJarStructure structure = IndexedJarStructure.get(jarFile);
assertThat(structure).isNotNull();
assertThat(structure.resolve("BOOT-INF/lib/spring-webmvc-6.1.4.jar")).extracting(Entry::type)
.isEqualTo(Type.LIBRARY);
assertThat(structure.resolve("BOOT-INF/classes/application.properties")).extracting(Entry::type)
.isEqualTo(Type.APPLICATION_CLASS_OR_RESOURCE);
}
private Map<String, String> getAttributes(Manifest manifest) {
Map<String, String> result = new HashMap<>();
manifest.getMainAttributes().forEach((key, value) -> result.put(key.toString(), value.toString()));
return result;
}
private IndexedJarStructure createStructure() throws IOException {
return new IndexedJarStructure(createManifest(), createIndexFile());
}
private String createIndexFile() {
return """
- "BOOT-INF/lib/spring-webmvc-6.1.4.jar"
- "BOOT-INF/lib/spring-web-6.1.4.jar"
- "BOOT-INF/lib/spring-boot-autoconfigure-3.3.0-SNAPSHOT.jar"
- "BOOT-INF/lib/spring-boot-3.3.0-SNAPSHOT.jar"
- "BOOT-INF/lib/jakarta.annotation-api-2.1.1.jar"
- "BOOT-INF/lib/spring-context-6.1.4.jar"
- "BOOT-INF/lib/spring-aop-6.1.4.jar"
- "BOOT-INF/lib/spring-beans-6.1.4.jar"
- "BOOT-INF/lib/spring-expression-6.1.4.jar"
- "BOOT-INF/lib/spring-core-6.1.4.jar"
- "BOOT-INF/lib/snakeyaml-2.2.jar"
- "BOOT-INF/lib/jackson-datatype-jdk8-2.16.1.jar"
- "BOOT-INF/lib/jackson-datatype-jsr310-2.16.1.jar"
- "BOOT-INF/lib/jackson-module-parameter-names-2.16.1.jar"
- "BOOT-INF/lib/jackson-databind-2.16.1.jar"
- "BOOT-INF/lib/tomcat-embed-websocket-10.1.19.jar"
- "BOOT-INF/lib/tomcat-embed-core-10.1.19.jar"
- "BOOT-INF/lib/tomcat-embed-el-10.1.19.jar"
- "BOOT-INF/lib/micrometer-observation-1.13.0-M1.jar"
- "BOOT-INF/lib/logback-classic-1.4.14.jar"
- "BOOT-INF/lib/log4j-to-slf4j-2.23.0.jar"
- "BOOT-INF/lib/jul-to-slf4j-2.0.12.jar"
- "BOOT-INF/lib/spring-jcl-6.1.4.jar"
- "BOOT-INF/lib/jackson-annotations-2.16.1.jar"
- "BOOT-INF/lib/jackson-core-2.16.1.jar"
- "BOOT-INF/lib/micrometer-commons-1.13.0-M1.jar"
- "BOOT-INF/lib/logback-core-1.4.14.jar"
- "BOOT-INF/lib/slf4j-api-2.0.12.jar"
- "BOOT-INF/lib/log4j-api-2.23.0.jar"
""";
}
private Manifest createManifest() throws IOException {
return new Manifest(new ByteArrayInputStream("""
Manifest-Version: 1.0
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: org.springframework.boot.jarmode.tools.IndexedJarStructureTests
Spring-Boot-Version: 3.3.0-SNAPSHOT
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Build-Jdk-Spec: 17
Implementation-Title: IndexedJarStructureTests
Implementation-Version: 0.0.1-SNAPSHOT
""".getBytes(StandardCharsets.UTF_8)));
}
}

18
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/IndexedLayersTests.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/IndexedLayersTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
import java.io.File;
import java.io.FileOutputStream;
@ -46,46 +46,46 @@ class IndexedLayersTests { @@ -46,46 +46,46 @@ class IndexedLayersTests {
@Test
void createWhenIndexFileIsEmptyThrowsException() {
assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers(" \n "))
assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers(" \n ", "BOOT-INF/classes"))
.withMessage("Empty layer index file loaded");
}
@Test
void createWhenIndexFileIsMalformedThrowsException() {
assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers("test"))
assertThatIllegalStateException().isThrownBy(() -> new IndexedLayers("test", "BOOT-INF/classes"))
.withMessage("Layer index file is malformed");
}
@Test
void iteratorReturnsLayers() throws Exception {
IndexedLayers layers = new IndexedLayers(getIndex());
IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes");
assertThat(layers).containsExactly("test", "empty", "application");
}
@Test
void getLayerWhenMatchesNameReturnsLayer() throws Exception {
IndexedLayers layers = new IndexedLayers(getIndex());
IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes");
assertThat(layers.getLayer(mockEntry("BOOT-INF/lib/a.jar"))).isEqualTo("test");
assertThat(layers.getLayer(mockEntry("BOOT-INF/classes/Demo.class"))).isEqualTo("application");
}
@Test
void getLayerWhenMatchesNameForMissingLayerThrowsException() throws Exception {
IndexedLayers layers = new IndexedLayers(getIndex());
IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes");
assertThatIllegalStateException().isThrownBy(() -> layers.getLayer(mockEntry("file.jar")))
.withMessage("No layer defined in index for file " + "'file.jar'");
}
@Test
void getLayerWhenMatchesDirectoryReturnsLayer() throws Exception {
IndexedLayers layers = new IndexedLayers(getIndex());
IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes");
assertThat(layers.getLayer(mockEntry("META-INF/MANIFEST.MF"))).isEqualTo("application");
assertThat(layers.getLayer(mockEntry("META-INF/a/sub/directory/and/a/file"))).isEqualTo("application");
}
@Test
void getLayerWhenFileHasSpaceReturnsLayer() throws Exception {
IndexedLayers layers = new IndexedLayers(getIndex());
IndexedLayers layers = new IndexedLayers(getIndex(), "BOOT-INF/classes");
assertThat(layers.getLayer(mockEntry("a b/c d"))).isEqualTo("application");
}

20
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/LayerToolsJarModeTests.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/LayerToolsJarModeTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 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.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
import java.io.File;
import java.io.FileOutputStream;
@ -62,43 +62,45 @@ class LayerToolsJarModeTests { @@ -62,43 +62,45 @@ class LayerToolsJarModeTests {
this.out = new TestPrintStream(this);
this.systemOut = System.out;
System.setOut(this.out);
LayerToolsJarMode.Runner.contextOverride = context;
LayerToolsJarMode.contextOverride = context;
System.setProperty("jarmode", "layertools");
}
@AfterEach
void restore() {
System.setOut(this.systemOut);
LayerToolsJarMode.Runner.contextOverride = null;
LayerToolsJarMode.contextOverride = null;
System.clearProperty("jarmode");
}
@Test
void mainWithNoParametersShowsHelp() {
new LayerToolsJarMode().run("layertools", NO_ARGS);
assertThat(this.out).hasSameContentAsResource("help-output.txt");
assertThat(this.out).hasSameContentAsResource("layertools-help-output.txt");
}
@Test
void mainWithArgRunsCommand() {
new LayerToolsJarMode().run("layertools", new String[] { "list" });
assertThat(this.out).hasSameContentAsResource("list-output.txt");
assertThat(this.out).hasSameContentAsResource("layertools-list-output.txt");
}
@Test
void mainWithUnknownCommandShowsErrorAndHelp() {
new LayerToolsJarMode().run("layertools", new String[] { "invalid" });
assertThat(this.out).hasSameContentAsResource("error-command-unknown-output.txt");
assertThat(this.out).hasSameContentAsResource("layertools-error-command-unknown-output.txt");
}
@Test
void mainWithUnknownOptionShowsErrorAndCommandHelp() {
new LayerToolsJarMode().run("layertools", new String[] { "extract", "--invalid" });
assertThat(this.out).hasSameContentAsResource("error-option-unknown-output.txt");
assertThat(this.out).hasSameContentAsResource("layertools-error-option-unknown-output.txt");
}
@Test
void mainWithOptionMissingRequiredValueShowsErrorAndCommandHelp() {
new LayerToolsJarMode().run("layertools", new String[] { "extract", "--destination" });
assertThat(this.out).hasSameContentAsResource("error-option-missing-value-output.txt");
assertThat(this.out).hasSameContentAsResource("layertools-error-option-missing-value-output.txt");
}
private File createJarFile(String name) throws Exception {

18
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/ListCommandTests.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListCommandTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2021 the original author or authors.
* Copyright 2012-2024 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.
@ -14,12 +14,11 @@ @@ -14,12 +14,11 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
@ -35,7 +34,6 @@ import org.mockito.Mock; @@ -35,7 +34,6 @@ import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.core.io.ClassPathResource;
import org.springframework.util.FileCopyUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
@ -55,16 +53,14 @@ class ListCommandTests { @@ -55,16 +53,14 @@ class ListCommandTests {
@Mock
private Context context;
private File jarFile;
private ListCommand command;
private TestPrintStream out;
@BeforeEach
void setup() throws Exception {
this.jarFile = createJarFile("test.jar");
given(this.context.getArchiveFile()).willReturn(this.jarFile);
File jarFile = createJarFile("test.jar");
given(this.context.getArchiveFile()).willReturn(jarFile);
this.command = new ListCommand(this.context);
this.out = new TestPrintStream(this);
}
@ -73,7 +69,7 @@ class ListCommandTests { @@ -73,7 +69,7 @@ class ListCommandTests {
void listLayersShouldListLayers() {
Layers layers = IndexedLayers.get(this.context);
this.command.printLayers(layers, this.out);
assertThat(this.out).hasSameContentAsResource("list-output.txt");
assertThat(this.out).hasSameContentAsResource("list-output-without-deprecation.txt");
}
private File createJarFile(String name) throws Exception {
@ -117,9 +113,7 @@ class ListCommandTests { @@ -117,9 +113,7 @@ class ListCommandTests {
}
private String getFile(String fileName) throws Exception {
ClassPathResource resource = new ClassPathResource(fileName, getClass());
InputStreamReader reader = new InputStreamReader(resource.getInputStream());
return FileCopyUtils.copyToString(reader);
return new ClassPathResource(fileName, getClass()).getContentAsString(StandardCharsets.UTF_8);
}
}

51
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ListLayersCommandTests.java

@ -0,0 +1,51 @@ @@ -0,0 +1,51 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.io.File;
import java.io.IOException;
import java.util.jar.Manifest;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ListLayersCommand}.
*
* @author Moritz Halbritter
*/
class ListLayersCommandTests extends AbstractTests {
@Test
void shouldListLayers() throws IOException {
Manifest manifest = createManifest("Spring-Boot-Layers-Index: META-INF/layers.idx");
TestPrintStream out = run(createArchive(manifest, "META-INF/layers.idx", "/jar-contents/layers.idx"));
assertThat(out).hasSameContentAsResource("list-layers-output.txt");
}
@Test
void shouldPrintErrorWhenLayersAreNotEnabled() throws IOException {
TestPrintStream out = run(createArchive());
assertThat(out).hasSameContentAsResource("list-layers-output-layers-disabled.txt");
}
private TestPrintStream run(File archive) {
return runCommand(ListLayersCommand::new, archive);
}
}

40
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestCommand.java

@ -0,0 +1,40 @@ @@ -0,0 +1,40 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.io.PrintStream;
import java.util.List;
import java.util.Map;
/**
* @author Moritz Halbritter
*/
class TestCommand extends Command {
TestCommand() {
super("test", "Description of test",
Options.of(Option.of("option1", "value1", "Description of option1"),
Option.of("option2", "value2", "Description of option2")),
Parameters.of("parameter1", "parameter2"));
}
@Override
protected void run(PrintStream out, Map<Option, String> options, List<String> parameters) {
}
}

6
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/java/org/springframework/boot/jarmode/layertools/TestPrintStream.java → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/TestPrintStream.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.jarmode.layertools;
package org.springframework.boot.jarmode.tools;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@ -27,7 +27,7 @@ import org.assertj.core.api.AbstractAssert; @@ -27,7 +27,7 @@ import org.assertj.core.api.AbstractAssert;
import org.assertj.core.api.AssertProvider;
import org.assertj.core.api.Assertions;
import org.springframework.boot.jarmode.layertools.TestPrintStream.PrintStreamAssert;
import org.springframework.boot.jarmode.tools.TestPrintStream.PrintStreamAssert;
import org.springframework.util.FileCopyUtils;
/**

102
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/java/org/springframework/boot/jarmode/tools/ToolsJarModeTests.java

@ -0,0 +1,102 @@ @@ -0,0 +1,102 @@
/*
* Copyright 2012-2024 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.jarmode.tools;
import java.io.IOException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ToolsJarMode}.
*
* @author Moritz Halbritter
*/
class ToolsJarModeTests extends AbstractTests {
private ToolsJarMode mode;
private TestPrintStream out;
@BeforeEach
void setUp() throws IOException {
this.out = new TestPrintStream(this);
Context context = new Context(createArchive(), this.tempDir);
this.mode = new ToolsJarMode(context, this.out);
}
@Test
void shouldAcceptToolsMode() {
assertThat(this.mode.accepts("tools")).isTrue();
assertThat(this.mode.accepts("something-else")).isFalse();
}
@Test
void noParametersShowsHelp() {
run();
assertThat(this.out).hasSameContentAsResource("tools-help-output.txt");
}
@Test
void helpForExtract() {
run("help", "extract");
assertThat(this.out).hasSameContentAsResource("tools-help-extract-output.txt");
}
@Test
void helpForListLayers() {
run("help", "list-layers");
assertThat(this.out).hasSameContentAsResource("tools-help-list-layers-output.txt");
}
@Test
void helpForHelp() {
run("help", "help");
assertThat(this.out).hasSameContentAsResource("tools-help-help-output.txt");
}
@Test
void helpForUnknownCommand() {
run("help", "unknown-command");
assertThat(this.out).hasSameContentAsResource("tools-help-unknown-command-output.txt");
}
@Test
void unknownCommandShowsErrorAndHelp() {
run("something-invalid");
assertThat(this.out).hasSameContentAsResource("tools-error-command-unknown-output.txt");
}
@Test
void unknownOptionShowsErrorAndCommandHelp() {
run("extract", "--something-invalid");
assertThat(this.out).hasSameContentAsResource("tools-error-option-unknown-output.txt");
}
@Test
void optionMissingRequiredValueShowsErrorAndCommandHelp() {
run("extract", "--destination");
assertThat(this.out).hasSameContentAsResource("tools-error-option-missing-value-output.txt");
}
private void run(String... args) {
this.mode.run("tools", args);
}
}

0
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/JarLauncher

1
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/application.properties

@ -0,0 +1 @@ @@ -0,0 +1 @@
spring.application.name=test

3
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/classpath.idx

@ -0,0 +1,3 @@ @@ -0,0 +1,3 @@
- "BOOT-INF/lib/dependency-1.jar"
- "BOOT-INF/lib/dependency-2.jar"
- "BOOT-INF/lib/dependency-3-SNAPSHOT.jar"

0
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-1

0
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-2

0
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/dependency-3-SNAPSHOT

12
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/jar-contents/layers.idx

@ -0,0 +1,12 @@ @@ -0,0 +1,12 @@
- "dependencies":
- "BOOT-INF/lib/dependency-1.jar"
- "BOOT-INF/lib/dependency-2.jar"
- "spring-boot-loader":
- "org/"
- "snapshot-dependencies":
- "BOOT-INF/lib/dependency-3-SNAPSHOT.jar"
- "application":
- "BOOT-INF/classes/"
- "BOOT-INF/classpath.idx"
- "BOOT-INF/layers.idx"
- "META-INF/"

2
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/ExtractCommand-printErrorIfLayersAreNotEnabled.txt

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
Error: Layers are not enabled

6
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-output.txt

@ -0,0 +1,6 @@ @@ -0,0 +1,6 @@
Usage:
java -Djarmode=tools -jar test.jar
Available commands:
test Description of test
help Help about any command

8
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/help-test-output.txt

@ -0,0 +1,8 @@ @@ -0,0 +1,8 @@
Description of test
Usage:
java -Djarmode=tools -jar test.jar test [options] parameter1 parameter2
Options:
--option1 value1 Description of option1
--option2 value2 Description of option2

3
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-command-unknown-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-command-unknown-output.txt

@ -4,6 +4,7 @@ Usage: @@ -4,6 +4,7 @@ Usage:
java -Djarmode=layertools -jar test.jar
Available commands:
help Help about any command
Deprecated commands:
list List layers from the jar that can be extracted
extract Extracts layers from the jar for image creation
help Help about any command

2
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-missing-value-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-missing-value-output.txt

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
Warning: This command is deprecated. Use '-Djarmode=tools extract --layers --launcher' instead.
Error: Option "--destination" for the extract command requires a value
Extracts layers from the jar for image creation

2
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/error-option-unknown-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-error-option-unknown-output.txt

@ -1,3 +1,5 @@ @@ -1,3 +1,5 @@
Warning: This command is deprecated. Use '-Djarmode=tools extract --layers --launcher' instead.
Error: Unknown option "--invalid" for the extract command
Extracts layers from the jar for image creation

3
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/help-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-help-output.txt

@ -2,6 +2,7 @@ Usage: @@ -2,6 +2,7 @@ Usage:
java -Djarmode=layertools -jar test.jar
Available commands:
help Help about any command
Deprecated commands:
list List layers from the jar that can be extracted
extract Extracts layers from the jar for image creation
help Help about any command

5
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/layertools-list-output.txt

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
Warning: This command is deprecated. Use '-Djarmode=tools list-layers' instead.
0001
0002
0003

2
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output-layers-disabled.txt

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
Error: Layers are not enabled

4
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-layers-output.txt

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
dependencies
spring-boot-loader
snapshot-dependencies
application

0
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/list-output.txt → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/list-output-without-deprecation.txt

0
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-layers.idx → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-layers.idx

0
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-manifest.MF → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-manifest.MF

0
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-war-layers.idx → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-war-layers.idx

0
spring-boot-project/spring-boot-tools/spring-boot-jarmode-layertools/src/test/resources/org/springframework/boot/jarmode/layertools/test-war-manifest.MF → spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/test-war-manifest.MF

9
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-command-unknown-output.txt

@ -0,0 +1,9 @@ @@ -0,0 +1,9 @@
Error: Unknown command "something-invalid"
Usage:
java -Djarmode=tools -jar test.jar
Available commands:
extract Extract the contents from the jar
list-layers List layers from the jar that can be extracted
help Help about any command

13
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-missing-value-output.txt

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
Error: Option "--destination" for the extract command requires a value
Extract the contents from the jar
Usage:
java -Djarmode=tools -jar test.jar extract [options]
Options:
--launcher Whether to extract the Spring Boot launcher
--layers string list Layers to extract
--destination string Directory to extract files to. Defaults to the current working directory
--libraries string Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/
--runner-filename string Name of the runner JAR file. Only applicable when not using --launcher. Defaults to runner.jar

13
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-error-option-unknown-output.txt

@ -0,0 +1,13 @@ @@ -0,0 +1,13 @@
Error: Unknown option "--something-invalid" for the extract command
Extract the contents from the jar
Usage:
java -Djarmode=tools -jar test.jar extract [options]
Options:
--launcher Whether to extract the Spring Boot launcher
--layers string list Layers to extract
--destination string Directory to extract files to. Defaults to the current working directory
--libraries string Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/
--runner-filename string Name of the runner JAR file. Only applicable when not using --launcher. Defaults to runner.jar

11
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-extract-output.txt

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
Extract the contents from the jar
Usage:
java -Djarmode=tools -jar test.jar extract [options]
Options:
--launcher Whether to extract the Spring Boot launcher
--layers string list Layers to extract
--destination string Directory to extract files to. Defaults to the current working directory
--libraries string Name of the libraries directory. Only applicable when not using --launcher. Defaults to lib/
--runner-filename string Name of the runner JAR file. Only applicable when not using --launcher. Defaults to runner.jar

4
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-help-output.txt

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
Help about any command
Usage:
java -Djarmode=tools -jar test.jar help [<command>]

4
spring-boot-project/spring-boot-tools/spring-boot-jarmode-tools/src/test/resources/org/springframework/boot/jarmode/tools/tools-help-list-layers-output.txt

@ -0,0 +1,4 @@ @@ -0,0 +1,4 @@
List layers from the jar that can be extracted
Usage:
java -Djarmode=tools -jar test.jar list-layers

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save