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