View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.cling.invoker;
20  
21  import java.io.PrintWriter;
22  import java.io.StringWriter;
23  import java.util.LinkedHashSet;
24  import java.util.List;
25  import java.util.ListIterator;
26  import java.util.Map;
27  import java.util.Optional;
28  import java.util.Set;
29  import java.util.function.Consumer;
30  import java.util.function.UnaryOperator;
31  
32  import org.apache.commons.cli.CommandLine;
33  import org.apache.commons.cli.DefaultParser;
34  import org.apache.commons.cli.DeprecatedAttributes;
35  import org.apache.commons.cli.HelpFormatter;
36  import org.apache.commons.cli.Option;
37  import org.apache.commons.cli.ParseException;
38  import org.apache.maven.api.cli.Options;
39  import org.apache.maven.api.cli.ParserRequest;
40  import org.apache.maven.api.services.Interpolator;
41  import org.apache.maven.api.services.InterpolatorException;
42  import org.apache.maven.jline.MessageUtils;
43  
44  import static java.util.Objects.requireNonNull;
45  import static org.apache.maven.cling.invoker.CliUtils.createInterpolator;
46  import static org.apache.maven.cling.invoker.CliUtils.toMap;
47  
48  public class CommonsCliOptions implements Options {
49      public static CommonsCliOptions parse(String source, String[] args) throws ParseException {
50          CLIManager cliManager = new CLIManager();
51          return new CommonsCliOptions(source, cliManager, cliManager.parse(args));
52      }
53  
54      protected final String source;
55      protected final CLIManager cliManager;
56      protected final CommandLine commandLine;
57  
58      protected CommonsCliOptions(String source, CLIManager cliManager, CommandLine commandLine) {
59          this.source = requireNonNull(source);
60          this.cliManager = requireNonNull(cliManager);
61          this.commandLine = requireNonNull(commandLine);
62      }
63  
64      @Override
65      public String source() {
66          return source;
67      }
68  
69      @Override
70      public Optional<Map<String, String>> userProperties() {
71          if (commandLine.hasOption(CLIManager.USER_PROPERTY)) {
72              return Optional.of(toMap(commandLine.getOptionProperties(CLIManager.USER_PROPERTY)));
73          }
74          return Optional.empty();
75      }
76  
77      @Override
78      public Optional<Boolean> showVersionAndExit() {
79          if (commandLine.hasOption(CLIManager.SHOW_VERSION_AND_EXIT)) {
80              return Optional.of(Boolean.TRUE);
81          }
82          return Optional.empty();
83      }
84  
85      @Override
86      public Optional<Boolean> showVersion() {
87          if (commandLine.hasOption(CLIManager.SHOW_VERSION)) {
88              return Optional.of(Boolean.TRUE);
89          }
90          return Optional.empty();
91      }
92  
93      @Override
94      public Optional<Boolean> quiet() {
95          if (commandLine.hasOption(CLIManager.QUIET)) {
96              return Optional.of(Boolean.TRUE);
97          }
98          return Optional.empty();
99      }
100 
101     @Override
102     public Optional<Boolean> verbose() {
103         if (commandLine.hasOption(CLIManager.VERBOSE)) {
104             return Optional.of(Boolean.TRUE);
105         }
106         return Optional.empty();
107     }
108 
109     @Override
110     public Optional<Boolean> showErrors() {
111         if (commandLine.hasOption(CLIManager.SHOW_ERRORS) || verbose().orElse(false)) {
112             return Optional.of(Boolean.TRUE);
113         }
114         return Optional.empty();
115     }
116 
117     @Override
118     public Optional<String> failOnSeverity() {
119         if (commandLine.hasOption(CLIManager.FAIL_ON_SEVERITY)) {
120             return Optional.of(commandLine.getOptionValue(CLIManager.FAIL_ON_SEVERITY));
121         }
122         return Optional.empty();
123     }
124 
125     @Override
126     public Optional<Boolean> nonInteractive() {
127         if (commandLine.hasOption(CLIManager.NON_INTERACTIVE) || commandLine.hasOption(CLIManager.BATCH_MODE)) {
128             return Optional.of(Boolean.TRUE);
129         }
130         return Optional.empty();
131     }
132 
133     @Override
134     public Optional<Boolean> forceInteractive() {
135         if (commandLine.hasOption(CLIManager.FORCE_INTERACTIVE)) {
136             return Optional.of(Boolean.TRUE);
137         }
138         return Optional.empty();
139     }
140 
141     @Override
142     public Optional<String> altUserSettings() {
143         if (commandLine.hasOption(CLIManager.ALTERNATE_USER_SETTINGS)) {
144             return Optional.of(commandLine.getOptionValue(CLIManager.ALTERNATE_USER_SETTINGS));
145         }
146         return Optional.empty();
147     }
148 
149     @Override
150     public Optional<String> altProjectSettings() {
151         if (commandLine.hasOption(CLIManager.ALTERNATE_PROJECT_SETTINGS)) {
152             return Optional.of(commandLine.getOptionValue(CLIManager.ALTERNATE_PROJECT_SETTINGS));
153         }
154         return Optional.empty();
155     }
156 
157     @Override
158     public Optional<String> altInstallationSettings() {
159         if (commandLine.hasOption(CLIManager.ALTERNATE_INSTALLATION_SETTINGS)) {
160             return Optional.of(commandLine.getOptionValue(CLIManager.ALTERNATE_INSTALLATION_SETTINGS));
161         }
162         if (commandLine.hasOption(CLIManager.ALTERNATE_GLOBAL_SETTINGS)) {
163             return Optional.of(commandLine.getOptionValue(CLIManager.ALTERNATE_GLOBAL_SETTINGS));
164         }
165         return Optional.empty();
166     }
167 
168     @Override
169     public Optional<String> altUserToolchains() {
170         if (commandLine.hasOption(CLIManager.ALTERNATE_USER_TOOLCHAINS)) {
171             return Optional.of(commandLine.getOptionValue(CLIManager.ALTERNATE_USER_TOOLCHAINS));
172         }
173         return Optional.empty();
174     }
175 
176     @Override
177     public Optional<String> altInstallationToolchains() {
178         if (commandLine.hasOption(CLIManager.ALTERNATE_INSTALLATION_TOOLCHAINS)) {
179             return Optional.of(commandLine.getOptionValue(CLIManager.ALTERNATE_INSTALLATION_TOOLCHAINS));
180         }
181         if (commandLine.hasOption(CLIManager.ALTERNATE_GLOBAL_TOOLCHAINS)) {
182             return Optional.of(commandLine.getOptionValue(CLIManager.ALTERNATE_GLOBAL_TOOLCHAINS));
183         }
184         return Optional.empty();
185     }
186 
187     @Override
188     public Optional<String> logFile() {
189         if (commandLine.hasOption(CLIManager.LOG_FILE)) {
190             return Optional.of(commandLine.getOptionValue(CLIManager.LOG_FILE));
191         }
192         return Optional.empty();
193     }
194 
195     @Override
196     public Optional<Boolean> rawStreams() {
197         if (commandLine.hasOption(CLIManager.RAW_STREAMS)) {
198             return Optional.of(Boolean.TRUE);
199         }
200         return Optional.empty();
201     }
202 
203     @Override
204     public Optional<String> color() {
205         if (commandLine.hasOption(CLIManager.COLOR)) {
206             if (commandLine.getOptionValue(CLIManager.COLOR) != null) {
207                 return Optional.of(commandLine.getOptionValue(CLIManager.COLOR));
208             } else {
209                 return Optional.of("auto");
210             }
211         }
212         return Optional.empty();
213     }
214 
215     @Override
216     public Optional<Boolean> offline() {
217         if (commandLine.hasOption(CLIManager.OFFLINE)) {
218             return Optional.of(Boolean.TRUE);
219         }
220         return Optional.empty();
221     }
222 
223     @Override
224     public Optional<Boolean> help() {
225         if (commandLine.hasOption(CLIManager.HELP)) {
226             return Optional.of(Boolean.TRUE);
227         }
228         return Optional.empty();
229     }
230 
231     @Override
232     public void warnAboutDeprecatedOptions(ParserRequest request, Consumer<String> printWriter) {
233         if (cliManager.getUsedDeprecatedOptions().isEmpty()) {
234             return;
235         }
236         printWriter.accept("Detected deprecated option use in " + source);
237         for (Option option : cliManager.getUsedDeprecatedOptions()) {
238             StringBuilder sb = new StringBuilder();
239             sb.append("The option ");
240             if (option.getOpt() != null) {
241                 sb.append("-").append(option.getOpt());
242             }
243             if (option.getLongOpt() != null) {
244                 if (option.getOpt() != null) {
245                     sb.append(",");
246                 }
247                 sb.append("--").append(option.getLongOpt());
248             }
249             sb.append(" is deprecated ");
250             if (option.getDeprecated().isForRemoval()) {
251                 sb.append("and will be removed in a future version");
252             }
253             if (option.getDeprecated().getSince() != null) {
254                 sb.append(" since ")
255                         .append(request.commandName())
256                         .append(" ")
257                         .append(option.getDeprecated().getSince());
258             }
259             printWriter.accept(sb.toString());
260         }
261     }
262 
263     @Override
264     public final Options interpolate(UnaryOperator<String> callback) {
265         try {
266             // now that we have properties, interpolate all arguments
267             Interpolator interpolator = createInterpolator();
268             CommandLine.Builder commandLineBuilder = CommandLine.builder();
269             commandLineBuilder.setDeprecatedHandler(o -> {});
270             for (Option option : commandLine.getOptions()) {
271                 if (!CommonsCliOptions.CLIManager.USER_PROPERTY.equals(option.getOpt())) {
272                     List<String> values = option.getValuesList();
273                     for (ListIterator<String> it = values.listIterator(); it.hasNext(); ) {
274                         it.set(interpolator.interpolate(it.next(), callback));
275                     }
276                 }
277                 commandLineBuilder.addOption(option);
278             }
279             for (String arg : commandLine.getArgList()) {
280                 commandLineBuilder.addArg(interpolator.interpolate(arg, callback));
281             }
282             return copy(source, cliManager, commandLineBuilder.build());
283         } catch (InterpolatorException e) {
284             throw new IllegalArgumentException("Could not interpolate CommonsCliOptions", e);
285         }
286     }
287 
288     protected CommonsCliOptions copy(String source, CLIManager cliManager, CommandLine commandLine) {
289         return new CommonsCliOptions(source, cliManager, commandLine);
290     }
291 
292     @Override
293     public void displayHelp(ParserRequest request, Consumer<String> printStream) {
294         cliManager.displayHelp(request.command(), printStream);
295     }
296 
297     protected static class CLIManager {
298         public static final String USER_PROPERTY = "D";
299         public static final String SHOW_VERSION_AND_EXIT = "v";
300         public static final String SHOW_VERSION = "V";
301         public static final String QUIET = "q";
302         public static final String VERBOSE = "X";
303 
304         public static final String SHOW_ERRORS = "e";
305 
306         public static final String FAIL_ON_SEVERITY = "fos";
307         public static final String NON_INTERACTIVE = "non-interactive";
308         public static final String BATCH_MODE = "B";
309         public static final String FORCE_INTERACTIVE = "force-interactive";
310         public static final String ALTERNATE_USER_SETTINGS = "s";
311         public static final String ALTERNATE_PROJECT_SETTINGS = "ps";
312         public static final String ALTERNATE_INSTALLATION_SETTINGS = "is";
313         public static final String ALTERNATE_USER_TOOLCHAINS = "t";
314         public static final String ALTERNATE_INSTALLATION_TOOLCHAINS = "it";
315         public static final String LOG_FILE = "l";
316         public static final String RAW_STREAMS = "raw-streams";
317         public static final String COLOR = "color";
318         public static final String OFFLINE = "o";
319         public static final String HELP = "h";
320 
321         // Not an Option: used only for early detection, when CLI args may not be even parsed
322         public static final String SHOW_ERRORS_CLI_ARG = "-" + SHOW_ERRORS;
323 
324         // parameters handled by script
325         public static final String DEBUG = "debug";
326         public static final String ENC = "enc";
327         public static final String UPGRADE = "up";
328         public static final String SHELL = "shell";
329         public static final String YJP = "yjp";
330 
331         // deprecated ones
332         @Deprecated
333         public static final String ALTERNATE_GLOBAL_SETTINGS = "gs";
334 
335         @Deprecated
336         public static final String ALTERNATE_GLOBAL_TOOLCHAINS = "gt";
337 
338         protected org.apache.commons.cli.Options options;
339         protected final LinkedHashSet<Option> usedDeprecatedOptions = new LinkedHashSet<>();
340 
341         @SuppressWarnings("checkstyle:MethodLength")
342         protected CLIManager() {
343             options = new org.apache.commons.cli.Options();
344             prepareOptions(options);
345         }
346 
347         protected void prepareOptions(org.apache.commons.cli.Options options) {
348             options.addOption(Option.builder(HELP)
349                     .longOpt("help")
350                     .desc("Display help information")
351                     .build());
352             options.addOption(Option.builder(USER_PROPERTY)
353                     .numberOfArgs(2)
354                     .valueSeparator('=')
355                     .desc("Define a user property")
356                     .build());
357             options.addOption(Option.builder(SHOW_VERSION_AND_EXIT)
358                     .longOpt("version")
359                     .desc("Display version information")
360                     .build());
361             options.addOption(Option.builder(QUIET)
362                     .longOpt("quiet")
363                     .desc("Quiet output - only show errors")
364                     .build());
365             options.addOption(Option.builder(VERBOSE)
366                     .longOpt("verbose")
367                     .desc("Produce execution verbose output")
368                     .build());
369             options.addOption(Option.builder(SHOW_ERRORS)
370                     .longOpt("errors")
371                     .desc("Produce execution error messages")
372                     .build());
373             options.addOption(Option.builder(BATCH_MODE)
374                     .longOpt("batch-mode")
375                     .desc("Run in non-interactive mode. Alias for --non-interactive (kept for backwards compatability)")
376                     .build());
377             options.addOption(Option.builder()
378                     .longOpt(NON_INTERACTIVE)
379                     .desc("Run in non-interactive mode. Alias for --batch-mode")
380                     .build());
381             options.addOption(Option.builder()
382                     .longOpt(FORCE_INTERACTIVE)
383                     .desc(
384                             "Run in interactive mode. Overrides, if applicable, the CI environment variable and --non-interactive/--batch-mode options")
385                     .build());
386             options.addOption(Option.builder(ALTERNATE_USER_SETTINGS)
387                     .longOpt("settings")
388                     .desc("Alternate path for the user settings file")
389                     .hasArg()
390                     .build());
391             options.addOption(Option.builder(ALTERNATE_PROJECT_SETTINGS)
392                     .longOpt("project-settings")
393                     .desc("Alternate path for the project settings file")
394                     .hasArg()
395                     .build());
396             options.addOption(Option.builder(ALTERNATE_INSTALLATION_SETTINGS)
397                     .longOpt("install-settings")
398                     .desc("Alternate path for the installation settings file")
399                     .hasArg()
400                     .build());
401             options.addOption(Option.builder(ALTERNATE_USER_TOOLCHAINS)
402                     .longOpt("toolchains")
403                     .desc("Alternate path for the user toolchains file")
404                     .hasArg()
405                     .build());
406             options.addOption(Option.builder(ALTERNATE_INSTALLATION_TOOLCHAINS)
407                     .longOpt("install-toolchains")
408                     .desc("Alternate path for the installation toolchains file")
409                     .hasArg()
410                     .build());
411             options.addOption(Option.builder(FAIL_ON_SEVERITY)
412                     .longOpt("fail-on-severity")
413                     .desc("Configure which severity of logging should cause the build to fail")
414                     .hasArg()
415                     .build());
416             options.addOption(Option.builder(LOG_FILE)
417                     .longOpt("log-file")
418                     .hasArg()
419                     .desc("Log file where all build output will go (disables output color)")
420                     .build());
421             options.addOption(Option.builder()
422                     .longOpt(RAW_STREAMS)
423                     .desc("Do not decorate standard output and error streams")
424                     .build());
425             options.addOption(Option.builder(SHOW_VERSION)
426                     .longOpt("show-version")
427                     .desc("Display version information WITHOUT stopping build")
428                     .build());
429             options.addOption(Option.builder()
430                     .longOpt(COLOR)
431                     .hasArg()
432                     .optionalArg(true)
433                     .desc("Defines the color mode of the output. Supported are 'auto', 'always', 'never'.")
434                     .build());
435             options.addOption(Option.builder(OFFLINE)
436                     .longOpt("offline")
437                     .desc("Work offline")
438                     .build());
439 
440             // Parameters handled by script
441             options.addOption(Option.builder()
442                     .longOpt(DEBUG)
443                     .desc("Launch the JVM in debug mode (script option).")
444                     .build());
445             options.addOption(Option.builder()
446                     .longOpt(ENC)
447                     .desc("Launch the Maven Encryption tool (script option).")
448                     .build());
449             options.addOption(Option.builder()
450                     .longOpt(UPGRADE)
451                     .desc("Launch the Maven Upgrade tool (script option).")
452                     .build());
453             options.addOption(Option.builder()
454                     .longOpt(SHELL)
455                     .desc("Launch the Maven Shell tool (script option).")
456                     .build());
457             options.addOption(Option.builder()
458                     .longOpt(YJP)
459                     .desc("Launch the JVM with Yourkit profiler (script option).")
460                     .build());
461 
462             // Deprecated
463             options.addOption(Option.builder(ALTERNATE_GLOBAL_SETTINGS)
464                     .longOpt("global-settings")
465                     .desc("<deprecated> Alternate path for the global settings file.")
466                     .hasArg()
467                     .deprecated(DeprecatedAttributes.builder()
468                             .setForRemoval(true)
469                             .setSince("4.0.0")
470                             .setDescription("Use -is,--install-settings instead.")
471                             .get())
472                     .build());
473             options.addOption(Option.builder(ALTERNATE_GLOBAL_TOOLCHAINS)
474                     .longOpt("global-toolchains")
475                     .desc("<deprecated> Alternate path for the global toolchains file.")
476                     .hasArg()
477                     .deprecated(DeprecatedAttributes.builder()
478                             .setForRemoval(true)
479                             .setSince("4.0.0")
480                             .setDescription("Use -it,--install-toolchains instead.")
481                             .get())
482                     .build());
483         }
484 
485         public CommandLine parse(String[] args) throws ParseException {
486             // We need to eat any quotes surrounding arguments...
487             String[] cleanArgs = CleanArgument.cleanArgs(args);
488             DefaultParser parser = DefaultParser.builder()
489                     .setDeprecatedHandler(this::addDeprecatedOption)
490                     .build();
491             CommandLine commandLine = parser.parse(options, cleanArgs);
492             // to trigger deprecation handler, so we can report deprecation BEFORE we actually use options
493             options.getOptions().forEach(commandLine::hasOption);
494             return commandLine;
495         }
496 
497         protected void addDeprecatedOption(Option option) {
498             usedDeprecatedOptions.add(option);
499         }
500 
501         public org.apache.commons.cli.Options getOptions() {
502             return options;
503         }
504 
505         public Set<Option> getUsedDeprecatedOptions() {
506             return usedDeprecatedOptions;
507         }
508 
509         public void displayHelp(String command, Consumer<String> pw) {
510             HelpFormatter formatter = new HelpFormatter();
511 
512             int width = MessageUtils.getTerminalWidth();
513             if (width <= 0) {
514                 width = HelpFormatter.DEFAULT_WIDTH;
515             }
516 
517             pw.accept("");
518 
519             StringWriter sw = new StringWriter();
520             PrintWriter pw2 = new PrintWriter(sw);
521             formatter.printHelp(
522                     pw2,
523                     width,
524                     commandLineSyntax(command),
525                     System.lineSeparator() + "Options:",
526                     options,
527                     HelpFormatter.DEFAULT_LEFT_PAD,
528                     HelpFormatter.DEFAULT_DESC_PAD,
529                     System.lineSeparator(),
530                     false);
531             pw2.flush();
532             for (String s : sw.toString().split(System.lineSeparator())) {
533                 pw.accept(s);
534             }
535         }
536 
537         protected String commandLineSyntax(String command) {
538             return command + " [options] [goals]";
539         }
540     }
541 }