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.FilenameFilter;
23 import java.io.IOException;
24 import java.io.PrintStream;
25 import java.io.Serializable;
26 import java.nio.charset.StandardCharsets;
27 import java.nio.file.Files;
28 import java.nio.file.Paths;
29 import java.util.Arrays;
30 import java.util.Comparator;
31 import java.util.List;
32 import java.util.regex.PatternSyntaxException;
33 import java.util.stream.Collectors;
34
35 import org.apache.commons.cli.CommandLine;
36 import org.apache.commons.cli.DefaultParser;
37 import org.apache.commons.cli.HelpFormatter;
38 import org.apache.commons.cli.Option;
39 import org.apache.commons.cli.OptionGroup;
40 import org.apache.commons.cli.Options;
41 import org.apache.commons.cli.ParseException;
42 import org.apache.commons.io.FileUtils;
43 import org.apache.commons.io.filefilter.NameFileFilter;
44 import org.apache.commons.io.filefilter.NotFileFilter;
45 import org.apache.commons.io.filefilter.OrFileFilter;
46 import org.apache.commons.io.filefilter.RegexFileFilter;
47 import org.apache.commons.io.filefilter.WildcardFileFilter;
48 import org.apache.commons.lang3.StringUtils;
49 import org.apache.rat.config.AddLicenseHeaders;
50 import org.apache.rat.license.LicenseSetFactory.LicenseFilter;
51 import org.apache.rat.report.IReportable;
52 import org.apache.rat.utils.DefaultLog;
53 import org.apache.rat.utils.Log;
54 import org.apache.rat.utils.Log.Level;
55 import org.apache.rat.walker.ArchiveWalker;
56 import org.apache.rat.walker.DirectoryWalker;
57
58
59
60
61 public class Report {
62
63
64
65 private static final String ADD = "A";
66 private static final String ADD_OLD = "a";
67
68
69
70 private static final String FORCE = "f";
71
72
73
74 private static final String COPYRIGHT = "c";
75
76
77
78 private static final String EXCLUDE_CLI = "e";
79
80
81
82
83 private static final String EXCLUDE_FILE_CLI = "E";
84
85
86
87 private static final String STYLESHEET_CLI = "s";
88
89
90
91 private static final String HELP = "h";
92
93
94
95 private static final String LICENSES = "licenses";
96
97
98
99 private static final String NO_DEFAULTS = "no-default-licenses";
100
101
102
103 private static final String SCAN_HIDDEN_DIRECTORIES = "scan-hidden-directories";
104
105
106
107 private static final String LIST_LICENSES = "list-licenses";
108
109
110
111
112 private static final String LIST_FAMILIES = "list-families";
113
114 private static final String LOG_LEVEL = "log-level";
115
116
117
118
119 private static final String XML = "x";
120
121
122
123
124
125
126
127
128
129 public static void main(String[] args) throws Exception {
130 Options opts = buildOptions();
131 CommandLine cl;
132 try {
133 cl = new DefaultParser().parse(opts, args);
134 } catch (ParseException e) {
135 DefaultLog.INSTANCE.error(e.getMessage());
136 DefaultLog.INSTANCE.error("Please use the \"--help\" option to see a list of valid commands and options");
137 System.exit(1);
138 return;
139
140 }
141
142 if (cl.hasOption(LOG_LEVEL)) {
143 try {
144 Log.Level level = Log.Level.valueOf(cl.getOptionValue(LOG_LEVEL).toUpperCase());
145 DefaultLog.INSTANCE.setLevel(level);
146 } catch (IllegalArgumentException e) {
147 DefaultLog.INSTANCE.warn(String.format("Invalid Log Level (%s) specified.", cl.getOptionValue(LOG_LEVEL)));
148 DefaultLog.INSTANCE.warn(String.format("Log level set at: %s", DefaultLog.INSTANCE.getLevel()));
149 }
150 }
151 if (cl.hasOption(HELP)) {
152 printUsage(opts);
153 }
154
155 args = cl.getArgs();
156 if (args == null || args.length != 1) {
157 printUsage(opts);
158 } else {
159 ReportConfiguration configuration = createConfiguration(args[0], cl);
160 configuration.validate(DefaultLog.INSTANCE::error);
161
162 boolean dryRun = false;
163
164 if (cl.hasOption(LIST_FAMILIES)) {
165 LicenseFilter f = LicenseFilter.fromText(cl.getOptionValue(LIST_FAMILIES));
166 if (f != LicenseFilter.none) {
167 dryRun = true;
168 Reporter.listLicenseFamilies(configuration, f);
169 }
170 }
171 if (cl.hasOption(LIST_LICENSES)) {
172 LicenseFilter f = LicenseFilter.fromText(cl.getOptionValue(LIST_LICENSES));
173 if (f != LicenseFilter.none) {
174 dryRun = true;
175 Reporter.listLicenses(configuration, f);
176 }
177 }
178
179 if (!dryRun) {
180 Reporter.report(configuration);
181 }
182 }
183 }
184
185 static ReportConfiguration createConfiguration(String baseDirectory, CommandLine cl) throws IOException {
186 final ReportConfiguration configuration = new ReportConfiguration(DefaultLog.INSTANCE);
187
188 if (cl.hasOption('o')) {
189 configuration.setOut(new File(cl.getOptionValue('o')));
190 }
191
192 if (cl.hasOption(SCAN_HIDDEN_DIRECTORIES)) {
193 configuration.setDirectoryFilter(null);
194 }
195
196 if (cl.hasOption('a') || cl.hasOption('A')) {
197 configuration.setAddLicenseHeaders(cl.hasOption('f') ? AddLicenseHeaders.FORCED : AddLicenseHeaders.TRUE);
198 configuration.setCopyrightMessage(cl.getOptionValue("c"));
199 }
200
201 if (cl.hasOption(EXCLUDE_CLI)) {
202 String[] excludes = cl.getOptionValues(EXCLUDE_CLI);
203 if (excludes != null) {
204 final FilenameFilter filter = parseExclusions(Arrays.asList(excludes));
205 configuration.setInputFileFilter(filter);
206 }
207 } else if (cl.hasOption(EXCLUDE_FILE_CLI)) {
208 String excludeFileName = cl.getOptionValue(EXCLUDE_FILE_CLI);
209 if (excludeFileName != null) {
210 final FilenameFilter filter = parseExclusions(
211 FileUtils.readLines(new File(excludeFileName), StandardCharsets.UTF_8));
212 configuration.setInputFileFilter(filter);
213 }
214 }
215
216 if (cl.hasOption(XML)) {
217 configuration.setStyleReport(false);
218 } else {
219 configuration.setStyleReport(true);
220 if (cl.hasOption(STYLESHEET_CLI)) {
221 String[] style = cl.getOptionValues(STYLESHEET_CLI);
222 if (style.length != 1) {
223 DefaultLog.INSTANCE.error("Please specify a single stylesheet");
224 System.exit(1);
225 }
226 configuration.setStyleSheet(() -> Files.newInputStream(Paths.get(style[0])));
227 }
228 }
229
230 Defaults.Builder defaultBuilder = Defaults.builder();
231 if (cl.hasOption(NO_DEFAULTS)) {
232 defaultBuilder.noDefault();
233 }
234 if (cl.hasOption(LICENSES)) {
235 for (String fn : cl.getOptionValues(LICENSES)) {
236 defaultBuilder.add(fn);
237 }
238 }
239 Defaults defaults = defaultBuilder.build();
240 configuration.setFrom(defaults);
241 configuration.setReportable(getDirectory(baseDirectory, configuration));
242 return configuration;
243 }
244
245
246
247
248
249
250
251 static FilenameFilter parseExclusions(List<String> excludes) {
252 final OrFileFilter orFilter = new OrFileFilter();
253 int ignoredLines = 0;
254 for (String exclude : excludes) {
255 try {
256
257 if (exclude.startsWith("#") || StringUtils.isEmpty(exclude)) {
258 ignoredLines++;
259 continue;
260 }
261
262 String exclusion = exclude.trim();
263
264
265 orFilter.addFileFilter(new RegexFileFilter(exclusion));
266 orFilter.addFileFilter(new NameFileFilter(exclusion));
267 orFilter.addFileFilter(WildcardFileFilter.builder().setWildcards(exclusion).get());
268 } catch (PatternSyntaxException e) {
269 DefaultLog.INSTANCE.error("Will skip given exclusion '" + exclude + "' due to " + e);
270 }
271 }
272 DefaultLog.INSTANCE.error("Ignored " + ignoredLines + " lines in your exclusion files as comments or empty lines.");
273 return new NotFileFilter(orFilter);
274 }
275
276 static Options buildOptions() {
277 String licFilterValues = String.join(", ",
278 Arrays.stream(LicenseFilter.values()).map(LicenseFilter::name).collect(Collectors.toList()));
279
280 Options opts = new Options()
281
282 .addOption(
283 Option.builder().hasArg(true).longOpt(LIST_FAMILIES)
284 .desc("List the defined license families (default is none). Valid options are: "+licFilterValues+".")
285 .build())
286 .addOption(
287 Option.builder().hasArg(true).longOpt(LIST_LICENSES)
288 .desc("List the defined licenses (default is none). Valid options are: "+licFilterValues+".")
289 .build())
290
291 .addOption(new Option(HELP, "help", false, "Print help for the RAT command line interface and exit."));
292
293
294 Option out = new Option("o", "out", true,
295 "Define the output file where to write a report to (default is System.out).");
296 opts.addOption(out);
297
298 String defaultHandlingText = " By default all approved default licenses are used";
299 Option noDefaults = new Option(null, NO_DEFAULTS, false, "Ignore default configuration." + defaultHandlingText);
300 opts.addOption(noDefaults);
301
302 opts.addOption(null, LICENSES, true, "File names or URLs for license definitions");
303 opts.addOption(null, SCAN_HIDDEN_DIRECTORIES, false, "Scan hidden directories");
304
305 OptionGroup addLicenseGroup = new OptionGroup();
306 String addLicenseDesc = "Add the default license header to any file with an unknown license that is not in the exclusion list. "
307 + "By default new files will be created with the license header, "
308 + "to force the modification of existing files use the --force option.";
309
310
311
312 Option addLicence = new Option(ADD_OLD, "addLicence", false, addLicenseDesc);
313 addLicenseGroup.addOption(addLicence);
314 Option addLicense = new Option(ADD, "addLicense", false, addLicenseDesc);
315 addLicenseGroup.addOption(addLicense);
316 opts.addOptionGroup(addLicenseGroup);
317
318 Option write = new Option(FORCE, "force", false,
319 "Forces any changes in files to be written directly to the source files (i.e. new files are not created).");
320 opts.addOption(write);
321
322 Option copyright = new Option(COPYRIGHT, "copyright", true,
323 "The copyright message to use in the license headers, usually in the form of \"Copyright 2008 Foo\"");
324 opts.addOption(copyright);
325
326 final Option exclude = Option.builder(EXCLUDE_CLI).argName("expression").longOpt("exclude").hasArgs()
327 .desc("Excludes files matching wildcard <expression>. "
328 + "Note that --dir is required when using this parameter. " + "Allows multiple arguments.")
329 .build();
330 opts.addOption(exclude);
331
332 final Option excludeFile = Option.builder(EXCLUDE_FILE_CLI).argName("fileName").longOpt("exclude-file")
333 .hasArgs().desc("Excludes files matching regular expression in <file> "
334 + "Note that --dir is required when using this parameter. ")
335 .build();
336 opts.addOption(excludeFile);
337
338 Option dir = new Option("d", "dir", false, "Used to indicate source when using --exclude");
339 opts.addOption(dir);
340
341 opts.addOption( Option.builder().argName("level").longOpt(LOG_LEVEL)
342 .hasArgs().desc("sets the log level. Valid options are: DEBUG, INFO, WARN, ERROR, OFF")
343 .build() );
344
345 OptionGroup outputType = new OptionGroup();
346
347 Option xml = new Option(XML, "xml", false, "Output the report in raw XML format. Not compatible with -s");
348 outputType.addOption(xml);
349
350 Option xslt = new Option(STYLESHEET_CLI, "stylesheet", true,
351 "XSLT stylesheet to use when creating the" + " report. Not compatible with -x");
352 outputType.addOption(xslt);
353 opts.addOptionGroup(outputType);
354
355
356
357 return opts;
358 }
359
360 private static void printUsage(Options opts) {
361 HelpFormatter f = new HelpFormatter();
362 f.setOptionComparator(new OptionComparator());
363 String header = "\nAvailable options";
364
365 String footer = "\nNOTE:\n" + "Rat is really little more than a grep ATM\n"
366 + "Rat is also rather memory hungry ATM\n" + "Rat is very basic ATM\n"
367 + "Rat highlights possible issues\n" + "Rat reports require interpretation\n"
368 + "Rat often requires some tuning before it runs well against a project\n"
369 + "Rat relies on heuristics: it may miss issues\n";
370
371 f.printHelp("java -jar apache-rat/target/apache-rat-CURRENT-VERSION.jar [options] [DIR|TARBALL]", header, opts,
372 footer, false);
373 System.exit(0);
374 }
375
376 private Report() {
377
378 }
379
380
381
382
383
384
385
386
387
388 private static IReportable getDirectory(String baseDirectory, ReportConfiguration config) {
389 try (PrintStream out = new PrintStream(config.getOutput().get())) {
390 File base = new File(baseDirectory);
391
392 if (!base.exists()) {
393 config.getLog().log(Level.ERROR, "Directory '"+baseDirectory+"' does not exist");
394 return null;
395 }
396
397 if (base.isDirectory()) {
398 return new DirectoryWalker(base, config.getInputFileFilter(), config.getDirectoryFilter());
399 }
400
401 try {
402 return new ArchiveWalker(base, config.getInputFileFilter());
403 } catch (IOException ex) {
404 config.getLog().log(Level.ERROR, "file '"+baseDirectory+"' is not valid gzip data.");
405 return null;
406 }
407 } catch (IOException e) {
408 throw new ConfigurationException("Error opening output", e);
409 }
410 }
411
412
413
414
415 private static class OptionComparator implements Comparator<Option>, Serializable {
416
417 private static final long serialVersionUID = 5305467873966684014L;
418
419 private String getKey(Option opt) {
420 String key = opt.getOpt();
421 key = key == null ? opt.getLongOpt() : key;
422 return key;
423 }
424
425
426
427
428
429
430
431
432
433
434
435 @Override
436 public int compare(final Option opt1, final Option opt2) {
437 return getKey(opt1).compareToIgnoreCase(getKey(opt2));
438 }
439 }
440 }