1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.rat;
20
21 import java.io.File;
22 import java.io.IOException;
23 import java.io.PrintWriter;
24 import java.io.Serial;
25 import java.io.Serializable;
26 import java.nio.charset.StandardCharsets;
27 import java.util.Arrays;
28 import java.util.Collections;
29 import java.util.Comparator;
30 import java.util.Map;
31 import java.util.Optional;
32 import java.util.TreeMap;
33 import java.util.function.Consumer;
34 import java.util.function.Supplier;
35 import java.util.stream.Collectors;
36
37 import org.apache.commons.cli.DefaultParser;
38 import org.apache.commons.cli.Option;
39 import org.apache.commons.cli.Options;
40 import org.apache.commons.cli.ParseException;
41 import org.apache.rat.api.Document;
42 import org.apache.rat.commandline.Arg;
43 import org.apache.rat.commandline.ArgumentContext;
44 import org.apache.rat.commandline.StyleSheets;
45 import org.apache.rat.config.exclusion.StandardCollection;
46 import org.apache.rat.document.DocumentName;
47 import org.apache.rat.document.DocumentNameMatcher;
48 import org.apache.rat.document.FileDocument;
49 import org.apache.rat.help.Licenses;
50 import org.apache.rat.license.LicenseSetFactory;
51 import org.apache.rat.report.Reportable;
52 import org.apache.rat.report.claim.ClaimStatistic;
53 import org.apache.rat.ui.ArgumentTracker;
54 import org.apache.rat.ui.UIOption;
55 import org.apache.rat.ui.UIOptionCollection;
56 import org.apache.rat.utils.DefaultLog;
57 import org.apache.rat.utils.Log.Level;
58 import org.apache.rat.walker.ArchiveWalker;
59 import org.apache.rat.walker.DirectoryWalker;
60
61 import static java.lang.String.format;
62
63
64
65
66
67 public final class OptionCollection {
68
69 private OptionCollection() {
70
71 }
72
73
74
75
76 private static final UIOptionCollection<? extends UIOption<?>> BASE_OPTION_COLLECTION = new CLIOptionCollection();
77
78
79
80
81 public static final Comparator<Option> OPTION_COMPARATOR = new OptionComparator();
82
83
84 public static final Option HELP = new Option("?", "help", false, "Print help for the RAT command line interface and exit.");
85
86
87 @Deprecated
88 private static final Map<String, Supplier<String>> ARGUMENT_TYPES;
89 static {
90 ARGUMENT_TYPES = new TreeMap<>();
91 for (ArgumentType argType : ArgumentType.values()) {
92 ARGUMENT_TYPES.put(argType.getDisplayName(), argType.description);
93 }
94 }
95
96
97
98
99
100
101 @Deprecated
102 public static Map<String, Supplier<String>> getArgumentTypes() {
103 return Collections.unmodifiableMap(ARGUMENT_TYPES);
104 }
105
106
107
108
109
110
111 private static String asString(final Object[] args) {
112 return Arrays.stream(args).map(Object::toString).collect(Collectors.joining(", "));
113 }
114
115
116
117
118
119
120
121
122
123
124 public static ReportConfiguration parseCommands(final File workingDirectory, final String[] args, final Consumer<Options> helpCmd) throws IOException {
125 return parseCommands(workingDirectory, args, helpCmd, false);
126 }
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148 public static synchronized ReportConfiguration parseCommands(final File workingDirectory, final String[] args,
149 final Consumer<Options> helpCmd, final boolean noArgs) throws IOException {
150 Options opts = buildOptions();
151 ArgumentContext argumentContext;
152 try {
153 argumentContext = new ArgumentContext(workingDirectory, opts, args);
154 } catch (ParseException e) {
155 System.exit(1);
156 return null;
157
158 }
159 Arg.processLogLevel(argumentContext, BASE_OPTION_COLLECTION);
160
161 if (argumentContext.hasOption(HELP)) {
162 helpCmd.accept(opts);
163 return null;
164 }
165
166 if (argumentContext.hasOption(Arg.HELP_LICENSES.option())) {
167 new Licenses(createConfiguration(argumentContext), new PrintWriter(System.out, false, StandardCharsets.UTF_8)).printHelp();
168 return null;
169 }
170
171 ReportConfiguration configuration = createConfiguration(argumentContext);
172 if (!noArgs && !configuration.hasSource()) {
173 String msg = "No directories or files specified for scanning. Did you forget to close a multi-argument option?";
174 DefaultLog.getInstance().error(msg);
175 helpCmd.accept(opts);
176 return null;
177 }
178
179 return configuration;
180 }
181
182
183
184
185
186
187
188
189
190
191 public static ReportConfiguration createConfiguration(final ArgumentContext argumentContext) {
192 try {
193 argumentContext.processArgs(BASE_OPTION_COLLECTION);
194 final ReportConfiguration configuration = argumentContext.getConfiguration();
195 Optional<Option> dirOpt = BASE_OPTION_COLLECTION.getSelected(Arg.DIR);
196 dirOpt.ifPresent(opt -> {
197 File directoryName = argumentContext.getParsedOptionValue(opt);
198 configuration.addSource(getReportable(directoryName, configuration));
199 });
200 for (String s : argumentContext.getArgs()) {
201 Reportable reportable = getReportable(new File(s), configuration);
202 if (reportable != null) {
203 configuration.addSource(reportable);
204 }
205 }
206 return configuration;
207 } catch (RuntimeException e) {
208 try (PrintWriter pw = new PrintWriter(DefaultLog.getInstance().asWriter(Level.ERROR))) {
209 pw.println("Unable to create configuration: " + e.getMessage());
210 pw.println("=== Command line options ===");
211 for (Option opt : argumentContext.getOptions()) {
212 String[] values = opt.getValues();
213 pw.printf(" %s: %s%n", ArgumentTracker.extractKey(opt), values == null ? "" : String.join(", ", values));
214 }
215 }
216 throw new ConfigurationException("Unable to create configuration", e);
217 }
218
219 }
220
221
222
223
224
225
226 public static Options buildOptions() {
227 BASE_OPTION_COLLECTION.resetSelected();
228 return BASE_OPTION_COLLECTION.getOptions();
229 }
230
231
232
233
234
235
236
237
238
239 public static Reportable getReportable(final File base, final ReportConfiguration config) {
240 File absBase = base.getAbsoluteFile();
241 DocumentName documentName = DocumentName.builder(absBase).build();
242 if (!absBase.exists()) {
243 DefaultLog.getInstance().error("Directory '" + documentName + "' does not exist.");
244 return null;
245 }
246 DocumentNameMatcher documentExcluder = config.getDocumentExcluder(documentName);
247
248 Document doc = new FileDocument(documentName, absBase, documentExcluder);
249 if (!documentExcluder.matches(doc.getName())) {
250 DefaultLog.getInstance().error("Directory '" + documentName + "' is in excluded list.");
251 return null;
252 }
253
254 if (absBase.isDirectory()) {
255 return new DirectoryWalker(doc);
256 }
257
258 return new ArchiveWalker(doc);
259 }
260
261
262
263
264 private static final class OptionComparator implements Comparator<Option>, Serializable {
265
266 @Serial
267 private static final long serialVersionUID = 5305467873966684014L;
268
269 private String getKey(final Option opt) {
270 String key = opt.getOpt();
271 key = key == null ? opt.getLongOpt() : key;
272 return key;
273 }
274
275
276
277
278
279
280
281
282
283
284
285 @Override
286 public int compare(final Option opt1, final Option opt2) {
287 return getKey(opt1).compareToIgnoreCase(getKey(opt2));
288 }
289 }
290
291 public enum ArgumentType {
292
293
294
295 FILE("File", () -> "A file name."),
296
297
298
299 INTEGER("Integer", () -> "An integer value."),
300
301
302
303 DIRORARCHIVE("DirOrArchive", () -> "A directory or archive file to scan."),
304
305
306
307 EXPRESSION("Expression", () -> "A file matching pattern usually of the form used in Ant build files and " +
308 "'.gitignore' files (see https://ant.apache.org/manual/dirtasks.html#patterns for examples). " +
309 "Regular expression patterns may be specified by surrounding the pattern with '%regex[' and ']'. " +
310 "For example '%regex[[A-Z].*]' would match files and directories that start with uppercase Latin letters."),
311
312
313
314 LICENSEFILTER("LicenseFilter", () -> format("A defined filter for the licenses to include. Valid values: %s.",
315 asString(LicenseSetFactory.LicenseFilter.values()))),
316
317
318
319 LOGLEVEL("LogLevel", () -> format("The log level to use. Valid values %s.", asString(Level.values()))),
320
321
322
323 PROCESSINGTYPE("ProcessingType", () -> format("Specifies how to process file types. Valid values are: %s%n",
324 Arrays.stream(ReportConfiguration.Processing.values())
325 .map(v -> format("\t%s: %s", v.name(), v.desc()))
326 .collect(Collectors.joining(System.lineSeparator())))),
327
328
329
330 STYLESHEET("StyleSheet", () -> format("Either an external xsl file or one of the internal named sheets. Internal sheets are: %n%s",
331 Arrays.stream(StyleSheets.values())
332 .map(v -> format("\t%s: %s%n", v.arg(), v.desc()))
333 .collect(Collectors.joining(System.lineSeparator())))),
334
335
336
337 LICENSEID("LicenseID", () -> "The ID for a license."),
338
339
340
341 FAMILYID("FamilyID", () -> "The ID for a license family."),
342
343
344
345 STANDARDCOLLECTION("StandardCollection", () -> format("Defines standard expression patterns (see above). Valid values are: %n%s%n",
346 Arrays.stream(StandardCollection.values())
347 .map(v -> format("\t%s: %s%n", v.name(), v.desc()))
348 .collect(Collectors.joining(System.lineSeparator())))),
349
350
351
352 COUNTERPATTERN("CounterPattern", () -> format("A pattern comprising one of the following prefixes followed by " +
353 "a colon and a count (e.g. %s:5). Prefixes are %n%s.", ClaimStatistic.Counter.UNAPPROVED,
354 Arrays.stream(ClaimStatistic.Counter.values())
355 .map(v -> format("\t%s: %s Default range [%s, %s]%n", v.name(), v.getDescription(),
356 v.getDefaultMinValue(),
357 v.getDefaultMaxValue() == -1 ? "unlimited" : v.getDefaultMaxValue()))
358 .collect(Collectors.joining(System.lineSeparator())))),
359
360
361
362 ARG("Arg", () -> "A string"),
363
364
365
366 NONE("", () -> "");
367
368
369
370
371 private final String displayName;
372
373
374
375 private final Supplier<String> description;
376
377 ArgumentType(final String name,
378 final Supplier<String> description) {
379 this.displayName = name;
380 this.description = description;
381 }
382
383
384
385
386
387 public String getDisplayName() {
388 return displayName;
389 }
390
391
392
393
394
395 public Supplier<String> description() {
396 return description;
397 }
398
399
400
401
402
403
404 public static Optional<ArgumentType> forDisplayName(final String displayName) {
405 return Arrays.stream(ArgumentType.values()).filter(argType -> argType.displayName.equals(displayName))
406 .findAny();
407 }
408 }
409 }