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