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}