Skip to content

Commit e513fe4

Browse files
committed
Add layertools jarmode
Add a new `spring-boot-layertools` module which provides jarmode support for working with layers. The module works with both classic fat jars, as well as layered jars. Closes gh-19849
1 parent 73a4205 commit e513fe4

File tree

27 files changed

+1917
-1
lines changed

27 files changed

+1917
-1
lines changed

eclipse/spring-boot-project.setup

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127
name="spring-boot-tools">
128128
<predicate
129129
xsi:type="predicates:NamePredicate"
130-
pattern="spring-boot-(tools|antlib|configuration-.*|loader|.*-tools|.*-plugin|autoconfigure-processor|cloudnativebuildpack)"/>
130+
pattern="spring-boot-(tools|antlib|configuration-.*|loader|.*-tools|*.layertools|.*-plugin|autoconfigure-processor|cloudnativebuildpack)"/>
131131
</workingSet>
132132
<workingSet
133133
name="spring-boot-starters">

settings.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ include 'spring-boot-project:spring-boot-tools:spring-boot-cloudnativebuildpack'
4545
include 'spring-boot-project:spring-boot-tools:spring-boot-configuration-metadata'
4646
include 'spring-boot-project:spring-boot-tools:spring-boot-configuration-processor'
4747
include 'spring-boot-project:spring-boot-tools:spring-boot-gradle-plugin'
48+
include 'spring-boot-project:spring-boot-tools:spring-boot-layertools'
4849
include 'spring-boot-project:spring-boot-tools:spring-boot-loader'
4950
include 'spring-boot-project:spring-boot-tools:spring-boot-loader-tools'
5051
include 'spring-boot-project:spring-boot-tools:spring-boot-maven-plugin'

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1425,6 +1425,7 @@ bom {
14251425
'spring-boot-configuration-metadata',
14261426
'spring-boot-configuration-processor',
14271427
'spring-boot-devtools',
1428+
'spring-boot-layertools',
14281429
'spring-boot-loader',
14291430
'spring-boot-loader-tools',
14301431
'spring-boot-properties-migrator',
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
plugins {
2+
id 'java-library'
3+
id 'org.springframework.boot.conventions'
4+
id 'org.springframework.boot.deployed'
5+
id 'org.springframework.boot.internal-dependency-management'
6+
}
7+
8+
description = 'Spring Boot Layers Tools'
9+
10+
dependencies {
11+
api platform(project(':spring-boot-project:spring-boot-parent'))
12+
13+
implementation project(':spring-boot-project:spring-boot-tools:spring-boot-loader')
14+
implementation project(':spring-boot-project:spring-boot')
15+
implementation 'org.springframework:spring-core'
16+
17+
testImplementation "org.assertj:assertj-core"
18+
testImplementation "org.junit.jupiter:junit-jupiter"
19+
testImplementation "org.mockito:mockito-core"
20+
}
21+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
/*
2+
* Copyright 2012-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.layertools;
18+
19+
import java.util.ArrayList;
20+
import java.util.Arrays;
21+
import java.util.Collection;
22+
import java.util.Collections;
23+
import java.util.Deque;
24+
import java.util.HashMap;
25+
import java.util.List;
26+
import java.util.Map;
27+
import java.util.stream.Stream;
28+
29+
/**
30+
* A command that can be launched from the layertools jarmode.
31+
*
32+
* @author Phillip Webb
33+
*/
34+
abstract class Command {
35+
36+
private final String name;
37+
38+
private final String description;
39+
40+
private final Options options;
41+
42+
private final Parameters parameters;
43+
44+
/**
45+
* Create a new {@link Command} instance.
46+
* @param name the name of the command
47+
* @param description a description of the command
48+
* @param options the command options
49+
* @param parameters the command parameters
50+
*/
51+
Command(String name, String description, Options options, Parameters parameters) {
52+
this.name = name;
53+
this.description = description;
54+
this.options = options;
55+
this.parameters = parameters;
56+
}
57+
58+
/**
59+
* Return the name of this command.
60+
* @return the command name
61+
*/
62+
String getName() {
63+
return this.name;
64+
}
65+
66+
/**
67+
* Return the description of this command.
68+
* @return the command description
69+
*/
70+
String getDescription() {
71+
return this.description;
72+
}
73+
74+
/**
75+
* Return options that this command accepts.
76+
* @return the command options
77+
*/
78+
Options getOptions() {
79+
return this.options;
80+
}
81+
82+
/**
83+
* Return parameters that this command accepts.
84+
* @return the command parameters
85+
*/
86+
Parameters getParameters() {
87+
return this.parameters;
88+
}
89+
90+
/**
91+
* Run the command by processing the remaining arguments.
92+
* @param args a mutable deque of the remaining arguments
93+
*/
94+
final void run(Deque<String> args) {
95+
List<String> parameters = new ArrayList<>();
96+
Map<Option, String> options = new HashMap<>();
97+
while (!args.isEmpty()) {
98+
String arg = args.removeFirst();
99+
Option option = this.options.find(arg);
100+
if (option != null) {
101+
options.put(option, option.claimArg(args));
102+
}
103+
else {
104+
parameters.add(arg);
105+
}
106+
}
107+
run(options, parameters);
108+
}
109+
110+
/**
111+
* Run the actual command.
112+
* @param options any options extracted from the arguments
113+
* @param parameters any parameters extracted from the arguements
114+
*/
115+
protected abstract void run(Map<Option, String> options, List<String> parameters);
116+
117+
/**
118+
* Static method that can be used to find a single command from a collection.
119+
* @param commands the commands to search
120+
* @param name the name of the command to find
121+
* @return a {@link Command} instance or {@code null}.
122+
*/
123+
static Command find(Collection<? extends Command> commands, String name) {
124+
for (Command command : commands) {
125+
if (command.getName().equals(name)) {
126+
return command;
127+
}
128+
}
129+
return null;
130+
}
131+
132+
/**
133+
* Parameters that the command accepts.
134+
*/
135+
protected static final class Parameters {
136+
137+
private final List<String> descriptions;
138+
139+
private Parameters(String[] descriptions) {
140+
this.descriptions = Collections.unmodifiableList(Arrays.asList(descriptions));
141+
}
142+
143+
/**
144+
* Return the parameter descriptions.
145+
* @return the descriptions
146+
*/
147+
List<String> getDescriptions() {
148+
return this.descriptions;
149+
}
150+
151+
@Override
152+
public String toString() {
153+
return this.descriptions.toString();
154+
}
155+
156+
/**
157+
* Factory method used if there are no expected parameters.
158+
* @return a new {@link Parameters} instance
159+
*/
160+
protected static Parameters none() {
161+
return of();
162+
}
163+
164+
/**
165+
* Factory method used to create a new {@link Parameters} instance with specific
166+
* descriptions.
167+
* @param descriptions the parameter descriptions
168+
* @return a new {@link Parameters} instance with the given descriptions
169+
*/
170+
protected static Parameters of(String... descriptions) {
171+
return new Parameters(descriptions);
172+
}
173+
174+
}
175+
176+
/**
177+
* Options that the command accepts.
178+
*/
179+
protected static final class Options {
180+
181+
private final Option[] values;
182+
183+
private Options(Option[] values) {
184+
this.values = values;
185+
}
186+
187+
private Option find(String arg) {
188+
if (arg.startsWith("--")) {
189+
String name = arg.substring(2);
190+
for (Option candidate : this.values) {
191+
if (candidate.getName().equals(name)) {
192+
return candidate;
193+
}
194+
}
195+
}
196+
return null;
197+
}
198+
199+
/**
200+
* Return if this options collection is empty.
201+
* @return if there are no options
202+
*/
203+
boolean isEmpty() {
204+
return this.values.length == 0;
205+
}
206+
207+
/**
208+
* Return a stream of each option.
209+
* @return a stream of the options
210+
*/
211+
Stream<Option> stream() {
212+
return Arrays.stream(this.values);
213+
}
214+
215+
/**
216+
* Factory method used if there are no expected options.
217+
* @return a new {@link Options} instance
218+
*/
219+
protected static Options none() {
220+
return of();
221+
}
222+
223+
/**
224+
* Factory method used to create a new {@link Options} instance with specific
225+
* values.
226+
* @param values the option values
227+
* @return a new {@link Options} instance with the given values
228+
*/
229+
protected static Options of(Option... values) {
230+
return new Options(values);
231+
}
232+
233+
}
234+
235+
/**
236+
* An individual option that the command can accepts. Can either be an option with a
237+
* value (e.g. {@literal --log debug}) or a flag (e.g. {@literal
238+
* --verbose}).
239+
*/
240+
protected static final class Option {
241+
242+
private final String name;
243+
244+
private final String valueDescription;
245+
246+
private final String description;
247+
248+
private Option(String name, String valueDescription, String description) {
249+
this.name = name;
250+
this.description = description;
251+
this.valueDescription = valueDescription;
252+
}
253+
254+
/**
255+
* Return the name of the option.
256+
* @return the options name
257+
*/
258+
String getName() {
259+
return this.name;
260+
}
261+
262+
/**
263+
* Return the description of the expected argument value or {@code null} if this
264+
* option is a flag/switch.
265+
* @return the option value description
266+
*/
267+
String getValueDescription() {
268+
return this.valueDescription;
269+
}
270+
271+
/**
272+
* Return the name and the value description combined.
273+
* @return the name and value description
274+
*/
275+
String getNameAndValueDescription() {
276+
return this.name + ((this.valueDescription != null) ? " " + this.valueDescription : "");
277+
}
278+
279+
/**
280+
* Return a description of the option.
281+
* @return the option description
282+
*/
283+
String getDescription() {
284+
return this.description;
285+
}
286+
287+
private String claimArg(Deque<String> args) {
288+
return (this.valueDescription != null) ? args.removeFirst() : null;
289+
}
290+
291+
@Override
292+
public boolean equals(Object obj) {
293+
if (this == obj) {
294+
return true;
295+
}
296+
if (obj == null || getClass() != obj.getClass()) {
297+
return false;
298+
}
299+
return this.name.equals(((Option) obj).name);
300+
}
301+
302+
@Override
303+
public int hashCode() {
304+
return this.name.hashCode();
305+
}
306+
307+
@Override
308+
public String toString() {
309+
return this.name;
310+
}
311+
312+
/**
313+
* Factory method to create a flag/switch option.
314+
* @param name the name of the option
315+
* @param description a description of the option
316+
* @return a new {@link Option} instance
317+
*/
318+
protected static Option flag(String name, String description) {
319+
return new Option(name, null, description);
320+
}
321+
322+
/**
323+
* Factory method to create value option.
324+
* @param name the name of the option
325+
* @param valueDescription a description of the expected value
326+
* @param description a description of the option
327+
* @return a new {@link Option} instance
328+
*/
329+
protected static Option of(String name, String valueDescription, String description) {
330+
return new Option(name, valueDescription, description);
331+
}
332+
333+
}
334+
335+
}

0 commit comments

Comments
 (0)