-
Notifications
You must be signed in to change notification settings - Fork 2k
/
VertxCommandLauncher.java
446 lines (385 loc) · 13.8 KB
/
VertxCommandLauncher.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
/*
* Copyright (c) 2011-2015 The original author or authors
* ------------------------------------------------------
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* and Apache License v2.0 which accompanies this distribution.
*
* The Eclipse Public License is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* The Apache License v2.0 is available at
* http://www.opensource.org/licenses/apache2.0.php
*
* You may elect to redistribute this code under either of these licenses.
*/
package io.vertx.core.impl.launcher;
import io.vertx.core.cli.*;
import io.vertx.core.cli.annotations.CLIConfigurator;
import io.vertx.core.impl.launcher.commands.RunCommand;
import io.vertx.core.spi.launcher.Command;
import io.vertx.core.spi.launcher.CommandFactory;
import io.vertx.core.spi.launcher.CommandFactoryLookup;
import io.vertx.core.spi.launcher.ExecutionContext;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.net.URL;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.stream.Collectors;
/**
* The entry point of the Vert.x Command Line interface.
*
* @author Clement Escoffier <clement@apache.org>
*/
public class VertxCommandLauncher extends UsageMessageFormatter {
private static List<String> PROCESS_ARGS;
/**
* @return the process argument. Verticles can use this method to retrieve the arguments.
*/
public static List<String> getProcessArguments() {
return PROCESS_ARGS;
}
/**
* the list of lookups.
*/
protected final List<CommandFactoryLookup> lookups;
/**
* the list of commands. Sub-classes can decide to remove commands by removing entries from this map.
*/
protected final Map<String, CommandRegistration> commandByName;
/**
* the {@code Main-Class} object.
*/
protected Object main;
private class CommandRegistration {
public final CommandFactory factory;
public final CLI cli;
private List<Command> commands = new ArrayList<>();
private CommandRegistration(CommandFactory factory, CLI cli) {
Objects.nonNull(factory);
Objects.nonNull(cli);
this.factory = factory;
this.cli = cli;
}
public void addCommand(Command command) {
commands.add(command);
}
public Command getCommand() {
if (!commands.isEmpty()) {
return commands.get(0);
}
return null;
}
public List<Command> getCommands() {
return commands;
}
}
/**
* Creates a new {@link VertxCommandLauncher} using the default {@link ServiceCommandFactoryLoader}.
*/
public VertxCommandLauncher() {
this(Collections.singletonList(new ServiceCommandFactoryLoader()));
}
/**
* Creates a new {@link VertxCommandLauncher} using the given list of {@link CommandFactoryLookup}.
*
* @param lookups the list of lookup
*/
public VertxCommandLauncher(Collection<CommandFactoryLookup> lookups) {
this.lookups = new ArrayList<>(lookups);
this.commandByName = new TreeMap<>();
load();
}
/**
* Loads the command. This method is {@link protected} to let sub-classes change the set of command or how
* they are loaded.
*/
protected void load() {
for (CommandFactoryLookup lookup : lookups) {
Collection<CommandFactory<?>> commands = lookup.lookup();
for (CommandFactory factory : commands) {
CLI cli = factory.define();
commandByName.put(cli.getName(), new CommandRegistration(factory, cli));
}
}
}
/**
* @return the list of command.
*/
public Collection<String> getCommandNames() {
return commandByName.keySet();
}
/**
* Creates a new {@link Command} instance. Sub-classes can change how {@link Command} instance are created.
*
* @param name the command name
* @param commandLine the command line
* @return the new instance, {@code null} if the command cannot be found.
*/
protected Command getNewCommandInstance(String name, CommandLine commandLine) {
CommandRegistration registration = commandByName.get(name);
if (registration != null) {
Command command = registration.factory.create(commandLine);
registration.addCommand(command);
return command;
}
return null;
}
/**
* Gets an existing instance of command.
*
* @param name the command name
* @return the {@link Command} instance, {@code null} if not found
*/
public Command getExistingCommandInstance(String name) {
CommandRegistration registration = commandByName.get(name);
if (registration != null) {
return registration.getCommand();
}
return null;
}
/**
* Executes the given command.
*
* @param command the command name
* @param cla the arguments
*/
public void execute(String command, String... cla) {
if (command != null && isAskingForVersion(command)) {
execute("version");
return;
}
if (command == null || isAskingForHelp(command)) {
printGlobalUsage();
return;
}
CommandRegistration registration = commandByName.get(command);
if (registration == null) {
printCommandNotFound(command);
return;
}
CLI cli = registration.cli;
try {
// Check for help - the command need to have been initialized ot get the complete model.
if (cla.length >= 1 && isAskingForHelp(cla[0])) {
printCommandUsage(cli);
return;
}
// Step 1 - parsing and injection
CommandLine evaluated = cli.parse(Arrays.asList(cla));
Command cmd = getNewCommandInstance(command, evaluated);
ExecutionContext context = new ExecutionContext(cmd, this, evaluated);
if (main != null) {
context.put("Main", main);
context.put("Main-Class", main.getClass().getName());
}
CLIConfigurator.inject(evaluated, cmd);
// Step 2 - validation
cmd.setUp(context);
// Step 3 - execution
cmd.run();
// Step 4 - cleanup
cmd.tearDown();
} catch (MissingOptionException | MissingValueException | InvalidValueException e) {
printSpecificException(cli, e);
} catch (CLIException e) {
printGenericExecutionError(cli, e);
} catch (RuntimeException e) {
if (e.getCause() instanceof CLIException) {
printGenericExecutionError(cli, (CLIException) e.getCause());
return;
}
throw e;
}
}
private void printCommandUsage(CLI cli) {
StringBuilder builder = new StringBuilder();
cli.usage(builder, getCommandLinePrefix());
getPrintStream().println(builder.toString());
}
protected void printGenericExecutionError(CLI cli, CLIException e) {
getPrintStream().println("Error while executing command " + cli.getName() + ": " + e.getMessage() + getNewLine());
if (e.getCause() != null) {
e.getCause().printStackTrace(getPrintStream());
}
}
protected void printSpecificException(CLI cli, Exception e) {
getPrintStream().println(e.getMessage() + getNewLine());
printCommandUsage(cli);
}
protected void printCommandNotFound(String command) {
StringBuilder builder = new StringBuilder();
buildWrapped(builder, 0, "The command '" + command + "' is not a valid command." + getNewLine()
+ "See '" + getCommandLinePrefix() + " --help'");
getPrintStream().println(builder.toString());
}
protected void printGlobalUsage() {
StringBuilder builder = new StringBuilder();
computeUsage(builder, getCommandLinePrefix() + " [COMMAND] [OPTIONS] [arg...]");
builder.append(getNewLine());
builder.append("Commands:").append(getNewLine());
renderCommands(builder, commandByName.values().stream().map(r -> r.cli).collect(Collectors.toList()));
builder.append(getNewLine()).append(getNewLine());
buildWrapped(builder, 0, "Run '" + getCommandLinePrefix() + " COMMAND --help' for more information on a command.");
getPrintStream().println(builder.toString());
}
protected String getCommandLinePrefix() {
// Check whether `vertx.cli.usage.prefix` is set, if so use it. This system property let scripts configure the value
// displayed by the usage, even if they are calling java.
String sysProp = System.getProperty("vertx.cli.usage.prefix");
if (sysProp != null) {
return sysProp;
}
String jar = CommandLineUtils.getJar();
if (jar != null) {
return "java -jar " + jar;
}
String command = CommandLineUtils.getFirstSegmentOfCommand();
if (command != null) {
return "java " + command;
}
return "vertx";
}
private static boolean isAskingForHelp(String command) {
return command.equalsIgnoreCase("--help")
|| command.equalsIgnoreCase("-help")
|| command.equalsIgnoreCase("-h")
|| command.equalsIgnoreCase("?")
|| command.equalsIgnoreCase("/?");
}
private static boolean isAskingForVersion(String command) {
return command.equalsIgnoreCase("-version") || command.equalsIgnoreCase("--version");
}
/**
* Dispatches to the right command. This method is generally called from the {@code main} method.
*
* @param args the command line arguments.
*/
public void dispatch(String[] args) {
dispatch(null, args);
}
/**
* Dispatches to the right command. This method is generally called from the {@code main} method.
*
* @param main the main instance on which hooks and callbacks are going to be called. If not set, the current
* object is used.
* @param args the command line arguments.
*/
public void dispatch(Object main, String[] args) {
this.main = main == null ? this : main;
PROCESS_ARGS = Collections.unmodifiableList(Arrays.asList(args));
// Several cases need to be detected here.
// The first argument may be "--help" => must display help message
// The first argument may be "--version" => must execute the version command.
// The first argument may be a command and the second "--help" => display command usage
// The first argument may be a command => command execution
// If the first argument is not a command, try to see if there is a given main verticle and execute the default
// command with the arguments (prepended with the main verticle).
// Finally, we have two fallbacks
// - if no args (and so no main verticle) - display usage
// - if args has been set, display command usage.
if (args.length >= 1 && isAskingForHelp(args[0])) {
printGlobalUsage();
return;
}
if (args.length >= 1 && isAskingForVersion(args[0])) {
execute("version");
return;
}
if (args.length >= 1 && commandByName.get(args[0]) != null) {
execute(args[0], Arrays.copyOfRange(args, 1, args.length));
return;
}
if (args.length >= 2 && isAskingForHelp(args[1])) {
execute(args[0], "--help");
return;
}
// We check whether or not we have a main verticle specified via the getMainVerticle method.
// By default this method retrieve the value from the 'Main-Verticle' Manifest header. However it can be overridden.
String verticle = getMainVerticle();
if (verticle != null) {
// We have a main verticle, append it to the arg list and execute the default command (run)
String[] newArgs = new String[args.length + 1];
newArgs[0] = verticle;
System.arraycopy(args, 0, newArgs, 1, args.length);
execute(getDefaultCommand(), newArgs);
return;
}
// Fall backs
if (args.length == 0) {
printGlobalUsage();
} else {
// compatibility support
if (args[0].equalsIgnoreCase("-ha")) {
execute("bare", Arrays.copyOfRange(args, 1, args.length));
} else {
printCommandNotFound(args[0]);
}
}
}
/**
* @return the default command if specified in the {@code MANIFEST}, "run" if not found.
*/
protected String getDefaultCommand() {
try {
Enumeration<URL> resources = RunCommand.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
while (resources.hasMoreElements()) {
InputStream stream = resources.nextElement().openStream();
Manifest manifest = new Manifest(stream);
Attributes attributes = manifest.getMainAttributes();
String mainClass = attributes.getValue("Main-Class");
if (main.getClass().getName().equals(mainClass)) {
String command = attributes.getValue("Main-Command");
if (command != null) {
stream.close();
return command;
}
}
stream.close();
}
} catch (IOException e) {
throw new IllegalStateException(e.getMessage());
}
return "run";
}
/**
* @return the printer used to write the messages. Defaults to {@link System#out}.
*/
public PrintStream getPrintStream() {
return System.out;
}
/**
* @return the main verticle, {@code null} if not found.
*/
protected String getMainVerticle() {
try {
Enumeration<URL> resources = RunCommand.class.getClassLoader().getResources("META-INF/MANIFEST.MF");
while (resources.hasMoreElements()) {
InputStream stream = resources.nextElement().openStream();
Manifest manifest = new Manifest(stream);
Attributes attributes = manifest.getMainAttributes();
String mainClass = attributes.getValue("Main-Class");
if (main != null && main.getClass().getName().equals(mainClass)) {
String theMainVerticle = attributes.getValue("Main-Verticle");
if (theMainVerticle != null) {
stream.close();
return theMainVerticle;
}
}
stream.close();
}
} catch (IOException e) {
throw new IllegalStateException(e.getMessage());
}
return null;
}
/**
* For testing purpose only - reset the process arguments
*/
public static void resetProcessArguments() {
PROCESS_ARGS = null;
}
}