001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2016 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.io.InputStream; 024import java.net.URI; 025import java.util.ArrayDeque; 026import java.util.Deque; 027import java.util.Iterator; 028import java.util.List; 029import java.util.Locale; 030import java.util.Map; 031 032import javax.xml.parsers.ParserConfigurationException; 033 034import org.apache.commons.logging.Log; 035import org.apache.commons.logging.LogFactory; 036import org.xml.sax.Attributes; 037import org.xml.sax.InputSource; 038import org.xml.sax.SAXException; 039import org.xml.sax.SAXParseException; 040 041import com.google.common.collect.Lists; 042import com.google.common.collect.Maps; 043import com.puppycrawl.tools.checkstyle.api.AbstractLoader; 044import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 045import com.puppycrawl.tools.checkstyle.api.Configuration; 046import com.puppycrawl.tools.checkstyle.api.SeverityLevel; 047import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 048 049/** 050 * Loads a configuration from a standard configuration XML file. 051 * 052 * @author Oliver Burn 053 */ 054public final class ConfigurationLoader { 055 /** Logger for ConfigurationLoader. */ 056 private static final Log LOG = LogFactory.getLog(ConfigurationLoader.class); 057 058 /** The public ID for version 1_0 of the configuration dtd. */ 059 private static final String DTD_PUBLIC_ID_1_0 = 060 "-//Puppy Crawl//DTD Check Configuration 1.0//EN"; 061 062 /** The resource for version 1_0 of the configuration dtd. */ 063 private static final String DTD_RESOURCE_NAME_1_0 = 064 "com/puppycrawl/tools/checkstyle/configuration_1_0.dtd"; 065 066 /** The public ID for version 1_1 of the configuration dtd. */ 067 private static final String DTD_PUBLIC_ID_1_1 = 068 "-//Puppy Crawl//DTD Check Configuration 1.1//EN"; 069 070 /** The resource for version 1_1 of the configuration dtd. */ 071 private static final String DTD_RESOURCE_NAME_1_1 = 072 "com/puppycrawl/tools/checkstyle/configuration_1_1.dtd"; 073 074 /** The public ID for version 1_2 of the configuration dtd. */ 075 private static final String DTD_PUBLIC_ID_1_2 = 076 "-//Puppy Crawl//DTD Check Configuration 1.2//EN"; 077 078 /** The resource for version 1_2 of the configuration dtd. */ 079 private static final String DTD_RESOURCE_NAME_1_2 = 080 "com/puppycrawl/tools/checkstyle/configuration_1_2.dtd"; 081 082 /** The public ID for version 1_3 of the configuration dtd. */ 083 private static final String DTD_PUBLIC_ID_1_3 = 084 "-//Puppy Crawl//DTD Check Configuration 1.3//EN"; 085 086 /** The resource for version 1_3 of the configuration dtd. */ 087 private static final String DTD_RESOURCE_NAME_1_3 = 088 "com/puppycrawl/tools/checkstyle/configuration_1_3.dtd"; 089 090 /** Prefix for the exception when unable to parse resource. */ 091 private static final String UNABLE_TO_PARSE_EXCEPTION_PREFIX = "unable to parse" 092 + " configuration stream"; 093 094 /** Dollar sign literal. */ 095 private static final char DOLLAR_SIGN = '$'; 096 097 /** The SAX document handler. */ 098 private final InternalLoader saxHandler; 099 100 /** Property resolver. **/ 101 private final PropertyResolver overridePropsResolver; 102 /** The loaded configurations. **/ 103 private final Deque<DefaultConfiguration> configStack = new ArrayDeque<>(); 104 105 /** Flags if modules with the severity 'ignore' should be omitted. */ 106 private final boolean omitIgnoredModules; 107 108 /** The Configuration that is being built. */ 109 private Configuration configuration; 110 111 /** 112 * Creates a new {@code ConfigurationLoader} instance. 113 * @param overrideProps resolver for overriding properties 114 * @param omitIgnoredModules {@code true} if ignored modules should be 115 * omitted 116 * @throws ParserConfigurationException if an error occurs 117 * @throws SAXException if an error occurs 118 */ 119 private ConfigurationLoader(final PropertyResolver overrideProps, 120 final boolean omitIgnoredModules) 121 throws ParserConfigurationException, SAXException { 122 saxHandler = new InternalLoader(); 123 overridePropsResolver = overrideProps; 124 this.omitIgnoredModules = omitIgnoredModules; 125 } 126 127 /** 128 * Creates mapping between local resources and dtd ids. 129 * @return map between local resources and dtd ids. 130 */ 131 private static Map<String, String> createIdToResourceNameMap() { 132 final Map<String, String> map = Maps.newHashMap(); 133 map.put(DTD_PUBLIC_ID_1_0, DTD_RESOURCE_NAME_1_0); 134 map.put(DTD_PUBLIC_ID_1_1, DTD_RESOURCE_NAME_1_1); 135 map.put(DTD_PUBLIC_ID_1_2, DTD_RESOURCE_NAME_1_2); 136 map.put(DTD_PUBLIC_ID_1_3, DTD_RESOURCE_NAME_1_3); 137 return map; 138 } 139 140 /** 141 * Parses the specified input source loading the configuration information. 142 * The stream wrapped inside the source, if any, is NOT 143 * explicitly closed after parsing, it is the responsibility of 144 * the caller to close the stream. 145 * 146 * @param source the source that contains the configuration data 147 * @throws IOException if an error occurs 148 * @throws SAXException if an error occurs 149 */ 150 private void parseInputSource(InputSource source) 151 throws IOException, SAXException { 152 saxHandler.parseInputSource(source); 153 } 154 155 /** 156 * Returns the module configurations in a specified file. 157 * @param config location of config file, can be either a URL or a filename 158 * @param overridePropsResolver overriding properties 159 * @return the check configurations 160 * @throws CheckstyleException if an error occurs 161 */ 162 public static Configuration loadConfiguration(String config, 163 PropertyResolver overridePropsResolver) throws CheckstyleException { 164 return loadConfiguration(config, overridePropsResolver, false); 165 } 166 167 /** 168 * Returns the module configurations in a specified file. 169 * 170 * @param config location of config file, can be either a URL or a filename 171 * @param overridePropsResolver overriding properties 172 * @param omitIgnoredModules {@code true} if modules with severity 173 * 'ignore' should be omitted, {@code false} otherwise 174 * @return the check configurations 175 * @throws CheckstyleException if an error occurs 176 */ 177 public static Configuration loadConfiguration(String config, 178 PropertyResolver overridePropsResolver, boolean omitIgnoredModules) 179 throws CheckstyleException { 180 // figure out if this is a File or a URL 181 final URI uri = CommonUtils.getUriByFilename(config); 182 final InputSource source = new InputSource(uri.toString()); 183 return loadConfiguration(source, overridePropsResolver, 184 omitIgnoredModules); 185 } 186 187 /** 188 * Returns the module configurations from a specified input stream. 189 * Note that clients are required to close the given stream by themselves 190 * 191 * @param configStream the input stream to the Checkstyle configuration 192 * @param overridePropsResolver overriding properties 193 * @param omitIgnoredModules {@code true} if modules with severity 194 * 'ignore' should be omitted, {@code false} otherwise 195 * @return the check configurations 196 * @throws CheckstyleException if an error occurs 197 * 198 * @deprecated As this method does not provide a valid system ID, 199 * preventing resolution of external entities, a 200 * {@link #loadConfiguration(InputSource,PropertyResolver,boolean) 201 * version using an InputSource} 202 * should be used instead 203 */ 204 @Deprecated 205 public static Configuration loadConfiguration(InputStream configStream, 206 PropertyResolver overridePropsResolver, boolean omitIgnoredModules) 207 throws CheckstyleException { 208 return loadConfiguration(new InputSource(configStream), 209 overridePropsResolver, omitIgnoredModules); 210 } 211 212 /** 213 * Returns the module configurations from a specified input source. 214 * Note that if the source does wrap an open byte or character 215 * stream, clients are required to close that stream by themselves 216 * 217 * @param configSource the input stream to the Checkstyle configuration 218 * @param overridePropsResolver overriding properties 219 * @param omitIgnoredModules {@code true} if modules with severity 220 * 'ignore' should be omitted, {@code false} otherwise 221 * @return the check configurations 222 * @throws CheckstyleException if an error occurs 223 */ 224 public static Configuration loadConfiguration(InputSource configSource, 225 PropertyResolver overridePropsResolver, boolean omitIgnoredModules) 226 throws CheckstyleException { 227 try { 228 final ConfigurationLoader loader = 229 new ConfigurationLoader(overridePropsResolver, 230 omitIgnoredModules); 231 loader.parseInputSource(configSource); 232 return loader.configuration; 233 } 234 catch (final SAXParseException ex) { 235 final String message = String.format(Locale.ROOT, "%s - %s:%s:%s", 236 UNABLE_TO_PARSE_EXCEPTION_PREFIX, 237 ex.getMessage(), ex.getLineNumber(), ex.getColumnNumber()); 238 throw new CheckstyleException(message, ex); 239 } 240 catch (final ParserConfigurationException | IOException | SAXException ex) { 241 throw new CheckstyleException(UNABLE_TO_PARSE_EXCEPTION_PREFIX, ex); 242 } 243 } 244 245 /** 246 * Replaces {@code ${xxx}} style constructions in the given value 247 * with the string value of the corresponding data types. 248 * 249 * <p>Code copied from ant - 250 * http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java 251 * 252 * @param value The string to be scanned for property references. 253 * May be {@code null}, in which case this 254 * method returns immediately with no effect. 255 * @param props Mapping (String to String) of property names to their 256 * values. Must not be {@code null}. 257 * @param defaultValue default to use if one of the properties in value 258 * cannot be resolved from props. 259 * 260 * @return the original string with the properties replaced, or 261 * {@code null} if the original string is {@code null}. 262 * @throws CheckstyleException if the string contains an opening 263 * {@code ${} without a closing 264 * {@code }} 265 */ 266 private static String replaceProperties( 267 String value, PropertyResolver props, String defaultValue) 268 throws CheckstyleException { 269 if (value == null) { 270 return null; 271 } 272 273 final List<String> fragments = Lists.newArrayList(); 274 final List<String> propertyRefs = Lists.newArrayList(); 275 parsePropertyString(value, fragments, propertyRefs); 276 277 final StringBuilder sb = new StringBuilder(); 278 final Iterator<String> fragmentsIterator = fragments.iterator(); 279 final Iterator<String> propertyRefsIterator = propertyRefs.iterator(); 280 while (fragmentsIterator.hasNext()) { 281 String fragment = fragmentsIterator.next(); 282 if (fragment == null) { 283 final String propertyName = propertyRefsIterator.next(); 284 fragment = props.resolve(propertyName); 285 if (fragment == null) { 286 if (defaultValue != null) { 287 return defaultValue; 288 } 289 throw new CheckstyleException( 290 "Property ${" + propertyName + "} has not been set"); 291 } 292 } 293 sb.append(fragment); 294 } 295 296 return sb.toString(); 297 } 298 299 /** 300 * Parses a string containing {@code ${xxx}} style property 301 * references into two lists. The first list is a collection 302 * of text fragments, while the other is a set of string property names. 303 * {@code null} entries in the first list indicate a property 304 * reference from the second list. 305 * 306 * <p>Code copied from ant - 307 * http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java 308 * 309 * @param value Text to parse. Must not be {@code null}. 310 * @param fragments List to add text fragments to. 311 * Must not be {@code null}. 312 * @param propertyRefs List to add property names to. 313 * Must not be {@code null}. 314 * 315 * @throws CheckstyleException if the string contains an opening 316 * {@code ${} without a closing 317 * {@code }} 318 */ 319 private static void parsePropertyString(String value, 320 List<String> fragments, 321 List<String> propertyRefs) 322 throws CheckstyleException { 323 int prev = 0; 324 //search for the next instance of $ from the 'prev' position 325 int pos = value.indexOf(DOLLAR_SIGN, prev); 326 while (pos >= 0) { 327 328 //if there was any text before this, add it as a fragment 329 if (pos > 0) { 330 fragments.add(value.substring(prev, pos)); 331 } 332 //if we are at the end of the string, we tack on a $ 333 //then move past it 334 if (pos == value.length() - 1) { 335 fragments.add(String.valueOf(DOLLAR_SIGN)); 336 prev = pos + 1; 337 } 338 else if (value.charAt(pos + 1) == '{') { 339 //property found, extract its name or bail on a typo 340 final int endName = value.indexOf('}', pos); 341 if (endName < 0) { 342 throw new CheckstyleException("Syntax error in property: " 343 + value); 344 } 345 final String propertyName = value.substring(pos + 2, endName); 346 fragments.add(null); 347 propertyRefs.add(propertyName); 348 prev = endName + 1; 349 } 350 else { 351 if (value.charAt(pos + 1) == DOLLAR_SIGN) { 352 //backwards compatibility two $ map to one mode 353 fragments.add(String.valueOf(DOLLAR_SIGN)); 354 prev = pos + 2; 355 } 356 else { 357 //new behaviour: $X maps to $X for all values of X!='$' 358 fragments.add(value.substring(pos, pos + 2)); 359 prev = pos + 2; 360 } 361 } 362 363 //search for the next instance of $ from the 'prev' position 364 pos = value.indexOf(DOLLAR_SIGN, prev); 365 } 366 //no more $ signs found 367 //if there is any tail to the file, append it 368 if (prev < value.length()) { 369 fragments.add(value.substring(prev)); 370 } 371 } 372 373 /** 374 * Implements the SAX document handler interfaces, so they do not 375 * appear in the public API of the ConfigurationLoader. 376 */ 377 private final class InternalLoader 378 extends AbstractLoader { 379 /** Module elements. */ 380 private static final String MODULE = "module"; 381 /** Name attribute. */ 382 private static final String NAME = "name"; 383 /** Property element. */ 384 private static final String PROPERTY = "property"; 385 /** Value attribute. */ 386 private static final String VALUE = "value"; 387 /** Default attribute. */ 388 private static final String DEFAULT = "default"; 389 /** Name of the severity property. */ 390 private static final String SEVERITY = "severity"; 391 /** Name of the message element. */ 392 private static final String MESSAGE = "message"; 393 /** Name of the message element. */ 394 private static final String METADATA = "metadata"; 395 /** Name of the key attribute. */ 396 private static final String KEY = "key"; 397 398 /** 399 * Creates a new InternalLoader. 400 * @throws SAXException if an error occurs 401 * @throws ParserConfigurationException if an error occurs 402 */ 403 InternalLoader() 404 throws SAXException, ParserConfigurationException { 405 super(createIdToResourceNameMap()); 406 } 407 408 @Override 409 public void startElement(String uri, 410 String localName, 411 String qName, 412 Attributes attributes) 413 throws SAXException { 414 if (qName.equals(MODULE)) { 415 //create configuration 416 final String name = attributes.getValue(NAME); 417 final DefaultConfiguration conf = 418 new DefaultConfiguration(name); 419 420 if (configuration == null) { 421 configuration = conf; 422 } 423 424 //add configuration to it's parent 425 if (!configStack.isEmpty()) { 426 final DefaultConfiguration top = 427 configStack.peek(); 428 top.addChild(conf); 429 } 430 431 configStack.push(conf); 432 } 433 else if (qName.equals(PROPERTY)) { 434 //extract value and name 435 final String value; 436 try { 437 value = replaceProperties(attributes.getValue(VALUE), 438 overridePropsResolver, attributes.getValue(DEFAULT)); 439 } 440 catch (final CheckstyleException ex) { 441 throw new SAXException(ex); 442 } 443 final String name = attributes.getValue(NAME); 444 445 //add to attributes of configuration 446 final DefaultConfiguration top = 447 configStack.peek(); 448 top.addAttribute(name, value); 449 } 450 else if (qName.equals(MESSAGE)) { 451 //extract key and value 452 final String key = attributes.getValue(KEY); 453 final String value = attributes.getValue(VALUE); 454 455 //add to messages of configuration 456 final DefaultConfiguration top = configStack.peek(); 457 top.addMessage(key, value); 458 } 459 else { 460 if (!qName.equals(METADATA)) { 461 throw new IllegalStateException("Unknown name:" + qName + "."); 462 } 463 } 464 } 465 466 @Override 467 public void endElement(String uri, 468 String localName, 469 String qName) { 470 if (qName.equals(MODULE)) { 471 472 final Configuration recentModule = 473 configStack.pop(); 474 475 // remove modules with severity ignore if these modules should 476 // be omitted 477 SeverityLevel level = null; 478 try { 479 final String severity = recentModule.getAttribute(SEVERITY); 480 level = SeverityLevel.getInstance(severity); 481 } 482 catch (final CheckstyleException ex) { 483 LOG.debug("Severity not set, ignoring exception", ex); 484 } 485 486 // omit this module if these should be omitted and the module 487 // has the severity 'ignore' 488 final boolean omitModule = omitIgnoredModules 489 && level == SeverityLevel.IGNORE; 490 491 if (omitModule && !configStack.isEmpty()) { 492 final DefaultConfiguration parentModule = 493 configStack.peek(); 494 parentModule.removeChild(recentModule); 495 } 496 } 497 } 498 } 499}