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