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}