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