001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2017 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
018////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle;
021
022import java.io.File;
023import java.io.FileInputStream;
024import java.io.FileNotFoundException;
025import java.io.FileOutputStream;
026import java.io.IOException;
027import java.io.OutputStream;
028import java.util.ArrayList;
029import java.util.LinkedList;
030import java.util.List;
031import java.util.Properties;
032import java.util.logging.ConsoleHandler;
033import java.util.logging.Filter;
034import java.util.logging.Level;
035import java.util.logging.LogRecord;
036import java.util.logging.Logger;
037import java.util.regex.Pattern;
038
039import org.apache.commons.cli.CommandLine;
040import org.apache.commons.cli.CommandLineParser;
041import org.apache.commons.cli.DefaultParser;
042import org.apache.commons.cli.HelpFormatter;
043import org.apache.commons.cli.Options;
044import org.apache.commons.cli.ParseException;
045import org.apache.commons.logging.Log;
046import org.apache.commons.logging.LogFactory;
047
048import com.google.common.io.Closeables;
049import com.puppycrawl.tools.checkstyle.api.AuditListener;
050import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
051import com.puppycrawl.tools.checkstyle.api.Configuration;
052import com.puppycrawl.tools.checkstyle.api.RootModule;
053import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
054
055/**
056 * Wrapper command line program for the Checker.
057 * @author the original author or authors.
058 *
059 **/
060public final class Main {
061    /** Logger for Main. */
062    private static final Log LOG = LogFactory.getLog(Main.class);
063
064    /** Width of CLI help option. */
065    private static final int HELP_WIDTH = 100;
066
067    /** Exit code returned when execution finishes with {@link CheckstyleException}. */
068    private static final int EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE = -2;
069
070    /** Name for the option 'v'. */
071    private static final String OPTION_V_NAME = "v";
072
073    /** Name for the option 'c'. */
074    private static final String OPTION_C_NAME = "c";
075
076    /** Name for the option 'f'. */
077    private static final String OPTION_F_NAME = "f";
078
079    /** Name for the option 'p'. */
080    private static final String OPTION_P_NAME = "p";
081
082    /** Name for the option 'o'. */
083    private static final String OPTION_O_NAME = "o";
084
085    /** Name for the option 't'. */
086    private static final String OPTION_T_NAME = "t";
087
088    /** Name for the option '--tree'. */
089    private static final String OPTION_TREE_NAME = "tree";
090
091    /** Name for the option '-T'. */
092    private static final String OPTION_CAPITAL_T_NAME = "T";
093
094    /** Name for the option '--treeWithComments'. */
095    private static final String OPTION_TREE_COMMENT_NAME = "treeWithComments";
096
097    /** Name for the option '-j'. */
098    private static final String OPTION_J_NAME = "j";
099
100    /** Name for the option '--javadocTree'. */
101    private static final String OPTION_JAVADOC_TREE_NAME = "javadocTree";
102
103    /** Name for the option '-J'. */
104    private static final String OPTION_CAPITAL_J_NAME = "J";
105
106    /** Name for the option '--treeWithJavadoc'. */
107    private static final String OPTION_TREE_JAVADOC_NAME = "treeWithJavadoc";
108
109    /** Name for the option '-d'. */
110    private static final String OPTION_D_NAME = "d";
111
112    /** Name for the option '--debug'. */
113    private static final String OPTION_DEBUG_NAME = "debug";
114
115    /** Name for the option 'e'. */
116    private static final String OPTION_E_NAME = "e";
117
118    /** Name for the option '--exclude'. */
119    private static final String OPTION_EXCLUDE_NAME = "exclude";
120
121    /** Name for the option '--executeIgnoredModules'. */
122    private static final String OPTION_EXECUTE_IGNORED_MODULES_NAME = "executeIgnoredModules";
123
124    /** Name for the option 'x'. */
125    private static final String OPTION_X_NAME = "x";
126
127    /** Name for the option '--exclude-regexp'. */
128    private static final String OPTION_EXCLUDE_REGEXP_NAME = "exclude-regexp";
129
130    /** Name for 'xml' format. */
131    private static final String XML_FORMAT_NAME = "xml";
132
133    /** Name for 'plain' format. */
134    private static final String PLAIN_FORMAT_NAME = "plain";
135
136    /** Don't create instance of this class, use {@link #main(String[])} method instead. */
137    private Main() {
138    }
139
140    /**
141     * Loops over the files specified checking them for errors. The exit code
142     * is the number of errors found in all the files.
143     * @param args the command line arguments.
144     * @throws IOException if there is a problem with files access
145     * @noinspection CallToPrintStackTrace
146     **/
147    public static void main(String... args) throws IOException {
148        int errorCounter = 0;
149        boolean cliViolations = false;
150        // provide proper exit code based on results.
151        final int exitWithCliViolation = -1;
152        int exitStatus = 0;
153
154        try {
155            //parse CLI arguments
156            final CommandLine commandLine = parseCli(args);
157
158            // show version and exit if it is requested
159            if (commandLine.hasOption(OPTION_V_NAME)) {
160                System.out.println("Checkstyle version: "
161                        + Main.class.getPackage().getImplementationVersion());
162                exitStatus = 0;
163            }
164            else {
165                final List<File> filesToProcess = getFilesToProcess(getExclusions(commandLine),
166                        commandLine.getArgs());
167
168                // return error if something is wrong in arguments
169                final List<String> messages = validateCli(commandLine, filesToProcess);
170                cliViolations = !messages.isEmpty();
171                if (cliViolations) {
172                    exitStatus = exitWithCliViolation;
173                    errorCounter = 1;
174                    messages.forEach(System.out::println);
175                }
176                else {
177                    errorCounter = runCli(commandLine, filesToProcess);
178                    exitStatus = errorCounter;
179                }
180            }
181        }
182        catch (ParseException pex) {
183            // something wrong with arguments - print error and manual
184            cliViolations = true;
185            exitStatus = exitWithCliViolation;
186            errorCounter = 1;
187            System.out.println(pex.getMessage());
188            printUsage();
189        }
190        catch (CheckstyleException ex) {
191            exitStatus = EXIT_WITH_CHECKSTYLE_EXCEPTION_CODE;
192            errorCounter = 1;
193            ex.printStackTrace();
194        }
195        finally {
196            // return exit code base on validation of Checker
197            if (errorCounter != 0 && !cliViolations) {
198                System.out.println(String.format("Checkstyle ends with %d errors.", errorCounter));
199            }
200            if (exitStatus != 0) {
201                System.exit(exitStatus);
202            }
203        }
204    }
205
206    /**
207     * Parses and executes Checkstyle based on passed arguments.
208     * @param args
209     *        command line parameters
210     * @return parsed information about passed parameters
211     * @throws ParseException
212     *         when passed arguments are not valid
213     */
214    private static CommandLine parseCli(String... args)
215            throws ParseException {
216        // parse the parameters
217        final CommandLineParser clp = new DefaultParser();
218        // always returns not null value
219        return clp.parse(buildOptions(), args);
220    }
221
222    /**
223     * Gets the list of exclusions provided through the command line argument.
224     * @param commandLine command line object
225     * @return List of exclusion patterns.
226     */
227    private static List<Pattern> getExclusions(CommandLine commandLine) {
228        final List<Pattern> result = new ArrayList<>();
229
230        if (commandLine.hasOption(OPTION_E_NAME)) {
231            for (String value : commandLine.getOptionValues(OPTION_E_NAME)) {
232                result.add(Pattern.compile("^" + Pattern.quote(new File(value).getAbsolutePath())
233                        + "$"));
234            }
235        }
236        if (commandLine.hasOption(OPTION_X_NAME)) {
237            for (String value : commandLine.getOptionValues(OPTION_X_NAME)) {
238                result.add(Pattern.compile(value));
239            }
240        }
241
242        return result;
243    }
244
245    /**
246     * Do validation of Command line options.
247     * @param cmdLine command line object
248     * @param filesToProcess List of files to process found from the command line.
249     * @return list of violations
250     */
251    // -@cs[CyclomaticComplexity] Breaking apart will damage encapsulation
252    private static List<String> validateCli(CommandLine cmdLine, List<File> filesToProcess) {
253        final List<String> result = new ArrayList<>();
254
255        if (filesToProcess.isEmpty()) {
256            result.add("Files to process must be specified, found 0.");
257        }
258        // ensure there is no conflicting options
259        else if (cmdLine.hasOption(OPTION_T_NAME) || cmdLine.hasOption(OPTION_CAPITAL_T_NAME)
260                || cmdLine.hasOption(OPTION_J_NAME) || cmdLine.hasOption(OPTION_CAPITAL_J_NAME)) {
261            if (cmdLine.hasOption(OPTION_C_NAME) || cmdLine.hasOption(OPTION_P_NAME)
262                    || cmdLine.hasOption(OPTION_F_NAME) || cmdLine.hasOption(OPTION_O_NAME)) {
263                result.add("Option '-t' cannot be used with other options.");
264            }
265            else if (filesToProcess.size() > 1) {
266                result.add("Printing AST is allowed for only one file.");
267            }
268        }
269        // ensure a configuration file is specified
270        else if (cmdLine.hasOption(OPTION_C_NAME)) {
271            final String configLocation = cmdLine.getOptionValue(OPTION_C_NAME);
272            try {
273                // test location only
274                CommonUtils.getUriByFilename(configLocation);
275            }
276            catch (CheckstyleException ignored) {
277                result.add(String.format("Could not find config XML file '%s'.", configLocation));
278            }
279
280            // validate optional parameters
281            if (cmdLine.hasOption(OPTION_F_NAME)) {
282                final String format = cmdLine.getOptionValue(OPTION_F_NAME);
283                if (!PLAIN_FORMAT_NAME.equals(format) && !XML_FORMAT_NAME.equals(format)) {
284                    result.add(String.format("Invalid output format."
285                            + " Found '%s' but expected '%s' or '%s'.",
286                            format, PLAIN_FORMAT_NAME, XML_FORMAT_NAME));
287                }
288            }
289            if (cmdLine.hasOption(OPTION_P_NAME)) {
290                final String propertiesLocation = cmdLine.getOptionValue(OPTION_P_NAME);
291                final File file = new File(propertiesLocation);
292                if (!file.exists()) {
293                    result.add(String.format("Could not find file '%s'.", propertiesLocation));
294                }
295            }
296        }
297        else {
298            result.add("Must specify a config XML file.");
299        }
300
301        return result;
302    }
303
304    /**
305     * Do execution of CheckStyle based on Command line options.
306     * @param commandLine command line object
307     * @param filesToProcess List of files to process found from the command line.
308     * @return number of violations
309     * @throws IOException if a file could not be read.
310     * @throws CheckstyleException if something happens processing the files.
311     */
312    private static int runCli(CommandLine commandLine, List<File> filesToProcess)
313            throws IOException, CheckstyleException {
314        int result = 0;
315
316        // create config helper object
317        final CliOptions config = convertCliToPojo(commandLine, filesToProcess);
318        if (commandLine.hasOption(OPTION_T_NAME)) {
319            // print AST
320            final File file = config.files.get(0);
321            final String stringAst = AstTreeStringPrinter.printFileAst(file, false);
322            System.out.print(stringAst);
323        }
324        else if (commandLine.hasOption(OPTION_CAPITAL_T_NAME)) {
325            final File file = config.files.get(0);
326            final String stringAst = AstTreeStringPrinter.printFileAst(file, true);
327            System.out.print(stringAst);
328        }
329        else if (commandLine.hasOption(OPTION_J_NAME)) {
330            final File file = config.files.get(0);
331            final String stringAst = DetailNodeTreeStringPrinter.printFileAst(file);
332            System.out.print(stringAst);
333        }
334        else if (commandLine.hasOption(OPTION_CAPITAL_J_NAME)) {
335            final File file = config.files.get(0);
336            final String stringAst = AstTreeStringPrinter.printJavaAndJavadocTree(file);
337            System.out.print(stringAst);
338        }
339        else {
340            if (commandLine.hasOption(OPTION_D_NAME)) {
341                final Logger parentLogger = Logger.getLogger(Main.class.getName()).getParent();
342                final ConsoleHandler handler = new ConsoleHandler();
343                handler.setLevel(Level.FINEST);
344                handler.setFilter(new Filter() {
345                    private final String packageName = Main.class.getPackage().getName();
346
347                    @Override
348                    public boolean isLoggable(LogRecord record) {
349                        return record.getLoggerName().startsWith(packageName);
350                    }
351                });
352                parentLogger.addHandler(handler);
353                parentLogger.setLevel(Level.FINEST);
354            }
355            if (LOG.isDebugEnabled()) {
356                LOG.debug("Checkstyle debug logging enabled");
357                LOG.debug("Running Checkstyle with version: "
358                        + Main.class.getPackage().getImplementationVersion());
359            }
360
361            // run Checker
362            result = runCheckstyle(config);
363        }
364
365        return result;
366    }
367
368    /**
369     * Util method to convert CommandLine type to POJO object.
370     * @param cmdLine command line object
371     * @param filesToProcess List of files to process found from the command line.
372     * @return command line option as POJO object
373     */
374    private static CliOptions convertCliToPojo(CommandLine cmdLine, List<File> filesToProcess) {
375        final CliOptions conf = new CliOptions();
376        conf.format = cmdLine.getOptionValue(OPTION_F_NAME);
377        if (conf.format == null) {
378            conf.format = PLAIN_FORMAT_NAME;
379        }
380        conf.outputLocation = cmdLine.getOptionValue(OPTION_O_NAME);
381        conf.configLocation = cmdLine.getOptionValue(OPTION_C_NAME);
382        conf.propertiesLocation = cmdLine.getOptionValue(OPTION_P_NAME);
383        conf.files = filesToProcess;
384        conf.executeIgnoredModules = cmdLine.hasOption(OPTION_EXECUTE_IGNORED_MODULES_NAME);
385        return conf;
386    }
387
388    /**
389     * Executes required Checkstyle actions based on passed parameters.
390     * @param cliOptions
391     *        pojo object that contains all options
392     * @return number of violations of ERROR level
393     * @throws FileNotFoundException
394     *         when output file could not be found
395     * @throws CheckstyleException
396     *         when properties file could not be loaded
397     */
398    private static int runCheckstyle(CliOptions cliOptions)
399            throws CheckstyleException, FileNotFoundException {
400        // setup the properties
401        final Properties props;
402
403        if (cliOptions.propertiesLocation == null) {
404            props = System.getProperties();
405        }
406        else {
407            props = loadProperties(new File(cliOptions.propertiesLocation));
408        }
409
410        // create a configuration
411        final Configuration config = ConfigurationLoader.loadConfiguration(
412                cliOptions.configLocation, new PropertiesExpander(props),
413                !cliOptions.executeIgnoredModules);
414
415        // create a listener for output
416        final AuditListener listener = createListener(cliOptions.format, cliOptions.outputLocation);
417
418        // create RootModule object and run it
419        final int errorCounter;
420        final ClassLoader moduleClassLoader = Checker.class.getClassLoader();
421        final RootModule rootModule = getRootModule(config.getName(), moduleClassLoader);
422
423        try {
424
425            rootModule.setModuleClassLoader(moduleClassLoader);
426            rootModule.configure(config);
427            rootModule.addListener(listener);
428
429            // run RootModule
430            errorCounter = rootModule.process(cliOptions.files);
431
432        }
433        finally {
434            rootModule.destroy();
435        }
436
437        return errorCounter;
438    }
439
440    /**
441     * Creates a new instance of the root module that will control and run
442     * Checkstyle.
443     * @param name The name of the module. This will either be a short name that
444     *        will have to be found or the complete package name.
445     * @param moduleClassLoader Class loader used to load the root module.
446     * @return The new instance of the root module.
447     * @throws CheckstyleException if no module can be instantiated from name
448     */
449    private static RootModule getRootModule(String name, ClassLoader moduleClassLoader)
450            throws CheckstyleException {
451        final ModuleFactory factory = new PackageObjectFactory(
452                Checker.class.getPackage().getName(), moduleClassLoader);
453
454        return (RootModule) factory.createModule(name);
455    }
456
457    /**
458     * Loads properties from a File.
459     * @param file
460     *        the properties file
461     * @return the properties in file
462     * @throws CheckstyleException
463     *         when could not load properties file
464     */
465    private static Properties loadProperties(File file)
466            throws CheckstyleException {
467        final Properties properties = new Properties();
468
469        FileInputStream fis = null;
470        try {
471            fis = new FileInputStream(file);
472            properties.load(fis);
473        }
474        catch (final IOException ex) {
475            throw new CheckstyleException(String.format(
476                    "Unable to load properties from file '%s'.", file.getAbsolutePath()), ex);
477        }
478        finally {
479            Closeables.closeQuietly(fis);
480        }
481
482        return properties;
483    }
484
485    /**
486     * Creates the audit listener.
487     *
488     * @param format format of the audit listener
489     * @param outputLocation the location of output
490     * @return a fresh new {@code AuditListener}
491     * @exception FileNotFoundException when provided output location is not found
492     */
493    private static AuditListener createListener(String format,
494                                                String outputLocation)
495            throws FileNotFoundException {
496
497        // setup the output stream
498        final OutputStream out;
499        final boolean closeOutputStream;
500        if (outputLocation == null) {
501            out = System.out;
502            closeOutputStream = false;
503        }
504        else {
505            out = new FileOutputStream(outputLocation);
506            closeOutputStream = true;
507        }
508
509        // setup a listener
510        final AuditListener listener;
511        if (XML_FORMAT_NAME.equals(format)) {
512            listener = new XMLLogger(out, closeOutputStream);
513
514        }
515        else if (PLAIN_FORMAT_NAME.equals(format)) {
516            listener = new DefaultLogger(out, closeOutputStream, out, false);
517
518        }
519        else {
520            if (closeOutputStream) {
521                CommonUtils.close(out);
522            }
523            throw new IllegalStateException(String.format(
524                    "Invalid output format. Found '%s' but expected '%s' or '%s'.",
525                    format, PLAIN_FORMAT_NAME, XML_FORMAT_NAME));
526        }
527
528        return listener;
529    }
530
531    /**
532     * Determines the files to process.
533     * @param patternsToExclude The list of directory patterns to exclude from searching.
534     * @param filesToProcess
535     *        arguments that were not processed yet but shall be
536     * @return list of files to process
537     */
538    private static List<File> getFilesToProcess(List<Pattern> patternsToExclude,
539            String... filesToProcess) {
540        final List<File> files = new LinkedList<>();
541        for (String element : filesToProcess) {
542            files.addAll(listFiles(new File(element), patternsToExclude));
543        }
544
545        return files;
546    }
547
548    /**
549     * Traverses a specified node looking for files to check. Found files are added to a specified
550     * list. Subdirectories are also traversed.
551     * @param node
552     *        the node to process
553     * @param patternsToExclude The list of directory patterns to exclude from searching.
554     * @return found files
555     */
556    private static List<File> listFiles(File node, List<Pattern> patternsToExclude) {
557        // could be replaced with org.apache.commons.io.FileUtils.list() method
558        // if only we add commons-io library
559        final List<File> result = new LinkedList<>();
560
561        if (node.canRead()) {
562            if (node.isDirectory()) {
563                if (!isDirectoryExcluded(node.getAbsolutePath(), patternsToExclude)) {
564                    final File[] files = node.listFiles();
565                    // listFiles() can return null, so we need to check it
566                    if (files != null) {
567                        for (File element : files) {
568                            result.addAll(listFiles(element, patternsToExclude));
569                        }
570                    }
571                }
572            }
573            else if (node.isFile()) {
574                result.add(node);
575            }
576        }
577        return result;
578    }
579
580    /**
581     * Checks if a directory {@code path} should be excluded based on if it matches one of the
582     * patterns supplied.
583     * @param path The path of the directory to check
584     * @param patternsToExclude The list of directory patterns to exclude from searching.
585     * @return True if the directory matches one of the patterns.
586     */
587    private static boolean isDirectoryExcluded(String path, List<Pattern> patternsToExclude) {
588        boolean result = false;
589
590        for (Pattern pattern : patternsToExclude) {
591            if (pattern.matcher(path).find()) {
592                result = true;
593                break;
594            }
595        }
596
597        return result;
598    }
599
600    /** Prints the usage information. **/
601    private static void printUsage() {
602        final HelpFormatter formatter = new HelpFormatter();
603        formatter.setWidth(HELP_WIDTH);
604        formatter.printHelp(String.format("java %s [options] -c <config.xml> file...",
605                Main.class.getName()), buildOptions());
606    }
607
608    /**
609     * Builds and returns list of parameters supported by cli Checkstyle.
610     * @return available options
611     */
612    private static Options buildOptions() {
613        final Options options = new Options();
614        options.addOption(OPTION_C_NAME, true, "Sets the check configuration file to use.");
615        options.addOption(OPTION_O_NAME, true, "Sets the output file. Defaults to stdout");
616        options.addOption(OPTION_P_NAME, true, "Loads the properties file");
617        options.addOption(OPTION_F_NAME, true, String.format(
618                "Sets the output format. (%s|%s). Defaults to %s",
619                PLAIN_FORMAT_NAME, XML_FORMAT_NAME, PLAIN_FORMAT_NAME));
620        options.addOption(OPTION_V_NAME, false, "Print product version and exit");
621        options.addOption(OPTION_T_NAME, OPTION_TREE_NAME, false,
622                "Print Abstract Syntax Tree(AST) of the file");
623        options.addOption(OPTION_CAPITAL_T_NAME, OPTION_TREE_COMMENT_NAME, false,
624                "Print Abstract Syntax Tree(AST) of the file including comments");
625        options.addOption(OPTION_J_NAME, OPTION_JAVADOC_TREE_NAME, false,
626                "Print Parse tree of the Javadoc comment");
627        options.addOption(OPTION_CAPITAL_J_NAME, OPTION_TREE_JAVADOC_NAME, false,
628                "Print full Abstract Syntax Tree of the file");
629        options.addOption(OPTION_D_NAME, OPTION_DEBUG_NAME, false,
630                "Print all debug logging of CheckStyle utility");
631        options.addOption(OPTION_E_NAME, OPTION_EXCLUDE_NAME, true,
632                "Directory path to exclude from CheckStyle");
633        options.addOption(OPTION_X_NAME, OPTION_EXCLUDE_REGEXP_NAME, true,
634                "Regular expression of directory to exclude from CheckStyle");
635        options.addOption(OPTION_EXECUTE_IGNORED_MODULES_NAME, false,
636                "Allows ignored modules to be run.");
637        return options;
638    }
639
640    /** Helper structure to clear show what is required for Checker to run. **/
641    private static class CliOptions {
642        /** Properties file location. */
643        private String propertiesLocation;
644        /** Config file location. */
645        private String configLocation;
646        /** Output format. */
647        private String format;
648        /** Output file location. */
649        private String outputLocation;
650        /** List of file to validate. */
651        private List<File> files;
652        /** Switch whether to execute ignored modules or not. */
653        private boolean executeIgnoredModules;
654    }
655}