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