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