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.rat.commandline;
20
21 import java.io.File;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collections;
25 import java.util.List;
26 import java.util.Objects;
27 import java.util.function.Supplier;
28
29 import org.apache.commons.cli.AlreadySelectedException;
30 import org.apache.commons.cli.CommandLine;
31 import org.apache.commons.cli.Option;
32 import org.apache.commons.cli.OptionGroup;
33 import org.apache.commons.cli.Options;
34 import org.apache.commons.cli.ParseException;
35 import org.apache.commons.lang3.StringUtils;
36 import org.apache.rat.ConfigurationException;
37 import org.apache.rat.OptionCollectionParser;
38 import org.apache.rat.ReportConfiguration;
39 import org.apache.rat.document.DocumentName;
40 import org.apache.rat.ui.UIOptionCollection;
41 import org.apache.rat.utils.DefaultLog;
42 import org.apache.rat.utils.Log;
43
44 import static java.lang.String.format;
45
46 /**
47 * Provides the context necessary to process various arguments.
48 * @since 0.17
49 */
50 public final class ArgumentContext {
51 /** The report configuration that is being built */
52 private final ReportConfiguration configuration;
53 /** The command line that is building the configuration */
54 private final CommandLine commandLine;
55 /** The directory from which relative file names will be resolved */
56 private final DocumentName workingDirectory;
57
58 /**
59 * Creates a context with the specified configuration.
60 * @param workingDirectory the directory from which relative file names will be resolved.
61 * @param configuration the configuration that is being built.
62 * @param opts the Options for the command line.
63 * @param args the arguments for the options.
64 * @throws ParseException if the options can not parse the arguments.
65 */
66 public ArgumentContext(final File workingDirectory, final ReportConfiguration configuration, final Options opts, final String[] args)
67 throws ParseException {
68 this.workingDirectory = DocumentName.builder(workingDirectory).build();
69 this.commandLine = OptionCollectionParser.parseCommandLine(clearSelected(opts), args);
70 this.configuration = configuration;
71 }
72
73 /**
74 * Creates a context with an empty configuration.
75 * @param workingDirectory the directory from which to resolve relative file names.
76 * @param opts the Options for the command line.
77 * @param args the arguments for the options.
78 * @throws ParseException if the options can not parse the arguments.
79 */
80 public ArgumentContext(final File workingDirectory, final Options opts, final String[] args) throws ParseException {
81 this(workingDirectory, new ReportConfiguration(), opts, args);
82 }
83
84 /**
85 * Gets a logging/debug string describing the Option.
86 * @param option the option to describe.
87 * @return the logging/debug string describing the Option.
88 */
89 public static String toString(final Option option) {
90
91 Supplier<String> values = () -> {
92 if (!option.hasArg()) {
93 return "";
94 }
95 String[] optValues = option.getValues();
96 if (optValues == null || optValues.length == 0) {
97 return " v:[]";
98 }
99 return " v:['" + String.join("' '", optValues) + "']";
100 };
101
102 return String.format("Option[%s%s]",
103 StringUtils.defaultIfEmpty(option.getLongOpt(), option.getOpt()),
104 values.get());
105 }
106 /**
107 * Clears the group selections in the options.
108 * @param options the options to clear.
109 * @return the options with all selections cleared.
110 */
111 private static Options clearSelected(final Options options) {
112 for (Option opt : options.getOptions()) {
113 OptionGroup group = options.getOptionGroup(opt);
114 if (group != null) {
115 try {
116 group.setSelected(null);
117 } catch (AlreadySelectedException e) {
118 throw new RuntimeException("Unexpected error with null argument", e);
119 }
120 }
121 }
122 return options;
123 }
124
125 /**
126 * Process the arguments specified in this context.
127 */
128 public void processArgs(final UIOptionCollection<?> uiOptionCollection) {
129 Arg.processArgs(this, uiOptionCollection);
130 }
131
132 /**
133 * Gets the configuration.
134 * @return the configuration that is being built.
135 */
136 public ReportConfiguration getConfiguration() {
137 return configuration;
138 }
139
140 /**
141 * Gets the Options from the command line.
142 * @return the Options from the command line.
143 */
144 public Option[] getOptions() {
145 return commandLine.getOptions();
146 }
147
148 /**
149 * Gets the arguments that followed all options on the command line.
150 * @return the arguments that followed all options.
151 */
152 public List<String> getArgs() {
153 return commandLine.getArgList();
154 }
155
156 /**
157 * Determines if option was specified on the command line.
158 * @param option the option to check.
159 * @return {@code true}, if option was specified on the command line.
160 */
161 public boolean hasOption(final Option option) {
162 return commandLine.hasOption(option);
163 }
164
165 /**
166 * Determines if option was specified on the command line.
167 * @param option the option to check.
168 * @return {@code true}, if option was specified on the command line.
169 */
170 public boolean hasOption(final String option) {
171 return commandLine.hasOption(option);
172 }
173
174 /**
175 * Gets the option value or {@code null} if it is not set on the command line.
176 * @param selected the option to retrieve the value for.
177 * @return the option value or {@code null} if it is not set.
178 */
179 public String getOptionValue(final Option selected) {
180 return commandLine.getOptionValue(selected);
181 }
182
183 /**
184 * Gets the list of option values from the command line.
185 * @param selected the option to get values for.
186 * @return The list of options from the command line. Might be an empty list but never {@code null}.
187 */
188 public List<String> getOptionValues(final Option selected) {
189 String[] result = commandLine.getOptionValues(selected);
190 return result == null ? Collections.emptyList() : List.of(result);
191 }
192
193 /**
194 * Gets the parsed option value from the command line.
195 * @param selected the option to get value for.
196 * @return the parsed value or {@code null} if not found.
197 * @param <T> the expected parsed value type.
198 */
199 public <T> T getParsedOptionValue(final Option selected) {
200 return this.getParsedOptionValue(selected, () -> null);
201 }
202
203 /**
204 * Gets the parsed option value from the command line.
205 * @param selected the option to get value for.
206 * @param defaultSupplier a supplier of default values.
207 * @return the parsed value or default value if not found.
208 * @param <T> the expected parsed value type.
209 */
210 public <T> T getParsedOptionValue(final Option selected, final Supplier<T> defaultSupplier) {
211 Objects.requireNonNull(selected);
212 Objects.requireNonNull(defaultSupplier);
213 try {
214 return commandLine.getParsedOptionValue(selected, defaultSupplier);
215 } catch (ParseException e) {
216 logParseException(e, selected);
217 throw new ConfigurationException(format("'%s' converter '%s' does not produce a class of type %s",
218 toString(selected),
219 selected.getConverter().getClass().getName(),
220 selected.getType()), e);
221 }
222 }
223
224 /**
225 * Gets the parsed option values from the command line.
226 * @param selected the option to get value for.
227 * @return the parsed value list an empty list if not found.
228 * @param <T> the expected parsed value type.
229 */
230 public <T> List<T> getParsedOptionValues(final Option selected) {
231 return this.getParsedOptionValues(selected, Collections::emptyList);
232 }
233
234 /**
235 * Gets the parsed option values from the command line.
236 * @param selected the option to get value for.
237 * @param defaultSupplier a supplier of default value list.
238 * @return the parsed value list or default value if not found.
239 * @param <T> the expected parsed value type.
240 */
241 public <T> List<T> getParsedOptionValues(final Option selected, final Supplier<List<T>> defaultSupplier) {
242 Objects.requireNonNull(selected);
243 Objects.requireNonNull(defaultSupplier);
244
245 List<String> strings = getOptionValues(selected);
246 if (strings.isEmpty()) {
247 return defaultSupplier.get();
248 }
249 List<T> result = new ArrayList<>();
250 for (String value : strings) {
251 try {
252 result.add((T) selected.getConverter().apply(value));
253 } catch (Throwable e) { // NOSONAR - need to catch Throwable here due to CommonsCLI's API
254 if (e instanceof Error err) {
255 throw err;
256 }
257 throw new ConfigurationException(format("'%s' converter '%s' does not produce a class of type %s",
258 toString(selected),
259 selected.getConverter().getClass().getName(),
260 selected.getType()), e);
261 }
262 }
263 return result;
264 }
265
266 /**
267 * Gets the directory name from which relative file names will be resolved.
268 * @return the directory name from which relative file names will be resolved.
269 */
270 public DocumentName getWorkingDirectory() {
271 return workingDirectory;
272 }
273
274 /**
275 * Logs a ParseException as a warning and the exception itself as a debug.
276 * @param exception the parse exception to log.
277 * @param opt the option being processed.
278 */
279 public void logParseException(final ParseException exception, final Option opt) {
280 DefaultLog.getInstance().warn(format("Invalid %s specifically: %s ", toString(opt),
281 exception.getMessage()));
282 if (DefaultLog.getInstance().isEnabled(Log.Level.DEBUG)) {
283 DefaultLog.getInstance().debug(exception);
284 }
285 }
286
287 /**
288 * Creates a visual description of the CommandLine.
289 * This used to be provided by the {@code CommandLine.toString()}
290 * method, but that has been removed.
291 * @return a String representation of the command line suitable for debugging.
292 */
293 public String commandLineDescription() {
294 List<String> options = Arrays.stream(commandLine.getOptions())
295 .map(ArgumentContext::toString)
296 .toList();
297
298 return "CommandLine[ options: " + options +
299 " args: " + commandLine.getArgList().toString() +
300 " ]";
301 }
302 }