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