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}