1
2
3
4
5
6 package dev.metaschema.cli.processor;
7
8 import static org.jline.jansi.Ansi.ansi;
9
10 import org.apache.commons.cli.Option;
11 import org.apache.logging.log4j.Level;
12 import org.apache.logging.log4j.LogManager;
13 import org.apache.logging.log4j.Logger;
14 import org.apache.logging.log4j.core.LoggerContext;
15 import org.apache.logging.log4j.core.config.Configuration;
16 import org.apache.logging.log4j.core.config.LoggerConfig;
17 import org.eclipse.jdt.annotation.NotOwning;
18 import org.jline.jansi.Ansi;
19
20 import java.io.PrintStream;
21 import java.util.Arrays;
22 import java.util.LinkedList;
23 import java.util.List;
24 import java.util.Map;
25 import java.util.function.Function;
26 import java.util.stream.Collectors;
27
28 import dev.metaschema.cli.processor.command.CommandService;
29 import dev.metaschema.cli.processor.command.ICommand;
30 import dev.metaschema.core.util.CollectionUtil;
31 import dev.metaschema.core.util.IVersionInfo;
32 import dev.metaschema.core.util.ObjectUtils;
33 import edu.umd.cs.findbugs.annotations.NonNull;
34 import edu.umd.cs.findbugs.annotations.Nullable;
35
36
37
38
39
40
41
42 public class CLIProcessor {
43 private static final Logger LOGGER = LogManager.getLogger(CLIProcessor.class);
44
45
46
47
48 @NonNull
49 public static final Option HELP_OPTION = ObjectUtils.notNull(Option.builder("h")
50 .longOpt("help")
51 .desc("display this help message")
52 .get());
53
54
55
56 @NonNull
57 public static final Option NO_COLOR_OPTION = ObjectUtils.notNull(Option.builder()
58 .longOpt("no-color")
59 .desc("do not colorize output")
60 .get());
61
62
63
64 @NonNull
65 public static final Option QUIET_OPTION = ObjectUtils.notNull(Option.builder("q")
66 .longOpt("quiet")
67 .desc("minimize output to include only errors")
68 .get());
69
70
71
72
73 @NonNull
74 public static final Option SHOW_STACK_TRACE_OPTION = ObjectUtils.notNull(Option.builder()
75 .longOpt("show-stack-trace")
76 .desc("display the stack trace associated with an error")
77 .get());
78
79
80
81 @NonNull
82 public static final Option VERSION_OPTION = ObjectUtils.notNull(Option.builder()
83 .longOpt("version")
84 .desc("display the application version")
85 .get());
86
87 @NonNull
88 static final List<Option> OPTIONS = ObjectUtils.notNull(List.of(
89 HELP_OPTION,
90 NO_COLOR_OPTION,
91 QUIET_OPTION,
92 SHOW_STACK_TRACE_OPTION,
93 VERSION_OPTION));
94
95
96
97
98 public static final String COMMAND_VERSION = "http://csrc.nist.gov/ns/metaschema-java/cli/command-version";
99
100 @NonNull
101 private final List<ICommand> commands = new LinkedList<>();
102 @NonNull
103 private final String exec;
104 @NonNull
105 private final Map<String, IVersionInfo> versionInfos;
106 @NonNull
107 @NotOwning
108 private final PrintStream outputStream;
109
110
111
112
113
114
115
116 public static void main(String... args) {
117 System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager");
118 CLIProcessor processor = new CLIProcessor("metaschema-cli");
119
120 CommandService.getInstance().getCommands().stream().forEach(command -> {
121 assert command != null;
122 processor.addCommandHandler(command);
123 });
124 System.exit(processor.process(args).getExitCode().getStatusCode());
125 }
126
127
128
129
130
131
132
133
134
135 public CLIProcessor(@NonNull String args) {
136 this(args, CollectionUtil.singletonMap(COMMAND_VERSION, new ProcessorVersion()));
137 }
138
139
140
141
142
143
144
145
146
147
148
149 public CLIProcessor(@NonNull String exec, @NonNull Map<String, IVersionInfo> versionInfos) {
150 this(exec, versionInfos, null);
151 }
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168 @SuppressWarnings("resource")
169 public CLIProcessor(@NonNull String exec, @NonNull Map<String, IVersionInfo> versionInfos,
170 @Nullable @NotOwning PrintStream outputStream) {
171 this.exec = exec;
172 this.versionInfos = versionInfos;
173
174
175 this.outputStream = outputStream != null ? outputStream : ObjectUtils.notNull(System.out);
176 }
177
178
179
180
181
182
183 @NonNull
184 public String getExec() {
185 return exec;
186 }
187
188
189
190
191
192
193 @NonNull
194 public Map<String, IVersionInfo> getVersionInfos() {
195 return versionInfos;
196 }
197
198
199
200
201
202
203
204 public void addCommandHandler(@NonNull ICommand handler) {
205 commands.add(handler);
206 }
207
208
209
210
211
212
213
214
215
216
217 @NonNull
218 public ExitStatus process(String... args) {
219 return parseCommand(args);
220 }
221
222 @NonNull
223 private ExitStatus parseCommand(String... args) {
224 List<String> commandArgs = Arrays.asList(args);
225 assert commandArgs != null;
226 CallingContext callingContext = new CallingContext(this, commandArgs);
227
228 if (LOGGER.isDebugEnabled()) {
229 String commandChain = callingContext.getCalledCommands().stream()
230 .map(ICommand::getName)
231 .collect(Collectors.joining(" -> "));
232 LOGGER.debug("Processing command chain: {}", commandChain);
233 }
234
235 ExitStatus status;
236
237
238
239 if (commandArgs.isEmpty()) {
240 status = ExitCode.INVALID_COMMAND.exit();
241 callingContext.showHelp();
242 } else {
243 status = callingContext.processCommand();
244 }
245 return status;
246 }
247
248
249
250
251
252
253 @NonNull
254 public final List<ICommand> getTopLevelCommands() {
255 return CollectionUtil.unmodifiableList(commands);
256 }
257
258
259
260
261
262
263 @NonNull
264 protected final Map<String, ICommand> getTopLevelCommandsByName() {
265 return ObjectUtils.notNull(getTopLevelCommands()
266 .stream()
267 .collect(Collectors.toUnmodifiableMap(ICommand::getName, Function.identity())));
268 }
269
270
271
272
273
274
275
276
277
278 static void handleNoColor() {
279 Ansi.setEnabled(false);
280 }
281
282
283
284
285
286
287
288
289 @NonNull
290 @NotOwning
291 PrintStream getOutputStream() {
292 return outputStream;
293 }
294
295
296
297
298 static void handleQuiet() {
299 LoggerContext ctx = (LoggerContext) LogManager.getContext(false);
300 Configuration config = ctx.getConfiguration();
301 LoggerConfig loggerConfig = config.getLoggerConfig(LogManager.ROOT_LOGGER_NAME);
302 Level oldLevel = loggerConfig.getLevel();
303 if (oldLevel.isLessSpecificThan(Level.ERROR)) {
304 loggerConfig.setLevel(Level.ERROR);
305 ctx.updateLoggers();
306 }
307 }
308
309
310
311
312 protected void showVersion() {
313 getVersionInfos().values().stream().forEach(info -> {
314 outputStream.println(ansi()
315 .bold().a(info.getName()).boldOff()
316 .a(" ")
317 .bold().a(info.getVersion()).boldOff()
318 .a(" built at ")
319 .bold().a(info.getBuildTimestamp()).boldOff()
320 .a(" from branch ")
321 .bold().a(info.getGitBranch()).boldOff()
322 .a(" (")
323 .bold().a(info.getGitCommit()).boldOff()
324 .a(") at ")
325 .bold().a(info.getGitOriginUrl()).boldOff()
326 .reset());
327 });
328 outputStream.flush();
329 }
330 }