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