001/////////////////////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code and other text files for adherence to a set of rules. 003// Copyright (C) 2001-2022 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 /** The loaded configurations. **/ 137 private final Deque<DefaultConfiguration> configStack = new ArrayDeque<>(); 138 139 /** Flags if modules with the severity 'ignore' should be omitted. */ 140 private final boolean omitIgnoredModules; 141 142 /** The thread mode configuration. */ 143 private final ThreadModeSettings threadModeSettings; 144 145 /** The Configuration that is being built. */ 146 private Configuration configuration; 147 148 /** 149 * Creates a new {@code ConfigurationLoader} instance. 150 * 151 * @param overrideProps resolver for overriding properties 152 * @param omitIgnoredModules {@code true} if ignored modules should be 153 * omitted 154 * @param threadModeSettings the thread mode configuration 155 * @throws ParserConfigurationException if an error occurs 156 * @throws SAXException if an error occurs 157 */ 158 private ConfigurationLoader(final PropertyResolver overrideProps, 159 final boolean omitIgnoredModules, 160 final ThreadModeSettings threadModeSettings) 161 throws ParserConfigurationException, SAXException { 162 saxHandler = new InternalLoader(); 163 overridePropsResolver = overrideProps; 164 this.omitIgnoredModules = omitIgnoredModules; 165 this.threadModeSettings = threadModeSettings; 166 } 167 168 /** 169 * Creates mapping between local resources and dtd ids. This method can't be 170 * moved to inner class because it must stay static because it is called 171 * from constructor and inner class isn't static. 172 * 173 * @return map between local resources and dtd ids. 174 */ 175 private static Map<String, String> createIdToResourceNameMap() { 176 final Map<String, String> map = new HashMap<>(); 177 map.put(DTD_PUBLIC_ID_1_0, DTD_CONFIGURATION_NAME_1_0); 178 map.put(DTD_PUBLIC_ID_1_1, DTD_CONFIGURATION_NAME_1_1); 179 map.put(DTD_PUBLIC_ID_1_2, DTD_CONFIGURATION_NAME_1_2); 180 map.put(DTD_PUBLIC_ID_1_3, DTD_CONFIGURATION_NAME_1_3); 181 map.put(DTD_PUBLIC_CS_ID_1_0, DTD_CONFIGURATION_NAME_1_0); 182 map.put(DTD_PUBLIC_CS_ID_1_1, DTD_CONFIGURATION_NAME_1_1); 183 map.put(DTD_PUBLIC_CS_ID_1_2, DTD_CONFIGURATION_NAME_1_2); 184 map.put(DTD_PUBLIC_CS_ID_1_3, DTD_CONFIGURATION_NAME_1_3); 185 return map; 186 } 187 188 /** 189 * Parses the specified input source loading the configuration information. 190 * The stream wrapped inside the source, if any, is NOT 191 * explicitly closed after parsing, it is the responsibility of 192 * the caller to close the stream. 193 * 194 * @param source the source that contains the configuration data 195 * @throws IOException if an error occurs 196 * @throws SAXException if an error occurs 197 */ 198 private void parseInputSource(InputSource source) 199 throws IOException, SAXException { 200 saxHandler.parseInputSource(source); 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 * @return the check configurations 209 * @throws CheckstyleException if an error occurs 210 */ 211 public static Configuration loadConfiguration(String config, 212 PropertyResolver overridePropsResolver) throws CheckstyleException { 213 return loadConfiguration(config, overridePropsResolver, IgnoredModulesOptions.EXECUTE); 214 } 215 216 /** 217 * Returns the module configurations in a specified file. 218 * 219 * @param config location of config file, can be either a URL or a filename 220 * @param overridePropsResolver overriding properties 221 * @param threadModeSettings the thread mode configuration 222 * @return the check configurations 223 * @throws CheckstyleException if an error occurs 224 */ 225 public static Configuration loadConfiguration(String config, 226 PropertyResolver overridePropsResolver, ThreadModeSettings threadModeSettings) 227 throws CheckstyleException { 228 return loadConfiguration(config, overridePropsResolver, 229 IgnoredModulesOptions.EXECUTE, threadModeSettings); 230 } 231 232 /** 233 * Returns the module configurations in a specified file. 234 * 235 * @param config location of config file, can be either a URL or a filename 236 * @param overridePropsResolver overriding properties 237 * @param ignoredModulesOptions {@code OMIT} if modules with severity 238 * 'ignore' should be omitted, {@code EXECUTE} otherwise 239 * @return the check configurations 240 * @throws CheckstyleException if an error occurs 241 */ 242 public static Configuration loadConfiguration(String config, 243 PropertyResolver overridePropsResolver, 244 IgnoredModulesOptions ignoredModulesOptions) 245 throws CheckstyleException { 246 return loadConfiguration(config, overridePropsResolver, ignoredModulesOptions, 247 ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE); 248 } 249 250 /** 251 * Returns the module configurations in a specified file. 252 * 253 * @param config location of config file, can be either a URL or a filename 254 * @param overridePropsResolver overriding properties 255 * @param ignoredModulesOptions {@code OMIT} if modules with severity 256 * 'ignore' should be omitted, {@code EXECUTE} otherwise 257 * @param threadModeSettings the thread mode configuration 258 * @return the check configurations 259 * @throws CheckstyleException if an error occurs 260 */ 261 public static Configuration loadConfiguration(String config, 262 PropertyResolver overridePropsResolver, 263 IgnoredModulesOptions ignoredModulesOptions, 264 ThreadModeSettings threadModeSettings) 265 throws CheckstyleException { 266 // figure out if this is a File or a URL 267 final URI uri = CommonUtil.getUriByFilename(config); 268 final InputSource source = new InputSource(uri.toString()); 269 return loadConfiguration(source, overridePropsResolver, 270 ignoredModulesOptions, threadModeSettings); 271 } 272 273 /** 274 * Returns the module configurations from a specified input source. 275 * Note that if the source does wrap an open byte or character 276 * stream, clients are required to close that stream by themselves 277 * 278 * @param configSource the input stream to the Checkstyle configuration 279 * @param overridePropsResolver overriding properties 280 * @param ignoredModulesOptions {@code OMIT} if modules with severity 281 * 'ignore' should be omitted, {@code EXECUTE} otherwise 282 * @return the check configurations 283 * @throws CheckstyleException if an error occurs 284 */ 285 public static Configuration loadConfiguration(InputSource configSource, 286 PropertyResolver overridePropsResolver, 287 IgnoredModulesOptions ignoredModulesOptions) 288 throws CheckstyleException { 289 return loadConfiguration(configSource, overridePropsResolver, 290 ignoredModulesOptions, ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE); 291 } 292 293 /** 294 * Returns the module configurations from a specified input source. 295 * Note that if the source does wrap an open byte or character 296 * stream, clients are required to close that stream by themselves 297 * 298 * @param configSource the input stream to the Checkstyle configuration 299 * @param overridePropsResolver overriding properties 300 * @param ignoredModulesOptions {@code OMIT} if modules with severity 301 * 'ignore' should be omitted, {@code EXECUTE} otherwise 302 * @param threadModeSettings the thread mode configuration 303 * @return the check configurations 304 * @throws CheckstyleException if an error occurs 305 * @noinspection WeakerAccess 306 * @noinspectionreason WeakerAccess - we avoid 'protected' when possible 307 */ 308 public static Configuration loadConfiguration(InputSource configSource, 309 PropertyResolver overridePropsResolver, 310 IgnoredModulesOptions ignoredModulesOptions, 311 ThreadModeSettings threadModeSettings) 312 throws CheckstyleException { 313 try { 314 final boolean omitIgnoreModules = ignoredModulesOptions == IgnoredModulesOptions.OMIT; 315 final ConfigurationLoader loader = 316 new ConfigurationLoader(overridePropsResolver, 317 omitIgnoreModules, threadModeSettings); 318 loader.parseInputSource(configSource); 319 return loader.configuration; 320 } 321 catch (final SAXParseException ex) { 322 final String message = String.format(Locale.ROOT, SAX_PARSE_EXCEPTION_FORMAT, 323 UNABLE_TO_PARSE_EXCEPTION_PREFIX, 324 ex.getMessage(), ex.getLineNumber(), ex.getColumnNumber()); 325 throw new CheckstyleException(message, ex); 326 } 327 catch (final ParserConfigurationException | IOException | SAXException ex) { 328 throw new CheckstyleException(UNABLE_TO_PARSE_EXCEPTION_PREFIX, ex); 329 } 330 } 331 332 /** 333 * Replaces {@code ${xxx}} style constructions in the given value 334 * with the string value of the corresponding data types. This method must remain 335 * outside inner class for easier testing since inner class requires an instance. 336 * 337 * <p>Code copied from ant - 338 * http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java 339 * 340 * @param value The string to be scanned for property references. Must 341 * not be {@code null}. 342 * @param props Mapping (String to String) of property names to their 343 * values. Must not be {@code null}. 344 * @param defaultValue default to use if one of the properties in value 345 * cannot be resolved from props. 346 * 347 * @return the original string with the properties replaced. 348 * @throws CheckstyleException if the string contains an opening 349 * {@code ${} without a closing 350 * {@code }} 351 */ 352 private static String replaceProperties( 353 String value, PropertyResolver props, String defaultValue) 354 throws CheckstyleException { 355 356 final List<String> fragments = new ArrayList<>(); 357 final List<String> propertyRefs = new ArrayList<>(); 358 parsePropertyString(value, fragments, propertyRefs); 359 360 final StringBuilder sb = new StringBuilder(256); 361 final Iterator<String> fragmentsIterator = fragments.iterator(); 362 final Iterator<String> propertyRefsIterator = propertyRefs.iterator(); 363 while (fragmentsIterator.hasNext()) { 364 String fragment = fragmentsIterator.next(); 365 if (fragment == null) { 366 final String propertyName = propertyRefsIterator.next(); 367 fragment = props.resolve(propertyName); 368 if (fragment == null) { 369 if (defaultValue != null) { 370 sb.replace(0, sb.length(), defaultValue); 371 break; 372 } 373 throw new CheckstyleException( 374 "Property ${" + propertyName + "} has not been set"); 375 } 376 } 377 sb.append(fragment); 378 } 379 380 return sb.toString(); 381 } 382 383 /** 384 * Parses a string containing {@code ${xxx}} style property 385 * references into two collections. The first one is a collection 386 * of text fragments, while the other is a set of string property names. 387 * {@code null} entries in the first collection indicate a property 388 * reference from the second collection. 389 * 390 * <p>Code copied from ant - 391 * http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java 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 /** 482 * Creates a new InternalLoader. 483 * 484 * @throws SAXException if an error occurs 485 * @throws ParserConfigurationException if an error occurs 486 */ 487 /* package */ InternalLoader() 488 throws SAXException, ParserConfigurationException { 489 super(createIdToResourceNameMap()); 490 } 491 492 @Override 493 public void startElement(String uri, 494 String localName, 495 String qName, 496 Attributes attributes) 497 throws SAXException { 498 if (MODULE.equals(qName)) { 499 // create configuration 500 final String originalName = attributes.getValue(NAME); 501 final String name = threadModeSettings.resolveName(originalName); 502 final DefaultConfiguration conf = 503 new DefaultConfiguration(name, threadModeSettings); 504 505 if (configuration == null) { 506 configuration = conf; 507 } 508 509 // add configuration to it's parent 510 if (!configStack.isEmpty()) { 511 final DefaultConfiguration top = 512 configStack.peek(); 513 top.addChild(conf); 514 } 515 516 configStack.push(conf); 517 } 518 else if (PROPERTY.equals(qName)) { 519 // extract value and name 520 final String attributesValue = attributes.getValue(VALUE); 521 522 final String value; 523 try { 524 value = replaceProperties(attributesValue, 525 overridePropsResolver, attributes.getValue(DEFAULT)); 526 } 527 catch (final CheckstyleException ex) { 528 // -@cs[IllegalInstantiation] SAXException is in the overridden 529 // method signature 530 throw new SAXException(ex); 531 } 532 533 final String name = attributes.getValue(NAME); 534 535 // add to attributes of configuration 536 final DefaultConfiguration top = 537 configStack.peek(); 538 top.addProperty(name, value); 539 } 540 else if (MESSAGE.equals(qName)) { 541 // extract key and value 542 final String key = attributes.getValue(KEY); 543 final String value = attributes.getValue(VALUE); 544 545 // add to messages of configuration 546 final DefaultConfiguration top = configStack.peek(); 547 top.addMessage(key, value); 548 } 549 else { 550 if (!METADATA.equals(qName)) { 551 throw new IllegalStateException("Unknown name:" + qName + "."); 552 } 553 } 554 } 555 556 @Override 557 public void endElement(String uri, 558 String localName, 559 String qName) throws SAXException { 560 if (MODULE.equals(qName)) { 561 final Configuration recentModule = 562 configStack.pop(); 563 564 // get severity attribute if it exists 565 SeverityLevel level = null; 566 if (containsAttribute(recentModule, SEVERITY)) { 567 try { 568 final String severity = recentModule.getProperty(SEVERITY); 569 level = SeverityLevel.getInstance(severity); 570 } 571 catch (final CheckstyleException ex) { 572 // -@cs[IllegalInstantiation] SAXException is in the overridden 573 // method signature 574 throw new SAXException( 575 "Problem during accessing '" + SEVERITY + "' attribute for " 576 + recentModule.getName(), ex); 577 } 578 } 579 580 // omit this module if these should be omitted and the module 581 // has the severity 'ignore' 582 final boolean omitModule = omitIgnoredModules 583 && level == SeverityLevel.IGNORE; 584 585 if (omitModule && !configStack.isEmpty()) { 586 final DefaultConfiguration parentModule = 587 configStack.peek(); 588 parentModule.removeChild(recentModule); 589 } 590 } 591 } 592 593 /** 594 * Util method to recheck attribute in module. 595 * 596 * @param module module to check 597 * @param attributeName name of attribute in module to find 598 * @return true if attribute is present in module 599 */ 600 private boolean containsAttribute(Configuration module, String attributeName) { 601 final String[] names = module.getPropertyNames(); 602 final Optional<String> result = Arrays.stream(names) 603 .filter(name -> name.equals(attributeName)).findFirst(); 604 return result.isPresent(); 605 } 606 607 } 608 609}