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