001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2014 Oliver Burn 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//////////////////////////////////////////////////////////////////////////////// 019package com.puppycrawl.tools.checkstyle; 020 021import com.google.common.collect.Lists; 022import com.google.common.collect.Maps; 023import com.puppycrawl.tools.checkstyle.api.AbstractLoader; 024import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 025import com.puppycrawl.tools.checkstyle.api.Configuration; 026import com.puppycrawl.tools.checkstyle.api.FastStack; 027import com.puppycrawl.tools.checkstyle.api.SeverityLevel; 028import org.xml.sax.Attributes; 029import org.xml.sax.InputSource; 030import org.xml.sax.SAXException; 031import org.xml.sax.SAXParseException; 032 033import javax.xml.parsers.ParserConfigurationException; 034import java.io.File; 035import java.io.FileNotFoundException; 036import java.io.IOException; 037import java.io.InputStream; 038import java.net.MalformedURLException; 039import java.net.URI; 040import java.net.URISyntaxException; 041import java.net.URL; 042import java.util.Iterator; 043import java.util.List; 044import java.util.Map; 045 046/** 047 * Loads a configuration from a standard configuration XML file. 048 * 049 * @author Oliver Burn 050 * @version 1.0 051 */ 052public final class ConfigurationLoader 053{ 054 /** the public ID for version 1_0 of the configuration dtd */ 055 private static final String DTD_PUBLIC_ID_1_0 = 056 "-//Puppy Crawl//DTD Check Configuration 1.0//EN"; 057 058 /** the resource for version 1_0 of the configuration dtd */ 059 private static final String DTD_RESOURCE_NAME_1_0 = 060 "com/puppycrawl/tools/checkstyle/configuration_1_0.dtd"; 061 062 /** the public ID for version 1_1 of the configuration dtd */ 063 private static final String DTD_PUBLIC_ID_1_1 = 064 "-//Puppy Crawl//DTD Check Configuration 1.1//EN"; 065 066 /** the resource for version 1_1 of the configuration dtd */ 067 private static final String DTD_RESOURCE_NAME_1_1 = 068 "com/puppycrawl/tools/checkstyle/configuration_1_1.dtd"; 069 070 /** the public ID for version 1_2 of the configuration dtd */ 071 private static final String DTD_PUBLIC_ID_1_2 = 072 "-//Puppy Crawl//DTD Check Configuration 1.2//EN"; 073 074 /** the resource for version 1_2 of the configuration dtd */ 075 private static final String DTD_RESOURCE_NAME_1_2 = 076 "com/puppycrawl/tools/checkstyle/configuration_1_2.dtd"; 077 078 /** the public ID for version 1_3 of the configuration dtd */ 079 private static final String DTD_PUBLIC_ID_1_3 = 080 "-//Puppy Crawl//DTD Check Configuration 1.3//EN"; 081 082 /** the resource for version 1_3 of the configuration dtd */ 083 private static final String DTD_RESOURCE_NAME_1_3 = 084 "com/puppycrawl/tools/checkstyle/configuration_1_3.dtd"; 085 086 /** 087 * Implements the SAX document handler interfaces, so they do not 088 * appear in the public API of the ConfigurationLoader. 089 */ 090 private final class InternalLoader 091 extends AbstractLoader 092 { 093 /** module elements */ 094 private static final String MODULE = "module"; 095 /** name attribute */ 096 private static final String NAME = "name"; 097 /** property element */ 098 private static final String PROPERTY = "property"; 099 /** value attribute */ 100 private static final String VALUE = "value"; 101 /** default attribute */ 102 private static final String DEFAULT = "default"; 103 /** name of the severity property */ 104 private static final String SEVERITY = "severity"; 105 /** name of the message element */ 106 private static final String MESSAGE = "message"; 107 /** name of the key attribute */ 108 private static final String KEY = "key"; 109 110 /** 111 * Creates a new InternalLoader. 112 * @throws SAXException if an error occurs 113 * @throws ParserConfigurationException if an error occurs 114 */ 115 private InternalLoader() 116 throws SAXException, ParserConfigurationException 117 { 118 // super(DTD_PUBLIC_ID_1_1, DTD_RESOURCE_NAME_1_1); 119 super(createIdToResourceNameMap()); 120 } 121 122 @Override 123 public void startElement(String namespaceURI, 124 String localName, 125 String qName, 126 Attributes atts) 127 throws SAXException 128 { 129 // TODO: debug logging for support purposes 130 if (qName.equals(MODULE)) { 131 //create configuration 132 final String name = atts.getValue(NAME); 133 final DefaultConfiguration conf = 134 new DefaultConfiguration(name); 135 136 if (configuration == null) { 137 configuration = conf; 138 } 139 140 //add configuration to it's parent 141 if (!configStack.isEmpty()) { 142 final DefaultConfiguration top = 143 configStack.peek(); 144 top.addChild(conf); 145 } 146 147 configStack.push(conf); 148 } 149 else if (qName.equals(PROPERTY)) { 150 //extract name and value 151 final String name = atts.getValue(NAME); 152 final String value; 153 try { 154 value = replaceProperties(atts.getValue(VALUE), 155 overridePropsResolver, atts.getValue(DEFAULT)); 156 } 157 catch (final CheckstyleException ex) { 158 throw new SAXException(ex.getMessage()); 159 } 160 161 //add to attributes of configuration 162 final DefaultConfiguration top = 163 configStack.peek(); 164 top.addAttribute(name, value); 165 } 166 else if (qName.equals(MESSAGE)) { 167 //extract key and value 168 final String key = atts.getValue(KEY); 169 final String value = atts.getValue(VALUE); 170 171 //add to messages of configuration 172 final DefaultConfiguration top = configStack.peek(); 173 top.addMessage(key, value); 174 } 175 } 176 177 @Override 178 public void endElement(String namespaceURI, 179 String localName, 180 String qName) 181 throws SAXException 182 { 183 if (qName.equals(MODULE)) { 184 185 final Configuration recentModule = 186 configStack.pop(); 187 188 // remove modules with severity ignore if these modules should 189 // be omitted 190 SeverityLevel level = null; 191 try { 192 final String severity = recentModule.getAttribute(SEVERITY); 193 level = SeverityLevel.getInstance(severity); 194 } 195 catch (final CheckstyleException e) { 196 //severity not set -> ignore 197 ; 198 } 199 200 // omit this module if these should be omitted and the module 201 // has the severity 'ignore' 202 final boolean omitModule = omitIgnoredModules 203 && SeverityLevel.IGNORE.equals(level); 204 205 if (omitModule && !configStack.isEmpty()) { 206 final DefaultConfiguration parentModule = 207 configStack.peek(); 208 parentModule.removeChild(recentModule); 209 } 210 } 211 } 212 213 } 214 215 /** the SAX document handler */ 216 private final InternalLoader saxHandler; 217 218 /** property resolver **/ 219 private final PropertyResolver overridePropsResolver; 220 /** the loaded configurations **/ 221 private final FastStack<DefaultConfiguration> configStack = 222 FastStack.newInstance(); 223 /** the Configuration that is being built */ 224 private Configuration configuration; 225 226 /** flags if modules with the severity 'ignore' should be omitted. */ 227 private final boolean omitIgnoredModules; 228 229 /** 230 * Creates mapping between local resources and dtd ids. 231 * @return map between local resources and dtd ids. 232 */ 233 private static Map<String, String> createIdToResourceNameMap() 234 { 235 final Map<String, String> map = Maps.newHashMap(); 236 map.put(DTD_PUBLIC_ID_1_0, DTD_RESOURCE_NAME_1_0); 237 map.put(DTD_PUBLIC_ID_1_1, DTD_RESOURCE_NAME_1_1); 238 map.put(DTD_PUBLIC_ID_1_2, DTD_RESOURCE_NAME_1_2); 239 map.put(DTD_PUBLIC_ID_1_3, DTD_RESOURCE_NAME_1_3); 240 return map; 241 } 242 243 /** 244 * Creates a new <code>ConfigurationLoader</code> instance. 245 * @param overrideProps resolver for overriding properties 246 * @param omitIgnoredModules <code>true</code> if ignored modules should be 247 * omitted 248 * @throws ParserConfigurationException if an error occurs 249 * @throws SAXException if an error occurs 250 */ 251 private ConfigurationLoader(final PropertyResolver overrideProps, 252 final boolean omitIgnoredModules) 253 throws ParserConfigurationException, SAXException 254 { 255 saxHandler = new InternalLoader(); 256 overridePropsResolver = overrideProps; 257 this.omitIgnoredModules = omitIgnoredModules; 258 } 259 260 /** 261 * Parses the specified input source loading the configuration information. 262 * The stream wrapped inside the source, if any, is NOT 263 * explicitely closed after parsing, it is the responsibility of 264 * the caller to close the stream. 265 * 266 * @param source the source that contains the configuration data 267 * @throws IOException if an error occurs 268 * @throws SAXException if an error occurs 269 */ 270 private void parseInputSource(InputSource source) 271 throws IOException, SAXException 272 { 273 saxHandler.parseInputSource(source); 274 } 275 276 /** 277 * Returns the module configurations in a specified file. 278 * @param config location of config file, can be either a URL or a filename 279 * @param overridePropsResolver overriding properties 280 * @return the check configurations 281 * @throws CheckstyleException if an error occurs 282 */ 283 public static Configuration loadConfiguration(String config, 284 PropertyResolver overridePropsResolver) throws CheckstyleException 285 { 286 return loadConfiguration(config, overridePropsResolver, false); 287 } 288 289 /** 290 * Returns the module configurations in a specified file. 291 * 292 * @param config location of config file, can be either a URL or a filename 293 * @param overridePropsResolver overriding properties 294 * @param omitIgnoredModules <code>true</code> if modules with severity 295 * 'ignore' should be omitted, <code>false</code> otherwise 296 * @return the check configurations 297 * @throws CheckstyleException if an error occurs 298 */ 299 public static Configuration loadConfiguration(String config, 300 PropertyResolver overridePropsResolver, boolean omitIgnoredModules) 301 throws CheckstyleException 302 { 303 try { 304 // figure out if this is a File or a URL 305 URI uri; 306 try { 307 final URL url = new URL(config); 308 uri = url.toURI(); 309 } 310 catch (final MalformedURLException ex) { 311 uri = null; 312 } 313 catch (final URISyntaxException ex) { 314 // URL violating RFC 2396 315 uri = null; 316 } 317 if (uri == null) { 318 final File file = new File(config); 319 if (file.exists()) { 320 uri = file.toURI(); 321 } 322 else { 323 // check to see if the file is in the classpath 324 try { 325 final URL configUrl = ConfigurationLoader.class 326 .getResource(config); 327 if (configUrl == null) { 328 throw new FileNotFoundException(config); 329 } 330 uri = configUrl.toURI(); 331 } 332 catch (final URISyntaxException e) { 333 throw new FileNotFoundException(config); 334 } 335 } 336 } 337 final InputSource source = new InputSource(uri.toString()); 338 return loadConfiguration(source, overridePropsResolver, 339 omitIgnoredModules); 340 } 341 catch (final FileNotFoundException e) { 342 throw new CheckstyleException("unable to find " + config, e); 343 } 344 catch (final CheckstyleException e) { 345 //wrap again to add file name info 346 throw new CheckstyleException("unable to read " + config + " - " 347 + e.getMessage(), e); 348 } 349 } 350 351 /** 352 * Returns the module configurations from a specified input stream. 353 * Note that clients are required to close the given stream by themselves 354 * 355 * @param configStream the input stream to the Checkstyle configuration 356 * @param overridePropsResolver overriding properties 357 * @param omitIgnoredModules <code>true</code> if modules with severity 358 * 'ignore' should be omitted, <code>false</code> otherwise 359 * @return the check configurations 360 * @throws CheckstyleException if an error occurs 361 * 362 * @deprecated As this method does not provide a valid system ID, 363 * preventing resolution of external entities, a 364 * {@link #loadConfiguration(InputSource,PropertyResolver,boolean) 365 * version using an InputSource} 366 * should be used instead 367 */ 368 @Deprecated 369 public static Configuration loadConfiguration(InputStream configStream, 370 PropertyResolver overridePropsResolver, boolean omitIgnoredModules) 371 throws CheckstyleException 372 { 373 return loadConfiguration(new InputSource(configStream), 374 overridePropsResolver, omitIgnoredModules); 375 } 376 377 /** 378 * Returns the module configurations from a specified input source. 379 * Note that if the source does wrap an open byte or character 380 * stream, clients are required to close that stream by themselves 381 * 382 * @param configSource the input stream to the Checkstyle configuration 383 * @param overridePropsResolver overriding properties 384 * @param omitIgnoredModules <code>true</code> if modules with severity 385 * 'ignore' should be omitted, <code>false</code> otherwise 386 * @return the check configurations 387 * @throws CheckstyleException if an error occurs 388 */ 389 public static Configuration loadConfiguration(InputSource configSource, 390 PropertyResolver overridePropsResolver, boolean omitIgnoredModules) 391 throws CheckstyleException 392 { 393 try { 394 final ConfigurationLoader loader = 395 new ConfigurationLoader(overridePropsResolver, 396 omitIgnoredModules); 397 loader.parseInputSource(configSource); 398 return loader.getConfiguration(); 399 } 400 catch (final ParserConfigurationException e) { 401 throw new CheckstyleException( 402 "unable to parse configuration stream", e); 403 } 404 catch (final SAXParseException e) { 405 throw new CheckstyleException("unable to parse configuration stream" 406 + " - " + e.getMessage() + ":" + e.getLineNumber() 407 + ":" + e.getColumnNumber(), e); 408 } 409 catch (final SAXException e) { 410 throw new CheckstyleException("unable to parse configuration stream" 411 + " - " + e.getMessage(), e); 412 } 413 catch (final IOException e) { 414 throw new CheckstyleException("unable to read from stream", e); 415 } 416 } 417 418 /** 419 * Returns the configuration in the last file parsed. 420 * @return Configuration object 421 */ 422 private Configuration getConfiguration() 423 { 424 return configuration; 425 } 426 427 /** 428 * Replaces <code>${xxx}</code> style constructions in the given value 429 * with the string value of the corresponding data types. 430 * 431 * The method is package visible to facilitate testing. 432 * 433 * @param value The string to be scanned for property references. 434 * May be <code>null</code>, in which case this 435 * method returns immediately with no effect. 436 * @param props Mapping (String to String) of property names to their 437 * values. Must not be <code>null</code>. 438 * @param defaultValue default to use if one of the properties in value 439 * cannot be resolved from props. 440 * 441 * @throws CheckstyleException if the string contains an opening 442 * <code>${</code> without a closing 443 * <code>}</code> 444 * @return the original string with the properties replaced, or 445 * <code>null</code> if the original string is <code>null</code>. 446 * 447 * Code copied from ant - 448 * http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java 449 */ 450 // Package visible for testing purposes 451 static String replaceProperties( 452 String value, PropertyResolver props, String defaultValue) 453 throws CheckstyleException 454 { 455 if (value == null) { 456 return null; 457 } 458 459 final List<String> fragments = Lists.newArrayList(); 460 final List<String> propertyRefs = Lists.newArrayList(); 461 parsePropertyString(value, fragments, propertyRefs); 462 463 final StringBuffer sb = new StringBuffer(); 464 final Iterator<String> i = fragments.iterator(); 465 final Iterator<String> j = propertyRefs.iterator(); 466 while (i.hasNext()) { 467 String fragment = i.next(); 468 if (fragment == null) { 469 final String propertyName = j.next(); 470 fragment = props.resolve(propertyName); 471 if (fragment == null) { 472 if (defaultValue != null) { 473 return defaultValue; 474 } 475 throw new CheckstyleException( 476 "Property ${" + propertyName + "} has not been set"); 477 } 478 } 479 sb.append(fragment); 480 } 481 482 return sb.toString(); 483 } 484 485 /** 486 * Parses a string containing <code>${xxx}</code> style property 487 * references into two lists. The first list is a collection 488 * of text fragments, while the other is a set of string property names. 489 * <code>null</code> entries in the first list indicate a property 490 * reference from the second list. 491 * 492 * @param value Text to parse. Must not be <code>null</code>. 493 * @param fragments List to add text fragments to. 494 * Must not be <code>null</code>. 495 * @param propertyRefs List to add property names to. 496 * Must not be <code>null</code>. 497 * 498 * @throws CheckstyleException if the string contains an opening 499 * <code>${</code> without a closing 500 * <code>}</code> 501 * Code copied from ant - 502 * http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java 503 */ 504 private static void parsePropertyString(String value, 505 List<String> fragments, 506 List<String> propertyRefs) 507 throws CheckstyleException 508 { 509 int prev = 0; 510 int pos; 511 //search for the next instance of $ from the 'prev' position 512 while ((pos = value.indexOf("$", prev)) >= 0) { 513 514 //if there was any text before this, add it as a fragment 515 //TODO, this check could be modified to go if pos>prev; 516 //seems like this current version could stick empty strings 517 //into the list 518 if (pos > 0) { 519 fragments.add(value.substring(prev, pos)); 520 } 521 //if we are at the end of the string, we tack on a $ 522 //then move past it 523 if (pos == (value.length() - 1)) { 524 fragments.add("$"); 525 prev = pos + 1; 526 } 527 else if (value.charAt(pos + 1) != '{') { 528 //peek ahead to see if the next char is a property or not 529 //not a property: insert the char as a literal 530 /* 531 fragments.addElement(value.substring(pos + 1, pos + 2)); 532 prev = pos + 2; 533 */ 534 if (value.charAt(pos + 1) == '$') { 535 //backwards compatibility two $ map to one mode 536 fragments.add("$"); 537 prev = pos + 2; 538 } 539 else { 540 //new behaviour: $X maps to $X for all values of X!='$' 541 fragments.add(value.substring(pos, pos + 2)); 542 prev = pos + 2; 543 } 544 545 } 546 else { 547 //property found, extract its name or bail on a typo 548 final int endName = value.indexOf('}', pos); 549 if (endName < 0) { 550 throw new CheckstyleException("Syntax error in property: " 551 + value); 552 } 553 final String propertyName = value.substring(pos + 2, endName); 554 fragments.add(null); 555 propertyRefs.add(propertyName); 556 prev = endName + 1; 557 } 558 } 559 //no more $ signs found 560 //if there is any tail to the file, append it 561 if (prev < value.length()) { 562 fragments.add(value.substring(prev)); 563 } 564 } 565}