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