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}