1   
2   
3   
4   
5   
6   
7   
8   
9   
10  
11  
12  
13  
14  
15  
16  
17  
18  
19  
20  package com.puppycrawl.tools.checkstyle;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStream;
26  import java.nio.file.Files;
27  import java.nio.file.Path;
28  import java.util.ArrayList;
29  import java.util.LinkedList;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.Objects;
33  import java.util.Properties;
34  import java.util.logging.ConsoleHandler;
35  import java.util.logging.Filter;
36  import java.util.logging.Level;
37  import java.util.logging.LogRecord;
38  import java.util.logging.Logger;
39  import java.util.regex.Pattern;
40  import java.util.stream.Collectors;
41  
42  import org.apache.commons.logging.Log;
43  import org.apache.commons.logging.LogFactory;
44  
45  import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean.OutputStreamOptions;
46  import com.puppycrawl.tools.checkstyle.api.AuditEvent;
47  import com.puppycrawl.tools.checkstyle.api.AuditListener;
48  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
49  import com.puppycrawl.tools.checkstyle.api.Configuration;
50  import com.puppycrawl.tools.checkstyle.api.RootModule;
51  import com.puppycrawl.tools.checkstyle.utils.ChainedPropertyUtil;
52  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
53  import com.puppycrawl.tools.checkstyle.utils.XpathUtil;
54  import picocli.CommandLine;
55  import picocli.CommandLine.Command;
56  import picocli.CommandLine.Option;
57  import picocli.CommandLine.ParameterException;
58  import picocli.CommandLine.Parameters;
59  import picocli.CommandLine.ParseResult;
60  
61  
62  
63  
64  public final class Main {
65  
66      
67  
68  
69  
70      public static final String ERROR_COUNTER = "Main.errorCounter";
71      
72  
73  
74  
75      public static final String LOAD_PROPERTIES_EXCEPTION = "Main.loadProperties";
76      
77  
78  
79  
80      public static final String CREATE_LISTENER_EXCEPTION = "Main.createListener";
81  
82      
83      private static final Log LOG = LogFactory.getLog(Main.class);
84  
85      
86      private static final int EXIT_WITH_INVALID_USER_INPUT_CODE = -1;
87  
88      
89      private static final int EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE = -2;
90  
91      
92  
93  
94  
95      private Main() {
96      }
97  
98      
99  
100 
101 
102 
103 
104 
105 
106 
107 
108 
109 
110 
111     public static void main(String... args) throws IOException {
112 
113         final CliOptions cliOptions = new CliOptions();
114         final CommandLine commandLine = new CommandLine(cliOptions);
115         commandLine.setUsageHelpWidth(CliOptions.HELP_WIDTH);
116         commandLine.setCaseInsensitiveEnumValuesAllowed(true);
117 
118         
119         int exitStatus = 0;
120         int errorCounter = 0;
121         try {
122             final ParseResult parseResult = commandLine.parseArgs(args);
123             if (parseResult.isVersionHelpRequested()) {
124                 printVersionToSystemOutput();
125             }
126             else if (parseResult.isUsageHelpRequested()) {
127                 commandLine.usage(System.out);
128             }
129             else {
130                 exitStatus = execute(parseResult, cliOptions);
131                 errorCounter = exitStatus;
132             }
133         }
134         catch (ParameterException exc) {
135             exitStatus = EXIT_WITH_INVALID_USER_INPUT_CODE;
136             System.err.println(exc.getMessage());
137             System.err.println("Usage: checkstyle [OPTIONS]... file(s) or folder(s) ...");
138             System.err.println("Try 'checkstyle --help' for more information.");
139         }
140         catch (CheckstyleException exc) {
141             exitStatus = EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE;
142             errorCounter = 1;
143             exc.printStackTrace();
144         }
145         finally {
146             
147             if (errorCounter > 0) {
148                 final LocalizedMessage errorCounterViolation = new LocalizedMessage(
149                         Definitions.CHECKSTYLE_BUNDLE, Main.class,
150                         ERROR_COUNTER, String.valueOf(errorCounter));
151                 
152                 
153                 System.err.println(errorCounterViolation.getMessage());
154             }
155         }
156         Runtime.getRuntime().exit(exitStatus);
157     }
158 
159     
160 
161 
162 
163 
164 
165 
166     private static void printVersionToSystemOutput() {
167         System.out.println("Checkstyle version: " + getVersionString());
168     }
169 
170     
171 
172 
173 
174 
175     private static String getVersionString() {
176         return Main.class.getPackage().getImplementationVersion();
177     }
178 
179     
180 
181 
182 
183 
184 
185 
186 
187 
188 
189 
190 
191 
192     private static int execute(ParseResult parseResult, CliOptions options)
193             throws IOException, CheckstyleException {
194 
195         final int exitStatus;
196 
197         
198         final List<File> filesToProcess = getFilesToProcess(options);
199         final List<String> messages = options.validateCli(parseResult, filesToProcess);
200         final boolean hasMessages = !messages.isEmpty();
201         if (hasMessages) {
202             messages.forEach(System.out::println);
203             exitStatus = EXIT_WITH_INVALID_USER_INPUT_CODE;
204         }
205         else {
206             exitStatus = runCli(options, filesToProcess);
207         }
208         return exitStatus;
209     }
210 
211     
212 
213 
214 
215 
216 
217     private static List<File> getFilesToProcess(CliOptions options) {
218         final List<Pattern> patternsToExclude = options.getExclusions();
219 
220         final List<File> result = new LinkedList<>();
221         for (File file : options.files) {
222             result.addAll(listFiles(file, patternsToExclude));
223         }
224         return result;
225     }
226 
227     
228 
229 
230 
231 
232 
233 
234 
235 
236 
237     private static List<File> listFiles(File node, List<Pattern> patternsToExclude) {
238         
239         
240         final List<File> result = new LinkedList<>();
241 
242         if (node.canRead() && !isPathExcluded(node.getAbsolutePath(), patternsToExclude)) {
243             if (node.isDirectory()) {
244                 final File[] files = node.listFiles();
245                 
246                 if (files != null) {
247                     for (File element : files) {
248                         result.addAll(listFiles(element, patternsToExclude));
249                     }
250                 }
251             }
252             else if (node.isFile()) {
253                 result.add(node);
254             }
255         }
256         return result;
257     }
258 
259     
260 
261 
262 
263 
264 
265 
266 
267 
268     private static boolean isPathExcluded(String path, Iterable<Pattern> patternsToExclude) {
269         boolean result = false;
270 
271         for (Pattern pattern : patternsToExclude) {
272             if (pattern.matcher(path).find()) {
273                 result = true;
274                 break;
275             }
276         }
277 
278         return result;
279     }
280 
281     
282 
283 
284 
285 
286 
287 
288 
289 
290 
291 
292 
293     private static int runCli(CliOptions options, List<File> filesToProcess)
294             throws IOException, CheckstyleException {
295         int result = 0;
296         final boolean hasSuppressionLineColumnNumber = options.suppressionLineColumnNumber != null;
297 
298         
299         if (options.printAst) {
300             
301             final File file = filesToProcess.get(0);
302             final String stringAst = AstTreeStringPrinter.printFileAst(file,
303                     JavaParser.Options.WITHOUT_COMMENTS);
304             System.out.print(stringAst);
305         }
306         else if (Objects.nonNull(options.xpath)) {
307             final String branch = XpathUtil.printXpathBranch(options.xpath, filesToProcess.get(0));
308             System.out.print(branch);
309         }
310         else if (options.printAstWithComments) {
311             final File file = filesToProcess.get(0);
312             final String stringAst = AstTreeStringPrinter.printFileAst(file,
313                     JavaParser.Options.WITH_COMMENTS);
314             System.out.print(stringAst);
315         }
316         else if (options.printJavadocTree) {
317             final File file = filesToProcess.get(0);
318             final String stringAst = DetailNodeTreeStringPrinter.printFileAst(file);
319             System.out.print(stringAst);
320         }
321         else if (options.printTreeWithJavadoc) {
322             final File file = filesToProcess.get(0);
323             final String stringAst = AstTreeStringPrinter.printJavaAndJavadocTree(file);
324             System.out.print(stringAst);
325         }
326         else if (hasSuppressionLineColumnNumber) {
327             final File file = filesToProcess.get(0);
328             final String stringSuppressions =
329                     SuppressionsStringPrinter.printSuppressions(file,
330                             options.suppressionLineColumnNumber, options.tabWidth);
331             System.out.print(stringSuppressions);
332         }
333         else {
334             if (options.debug) {
335                 final Logger parentLogger = Logger.getLogger(Main.class.getName()).getParent();
336                 final ConsoleHandler handler = new ConsoleHandler();
337                 handler.setLevel(Level.FINEST);
338                 handler.setFilter(new OnlyCheckstyleLoggersFilter());
339                 parentLogger.addHandler(handler);
340                 parentLogger.setLevel(Level.FINEST);
341             }
342             if (LOG.isDebugEnabled()) {
343                 LOG.debug("Checkstyle debug logging enabled");
344             }
345 
346             
347             result = runCheckstyle(options, filesToProcess);
348         }
349 
350         return result;
351     }
352 
353     
354 
355 
356 
357 
358 
359 
360 
361 
362 
363 
364     private static int runCheckstyle(CliOptions options, List<File> filesToProcess)
365             throws CheckstyleException, IOException {
366         
367         final Properties props;
368 
369         if (options.propertiesFile == null) {
370             props = System.getProperties();
371         }
372         else {
373             props = loadProperties(options.propertiesFile);
374         }
375 
376         
377 
378         final ConfigurationLoader.IgnoredModulesOptions ignoredModulesOptions;
379         if (options.executeIgnoredModules) {
380             ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.EXECUTE;
381         }
382         else {
383             ignoredModulesOptions = ConfigurationLoader.IgnoredModulesOptions.OMIT;
384         }
385 
386         final ThreadModeSettings multiThreadModeSettings =
387                 new ThreadModeSettings(CliOptions.CHECKER_THREADS_NUMBER,
388                 CliOptions.TREE_WALKER_THREADS_NUMBER);
389         final Configuration config = ConfigurationLoader.loadConfiguration(
390                 options.configurationFile, new PropertiesExpander(props),
391                 ignoredModulesOptions, multiThreadModeSettings);
392 
393         
394         final int errorCounter;
395         final ClassLoader moduleClassLoader = Checker.class.getClassLoader();
396         final RootModule rootModule = getRootModule(config.getName(), moduleClassLoader);
397 
398         try {
399             final AuditListener listener;
400             if (options.generateXpathSuppressionsFile) {
401                 
402                 final Configuration treeWalkerConfig = getTreeWalkerConfig(config);
403                 if (treeWalkerConfig != null) {
404                     final DefaultConfiguration moduleConfig =
405                             new DefaultConfiguration(
406                                     XpathFileGeneratorAstFilter.class.getName());
407                     moduleConfig.addProperty(CliOptions.ATTRIB_TAB_WIDTH_NAME,
408                             String.valueOf(options.tabWidth));
409                     ((DefaultConfiguration) treeWalkerConfig).addChild(moduleConfig);
410                 }
411 
412                 listener = new XpathFileGeneratorAuditListener(getOutputStream(options.outputPath),
413                         getOutputStreamOptions(options.outputPath));
414             }
415             else if (options.generateCheckAndFileSuppressionsFile) {
416                 listener = new ChecksAndFilesSuppressionFileGeneratorAuditListener(
417                         getOutputStream(options.outputPath),
418                         getOutputStreamOptions(options.outputPath));
419             }
420             else {
421                 listener = createListener(options.format, options.outputPath);
422             }
423 
424             rootModule.setModuleClassLoader(moduleClassLoader);
425             rootModule.configure(config);
426             rootModule.addListener(listener);
427 
428             
429             errorCounter = rootModule.process(filesToProcess);
430         }
431         finally {
432             rootModule.destroy();
433         }
434 
435         return errorCounter;
436     }
437 
438     
439 
440 
441 
442 
443 
444 
445 
446 
447     private static Properties loadProperties(File file)
448             throws CheckstyleException {
449         final Properties properties = new Properties();
450 
451         try (InputStream stream = Files.newInputStream(file.toPath())) {
452             properties.load(stream);
453         }
454         catch (final IOException exc) {
455             final LocalizedMessage loadPropertiesExceptionMessage = new LocalizedMessage(
456                     Definitions.CHECKSTYLE_BUNDLE, Main.class,
457                     LOAD_PROPERTIES_EXCEPTION, file.getAbsolutePath());
458             throw new CheckstyleException(loadPropertiesExceptionMessage.getMessage(), exc);
459         }
460 
461         return ChainedPropertyUtil.getResolvedProperties(properties);
462     }
463 
464     
465 
466 
467 
468 
469 
470 
471 
472 
473 
474     private static RootModule getRootModule(String name, ClassLoader moduleClassLoader)
475             throws CheckstyleException {
476         final ModuleFactory factory = new PackageObjectFactory(
477                 Checker.class.getPackage().getName(), moduleClassLoader);
478 
479         return (RootModule) factory.createModule(name);
480     }
481 
482     
483 
484 
485 
486 
487 
488     private static Configuration getTreeWalkerConfig(Configuration config) {
489         Configuration result = null;
490 
491         final Configuration[] children = config.getChildren();
492         for (Configuration child : children) {
493             if ("TreeWalker".equals(child.getName())) {
494                 result = child;
495                 break;
496             }
497         }
498         return result;
499     }
500 
501     
502 
503 
504 
505 
506 
507 
508 
509 
510 
511     private static AuditListener createListener(OutputFormat format, Path outputLocation)
512             throws IOException {
513         final OutputStream out = getOutputStream(outputLocation);
514         final OutputStreamOptions closeOutputStreamOption =
515                 getOutputStreamOptions(outputLocation);
516         return format.createListener(out, closeOutputStreamOption);
517     }
518 
519     
520 
521 
522 
523 
524 
525 
526 
527 
528 
529     @SuppressWarnings("resource")
530     private static OutputStream getOutputStream(Path outputPath) throws IOException {
531         final OutputStream result;
532         if (outputPath == null) {
533             result = System.out;
534         }
535         else {
536             result = Files.newOutputStream(outputPath);
537         }
538         return result;
539     }
540 
541     
542 
543 
544 
545 
546 
547     private static OutputStreamOptions getOutputStreamOptions(Path outputPath) {
548         final OutputStreamOptions result;
549         if (outputPath == null) {
550             result = OutputStreamOptions.NONE;
551         }
552         else {
553             result = OutputStreamOptions.CLOSE;
554         }
555         return result;
556     }
557 
558     
559 
560 
561 
562 
563 
564     enum OutputFormat {
565         
566         XML,
567         
568         SARIF,
569         
570         PLAIN;
571 
572         
573 
574 
575 
576 
577 
578 
579 
580         public AuditListener createListener(
581             OutputStream out,
582             OutputStreamOptions options) throws IOException {
583             final AuditListener result;
584             if (this == XML) {
585                 result = new XMLLogger(out, options);
586             }
587             else if (this == SARIF) {
588                 result = new SarifLogger(out, options);
589             }
590             else {
591                 result = new DefaultLogger(out, options);
592             }
593             return result;
594         }
595 
596         
597 
598 
599 
600 
601         @Override
602         public String toString() {
603             return name().toLowerCase(Locale.ROOT);
604         }
605     }
606 
607     
608     private static final class OnlyCheckstyleLoggersFilter implements Filter {
609         
610         private final String packageName = Main.class.getPackage().getName();
611 
612         
613 
614 
615 
616 
617 
618         @Override
619         public boolean isLoggable(LogRecord logRecord) {
620             return logRecord.getLoggerName().startsWith(packageName);
621         }
622     }
623 
624     
625 
626 
627 
628 
629 
630 
631 
632 
633 
634 
635 
636 
637 
638     @Command(name = "checkstyle", description = "Checkstyle verifies that the specified "
639             + "source code files adhere to the specified rules. By default, violations are "
640             + "reported to standard out in plain format. Checkstyle requires a configuration "
641             + "XML file that configures the checks to apply.",
642             mixinStandardHelpOptions = true)
643     private static final class CliOptions {
644 
645         
646         private static final int HELP_WIDTH = 100;
647 
648         
649         private static final int DEFAULT_THREAD_COUNT = 1;
650 
651         
652         private static final String ATTRIB_TAB_WIDTH_NAME = "tabWidth";
653 
654         
655         private static final OutputFormat DEFAULT_OUTPUT_FORMAT = OutputFormat.PLAIN;
656 
657         
658         private static final String OUTPUT_FORMAT_OPTION = "-f";
659 
660         
661 
662 
663 
664 
665         private static final int CHECKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
666 
667         
668 
669 
670 
671         private static final int TREE_WALKER_THREADS_NUMBER = DEFAULT_THREAD_COUNT;
672 
673         
674         @Parameters(arity = "1..*", paramLabel = "<files or folders>",
675                 description = "One or more source files to verify")
676         private List<File> files;
677 
678         
679         @Option(names = "-c", description = "Specifies the location of the file that defines"
680                 + " the configuration modules. The location can either be a filesystem location"
681                 + ", or a name passed to the ClassLoader.getResource() method.")
682         private String configurationFile;
683 
684         
685         @Option(names = "-o", description = "Sets the output file. Defaults to stdout.")
686         private Path outputPath;
687 
688         
689         @Option(names = "-p", description = "Sets the property files to load.")
690         private File propertiesFile;
691 
692         
693         @Option(names = "-s",
694                 description = "Prints xpath suppressions at the file's line and column position. "
695                         + "Argument is the line and column number (separated by a : ) in the file "
696                         + "that the suppression should be generated for. The option cannot be used "
697                         + "with other options and requires exactly one file to run on to be "
698                         + "specified. Note that the generated result will have few queries, joined "
699                         + "by pipe(|). Together they will match all AST nodes on "
700                         + "specified line and column. You need to choose only one and recheck "
701                         + "that it works. Usage of all of them is also ok, but might result in "
702                         + "undesirable matching and suppress other issues.")
703         private String suppressionLineColumnNumber;
704 
705         
706 
707 
708 
709 
710 
711 
712         @Option(names = {"-w", "--tabWidth"},
713                 description = "Sets the length of the tab character. "
714                 + "Used only with -s option. Default value is ${DEFAULT-VALUE}.")
715         private int tabWidth = CommonUtil.DEFAULT_TAB_WIDTH;
716 
717         
718         @Option(names = {"-g", "--generate-xpath-suppression"},
719                 description = "Generates to output a xpath suppression xml to use to suppress all "
720                         + "violations from user's config. Instead of printing every violation, "
721                         + "all violations will be catched and single suppressions xml file will "
722                         + "be printed out. Used only with -c option. Output "
723                         + "location can be specified with -o option.")
724         private boolean generateXpathSuppressionsFile;
725 
726         
727         @Option(names = {"-G", "--generate-checks-and-files-suppression"},
728                 description = "Generates to output a suppression xml that will have suppress "
729                         + "elements with \"checks\" and \"files\" attributes only to use to "
730                         + "suppress all violations from user's config. Instead of printing every "
731                         + "violation, all violations will be catched and single suppressions xml "
732                         + "file will be printed out. Used only with -c option. Output "
733                         + "location can be specified with -o option.")
734         private boolean generateCheckAndFileSuppressionsFile;
735 
736         
737 
738 
739 
740 
741 
742 
743         @Option(names = "-f",
744                 description = "Specifies the output format. Valid values: "
745                 + "${COMPLETION-CANDIDATES} for XMLLogger, SarifLogger, "
746                 + "and DefaultLogger respectively. Defaults to ${DEFAULT-VALUE}.")
747         private OutputFormat format = DEFAULT_OUTPUT_FORMAT;
748 
749         
750         @Option(names = {"-t", "--tree"},
751                 description = "This option is used to display the Abstract Syntax Tree (AST) "
752                         + "without any comments of the specified file. It can only be used on "
753                         + "a single file and cannot be combined with other options.")
754         private boolean printAst;
755 
756         
757         @Option(names = {"-T", "--treeWithComments"},
758                 description = "This option is used to display the Abstract Syntax Tree (AST) "
759                         + "with comment nodes excluding Javadoc of the specified file. It can only"
760                         + " be used on a single file and cannot be combined with other options.")
761         private boolean printAstWithComments;
762 
763         
764         @Option(names = {"-j", "--javadocTree"},
765                 description = "This option is used to print the Parse Tree of the Javadoc comment."
766                         + " The file has to contain only Javadoc comment content "
767                         + "excluding '/**' and '*/' at the beginning and at the end respectively. "
768                         + "It can only be used on a single file and cannot be combined "
769                         + "with other options.")
770         private boolean printJavadocTree;
771 
772         
773         @Option(names = {"-J", "--treeWithJavadoc"},
774                 description = "This option is used to display the Abstract Syntax Tree (AST) "
775                         + "with Javadoc nodes of the specified file. It can only be used on a "
776                         + "single file and cannot be combined with other options.")
777         private boolean printTreeWithJavadoc;
778 
779         
780         @Option(names = {"-d", "--debug"},
781                 description = "Prints all debug logging of CheckStyle utility.")
782         private boolean debug;
783 
784         
785 
786 
787 
788 
789 
790 
791         @Option(names = {"-e", "--exclude"},
792                 description = "Directory/file to exclude from CheckStyle. The path can be the "
793                         + "full, absolute path, or relative to the current path. Multiple "
794                         + "excludes are allowed.")
795         private List<File> exclude = new ArrayList<>();
796 
797         
798 
799 
800 
801 
802 
803 
804         @Option(names = {"-x", "--exclude-regexp"},
805                 description = "Directory/file pattern to exclude from CheckStyle. Multiple "
806                         + "excludes are allowed.")
807         private List<Pattern> excludeRegex = new ArrayList<>();
808 
809         
810         @Option(names = {"-E", "--executeIgnoredModules"},
811                 description = "Allows ignored modules to be run.")
812         private boolean executeIgnoredModules;
813 
814         
815         @Option(names = {"-b", "--branch-matching-xpath"},
816             description = "Shows Abstract Syntax Tree(AST) branches that match given XPath query.")
817         private String xpath;
818 
819         
820 
821 
822 
823 
824         private List<Pattern> getExclusions() {
825             final List<Pattern> result = exclude.stream()
826                     .map(File::getAbsolutePath)
827                     .map(Pattern::quote)
828                     .map(pattern -> Pattern.compile("^" + pattern + "$"))
829                     .collect(Collectors.toCollection(ArrayList::new));
830             result.addAll(excludeRegex);
831             return result;
832         }
833 
834         
835 
836 
837 
838 
839 
840 
841         
842         private List<String> validateCli(ParseResult parseResult, List<File> filesToProcess) {
843             final List<String> result = new ArrayList<>();
844             final boolean hasConfigurationFile = configurationFile != null;
845             final boolean hasSuppressionLineColumnNumber = suppressionLineColumnNumber != null;
846 
847             if (filesToProcess.isEmpty()) {
848                 result.add("Files to process must be specified, found 0.");
849             }
850             
851             else if (printAst || printAstWithComments || printJavadocTree || printTreeWithJavadoc
852                 || xpath != null) {
853                 if (suppressionLineColumnNumber != null || configurationFile != null
854                         || propertiesFile != null || outputPath != null
855                         || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
856                     result.add("Option '-t' cannot be used with other options.");
857                 }
858                 else if (filesToProcess.size() > 1) {
859                     result.add("Printing AST is allowed for only one file.");
860                 }
861             }
862             else if (hasSuppressionLineColumnNumber) {
863                 if (configurationFile != null || propertiesFile != null
864                         || outputPath != null
865                         || parseResult.hasMatchedOption(OUTPUT_FORMAT_OPTION)) {
866                     result.add("Option '-s' cannot be used with other options.");
867                 }
868                 else if (filesToProcess.size() > 1) {
869                     result.add("Printing xpath suppressions is allowed for only one file.");
870                 }
871             }
872             else if (hasConfigurationFile) {
873                 try {
874                     
875                     CommonUtil.getUriByFilename(configurationFile);
876                 }
877                 catch (CheckstyleException ignored) {
878                     final String msg = "Could not find config XML file '%s'.";
879                     result.add(String.format(Locale.ROOT, msg, configurationFile));
880                 }
881                 result.addAll(validateOptionalCliParametersIfConfigDefined());
882             }
883             else {
884                 result.add("Must specify a config XML file.");
885             }
886 
887             return result;
888         }
889 
890         
891 
892 
893 
894 
895         private List<String> validateOptionalCliParametersIfConfigDefined() {
896             final List<String> result = new ArrayList<>();
897             if (propertiesFile != null && !propertiesFile.exists()) {
898                 result.add(String.format(Locale.ROOT,
899                         "Could not find file '%s'.", propertiesFile));
900             }
901             return result;
902         }
903     }
904 
905 }