Skip to content

Commit 9a08358

Browse files
Improve validation of layertools input
This commit improves the validation performed on the user input provided to the layertools jarmode to provide more clear error messages when the input is not correct and reduce the chance of ambiguity. Fixes gh-22042
1 parent a2f7ce0 commit 9a08358

File tree

9 files changed

+176
-5
lines changed

9 files changed

+176
-5
lines changed

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
* A command that can be launched from the layertools jarmode.
3131
*
3232
* @author Phillip Webb
33+
* @author Scott Frederick
3334
*/
3435
abstract class Command {
3536

@@ -192,6 +193,7 @@ private Option find(String arg) {
192193
return candidate;
193194
}
194195
}
196+
throw new UnknownOptionException(name);
195197
}
196198
return null;
197199
}
@@ -285,7 +287,13 @@ String getDescription() {
285287
}
286288

287289
private String claimArg(Deque<String> args) {
288-
return (this.valueDescription != null) ? args.removeFirst() : null;
290+
if (this.valueDescription != null) {
291+
if (args.isEmpty()) {
292+
throw new MissingValueException(this.name);
293+
}
294+
return args.removeFirst();
295+
}
296+
return null;
289297
}
290298

291299
@Override

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

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
* {@link JarMode} providing {@code "layertools"} support.
3030
*
3131
* @author Phillip Webb
32+
* @author Scott Frederick
3233
* @since 2.3.0
3334
*/
3435
public class LayerToolsJarMode implements JarMode {
@@ -63,22 +64,48 @@ static class Runner {
6364
}
6465

6566
private void run(String[] args) {
66-
run(new ArrayDeque<>(Arrays.asList(args)));
67+
run(dequeOf(args));
6768
}
6869

6970
private void run(Deque<String> args) {
7071
if (!args.isEmpty()) {
71-
Command command = Command.find(this.commands, args.removeFirst());
72+
String commandName = args.removeFirst();
73+
Command command = Command.find(this.commands, commandName);
7274
if (command != null) {
73-
command.run(args);
75+
runCommand(command, args);
7476
return;
7577
}
78+
printError("Unknown command \"" + commandName + "\"");
7679
}
7780
this.help.run(args);
7881
}
7982

83+
private void runCommand(Command command, Deque<String> args) {
84+
try {
85+
command.run(args);
86+
}
87+
catch (UnknownOptionException ex) {
88+
printError("Unknown option \"" + ex.getMessage() + "\" for the " + command.getName() + " command");
89+
this.help.run(dequeOf(command.getName()));
90+
}
91+
catch (MissingValueException ex) {
92+
printError("Option \"" + ex.getMessage() + "\" for the " + command.getName()
93+
+ " command requires a value");
94+
this.help.run(dequeOf(command.getName()));
95+
}
96+
}
97+
98+
private void printError(String errorMessage) {
99+
System.out.println("Error: " + errorMessage);
100+
System.out.println();
101+
}
102+
103+
private Deque<String> dequeOf(String... args) {
104+
return new ArrayDeque<>(Arrays.asList(args));
105+
}
106+
80107
static List<Command> getCommands(Context context) {
81-
List<Command> commands = new ArrayList<Command>();
108+
List<Command> commands = new ArrayList<>();
82109
commands.add(new ListCommand(context));
83110
commands.add(new ExtractCommand(context));
84111
return Collections.unmodifiableList(commands);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.jarmode.layertools;
18+
19+
/**
20+
* Exception thrown when a required value is not provided for an option.
21+
*
22+
* @author Scott Frederick
23+
*/
24+
class MissingValueException extends RuntimeException {
25+
26+
private final String optionName;
27+
28+
MissingValueException(String optionName) {
29+
this.optionName = optionName;
30+
}
31+
32+
@Override
33+
public String getMessage() {
34+
return "--" + this.optionName;
35+
}
36+
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
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.jarmode.layertools;
18+
19+
/**
20+
* Exception thrown when an unrecognized option is encountered.
21+
*
22+
* @author Scott Frederick
23+
*/
24+
class UnknownOptionException extends RuntimeException {
25+
26+
private final String optionName;
27+
28+
UnknownOptionException(String optionName) {
29+
this.optionName = optionName;
30+
}
31+
32+
@Override
33+
public String getMessage() {
34+
return "--" + this.optionName;
35+
}
36+
37+
}

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,13 @@
3030

3131
import static org.assertj.core.api.Assertions.as;
3232
import static org.assertj.core.api.Assertions.assertThat;
33+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
3334

3435
/**
3536
* Tests for {@link Command}.
3637
*
3738
* @author Phillip Webb
39+
* @author Scott Frederick
3840
*/
3941
class CommandTests {
4042

@@ -77,6 +79,20 @@ void runWithOptionsAndParametersParsesOptionsAndParameters() {
7779
assertThat(command.getRunParameters()).containsExactly("test2", "test3");
7880
}
7981

82+
@Test
83+
void runWithUnknownOptionThrowsException() {
84+
TestCommand command = new TestCommand("test", VERBOSE_FLAG, LOG_LEVEL_OPTION);
85+
assertThatExceptionOfType(UnknownOptionException.class).isThrownBy(() -> run(command, "--invalid"))
86+
.withMessage("--invalid");
87+
}
88+
89+
@Test
90+
void runWithOptionMissingRequiredValueThrowsException() {
91+
TestCommand command = new TestCommand("test", VERBOSE_FLAG, LOG_LEVEL_OPTION);
92+
assertThatExceptionOfType(MissingValueException.class)
93+
.isThrownBy(() -> run(command, "--verbose", "--log-level")).withMessage("--log-level");
94+
}
95+
8096
@Test
8197
void findWhenNameMatchesReturnsCommand() {
8298
TestCommand test1 = new TestCommand("test1");

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
* Tests for {@link LayerToolsJarMode}.
4040
*
4141
* @author Phillip Webb
42+
* @author Scott Frederick
4243
*/
4344
class LayerToolsJarModeTests {
4445

@@ -79,6 +80,24 @@ void mainWithArgRunsCommand() {
7980
assertThat(this.out).hasSameContentAsResource("list-output.txt");
8081
}
8182

83+
@Test
84+
void mainWithUnknownCommandShowsErrorAndHelp() {
85+
new LayerToolsJarMode().run("layertools", new String[] { "invalid" });
86+
assertThat(this.out).hasSameContentAsResource("error-command-unknown-output.txt");
87+
}
88+
89+
@Test
90+
void mainWithUnknownOptionShowsErrorAndCommandHelp() {
91+
new LayerToolsJarMode().run("layertools", new String[] { "extract", "--invalid" });
92+
assertThat(this.out).hasSameContentAsResource("error-option-unknown-output.txt");
93+
}
94+
95+
@Test
96+
void mainWithOptionMissingRequiredValueShowsErrorAndCommandHelp() {
97+
new LayerToolsJarMode().run("layertools", new String[] { "extract", "--destination" });
98+
assertThat(this.out).hasSameContentAsResource("error-option-missing-value-output.txt");
99+
}
100+
82101
private File createJarFile(String name) throws IOException {
83102
File file = new File(this.temp, name);
84103
try (ZipOutputStream jarOutputStream = new ZipOutputStream(new FileOutputStream(file))) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Error: Unknown command "invalid"
2+
3+
Usage:
4+
java -Djarmode=layertools -jar test.jar
5+
6+
Available commands:
7+
list List layers from the jar that can be extracted
8+
extract Extracts layers from the jar for image creation
9+
help Help about any command
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Error: Option "--destination" for the extract command requires a value
2+
3+
Extracts layers from the jar for image creation
4+
5+
Usage:
6+
java -Djarmode=layertools -jar test.jar extract [options] [<layer>...]
7+
8+
Options:
9+
--destination string The destination to extract files to
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Error: Unknown option "--invalid" for the extract command
2+
3+
Extracts layers from the jar for image creation
4+
5+
Usage:
6+
java -Djarmode=layertools -jar test.jar extract [options] [<layer>...]
7+
8+
Options:
9+
--destination string The destination to extract files to

0 commit comments

Comments
 (0)