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