001//////////////////////////////////////////////////////////////////////////////// 002// checkstyle: Checks Java source code for adherence to a set of rules. 003// Copyright (C) 2001-2015 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.annotations.VisibleForTesting; 042import com.google.common.collect.Lists; 043import com.google.common.collect.Maps; 044import com.puppycrawl.tools.checkstyle.api.AbstractLoader; 045import com.puppycrawl.tools.checkstyle.api.CheckstyleException; 046import com.puppycrawl.tools.checkstyle.api.Configuration; 047import com.puppycrawl.tools.checkstyle.api.SeverityLevel; 048import com.puppycrawl.tools.checkstyle.utils.CommonUtils; 049 050/** 051 * Loads a configuration from a standard configuration XML file. 052 * 053 * @author Oliver Burn 054 */ 055public final class ConfigurationLoader { 056 /** Logger for ConfigurationLoader. */ 057 private static final Log LOG = LogFactory.getLog(ConfigurationLoader.class); 058 059 /** The public ID for version 1_0 of the configuration dtd. */ 060 private static final String DTD_PUBLIC_ID_1_0 = 061 "-//Puppy Crawl//DTD Check Configuration 1.0//EN"; 062 063 /** The resource for version 1_0 of the configuration dtd. */ 064 private static final String DTD_RESOURCE_NAME_1_0 = 065 "com/puppycrawl/tools/checkstyle/configuration_1_0.dtd"; 066 067 /** The public ID for version 1_1 of the configuration dtd. */ 068 private static final String DTD_PUBLIC_ID_1_1 = 069 "-//Puppy Crawl//DTD Check Configuration 1.1//EN"; 070 071 /** The resource for version 1_1 of the configuration dtd. */ 072 private static final String DTD_RESOURCE_NAME_1_1 = 073 "com/puppycrawl/tools/checkstyle/configuration_1_1.dtd"; 074 075 /** The public ID for version 1_2 of the configuration dtd. */ 076 private static final String DTD_PUBLIC_ID_1_2 = 077 "-//Puppy Crawl//DTD Check Configuration 1.2//EN"; 078 079 /** The resource for version 1_2 of the configuration dtd. */ 080 private static final String DTD_RESOURCE_NAME_1_2 = 081 "com/puppycrawl/tools/checkstyle/configuration_1_2.dtd"; 082 083 /** The public ID for version 1_3 of the configuration dtd. */ 084 private static final String DTD_PUBLIC_ID_1_3 = 085 "-//Puppy Crawl//DTD Check Configuration 1.3//EN"; 086 087 /** The resource for version 1_3 of the configuration dtd. */ 088 private static final String DTD_RESOURCE_NAME_1_3 = 089 "com/puppycrawl/tools/checkstyle/configuration_1_3.dtd"; 090 091 /** Prefix for the exception when unable to parse resource. */ 092 private static final String UNABLE_TO_PARSE_EXCEPTION_PREFIX = "unable to parse" 093 + " configuration stream"; 094 095 /** Dollar sign literal. */ 096 private static final char DOLLAR_SIGN = '$'; 097 098 /** The SAX document handler. */ 099 private final InternalLoader saxHandler; 100 101 /** Property resolver. **/ 102 private final PropertyResolver overridePropsResolver; 103 /** The loaded configurations. **/ 104 private final Deque<DefaultConfiguration> configStack = new ArrayDeque<>(); 105 /** The Configuration that is being built. */ 106 private Configuration configuration; 107 108 /** Flags if modules with the severity 'ignore' should be omitted. */ 109 private final boolean omitIgnoredModules; 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 e) { 235 final String message = String.format(Locale.ROOT, "%s - %s:%s:%s", 236 UNABLE_TO_PARSE_EXCEPTION_PREFIX, 237 e.getMessage(), e.getLineNumber(), e.getColumnNumber()); 238 throw new CheckstyleException(message, e); 239 } 240 catch (final ParserConfigurationException | IOException | SAXException e) { 241 throw new CheckstyleException(UNABLE_TO_PARSE_EXCEPTION_PREFIX, e); 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>The method is package visible to facilitate testing. 250 * 251 * <p>Code copied from ant - 252 * http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java 253 * 254 * @param value The string to be scanned for property references. 255 * May be {@code null}, in which case this 256 * method returns immediately with no effect. 257 * @param props Mapping (String to String) of property names to their 258 * values. Must not be {@code null}. 259 * @param defaultValue default to use if one of the properties in value 260 * cannot be resolved from props. 261 * 262 * @return the original string with the properties replaced, or 263 * {@code null} if the original string is {@code null}. 264 * @throws CheckstyleException if the string contains an opening 265 * {@code ${} without a closing 266 * {@code }} 267 */ 268 @VisibleForTesting 269 static String replaceProperties( 270 String value, PropertyResolver props, String defaultValue) 271 throws CheckstyleException { 272 if (value == null) { 273 return null; 274 } 275 276 final List<String> fragments = Lists.newArrayList(); 277 final List<String> propertyRefs = Lists.newArrayList(); 278 parsePropertyString(value, fragments, propertyRefs); 279 280 final StringBuilder sb = new StringBuilder(); 281 final Iterator<String> fragmentsIterator = fragments.iterator(); 282 final Iterator<String> propertyRefsIterator = propertyRefs.iterator(); 283 while (fragmentsIterator.hasNext()) { 284 String fragment = fragmentsIterator.next(); 285 if (fragment == null) { 286 final String propertyName = propertyRefsIterator.next(); 287 fragment = props.resolve(propertyName); 288 if (fragment == null) { 289 if (defaultValue != null) { 290 return defaultValue; 291 } 292 throw new CheckstyleException( 293 "Property ${" + propertyName + "} has not been set"); 294 } 295 } 296 sb.append(fragment); 297 } 298 299 return sb.toString(); 300 } 301 302 /** 303 * Parses a string containing {@code ${xxx}} style property 304 * references into two lists. The first list is a collection 305 * of text fragments, while the other is a set of string property names. 306 * {@code null} entries in the first list indicate a property 307 * reference from the second list. 308 * 309 * <p>Code copied from ant - 310 * http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java 311 * 312 * @param value Text to parse. Must not be {@code null}. 313 * @param fragments List to add text fragments to. 314 * Must not be {@code null}. 315 * @param propertyRefs List to add property names to. 316 * Must not be {@code null}. 317 * 318 * @throws CheckstyleException if the string contains an opening 319 * {@code ${} without a closing 320 * {@code }} 321 */ 322 private static void parsePropertyString(String value, 323 List<String> fragments, 324 List<String> propertyRefs) 325 throws CheckstyleException { 326 int prev = 0; 327 //search for the next instance of $ from the 'prev' position 328 int pos = value.indexOf(DOLLAR_SIGN, prev); 329 while (pos >= 0) { 330 331 //if there was any text before this, add it as a fragment 332 if (pos > 0) { 333 fragments.add(value.substring(prev, pos)); 334 } 335 //if we are at the end of the string, we tack on a $ 336 //then move past it 337 if (pos == value.length() - 1) { 338 fragments.add(String.valueOf(DOLLAR_SIGN)); 339 prev = pos + 1; 340 } 341 else if (value.charAt(pos + 1) == '{') { 342 //property found, extract its name or bail on a typo 343 final int endName = value.indexOf('}', pos); 344 if (endName < 0) { 345 throw new CheckstyleException("Syntax error in property: " 346 + value); 347 } 348 final String propertyName = value.substring(pos + 2, endName); 349 fragments.add(null); 350 propertyRefs.add(propertyName); 351 prev = endName + 1; 352 } 353 else { 354 if (value.charAt(pos + 1) == DOLLAR_SIGN) { 355 //backwards compatibility two $ map to one mode 356 fragments.add(String.valueOf(DOLLAR_SIGN)); 357 prev = pos + 2; 358 } 359 else { 360 //new behaviour: $X maps to $X for all values of X!='$' 361 fragments.add(value.substring(pos, pos + 2)); 362 prev = pos + 2; 363 } 364 } 365 366 //search for the next instance of $ from the 'prev' position 367 pos = value.indexOf(DOLLAR_SIGN, prev); 368 } 369 //no more $ signs found 370 //if there is any tail to the file, append it 371 if (prev < value.length()) { 372 fragments.add(value.substring(prev)); 373 } 374 } 375 376 /** 377 * Implements the SAX document handler interfaces, so they do not 378 * appear in the public API of the ConfigurationLoader. 379 */ 380 private final class InternalLoader 381 extends AbstractLoader { 382 /** Module elements. */ 383 private static final String MODULE = "module"; 384 /** Name attribute. */ 385 private static final String NAME = "name"; 386 /** Property element. */ 387 private static final String PROPERTY = "property"; 388 /** Value attribute. */ 389 private static final String VALUE = "value"; 390 /** Default attribute. */ 391 private static final String DEFAULT = "default"; 392 /** Name of the severity property. */ 393 private static final String SEVERITY = "severity"; 394 /** Name of the message element. */ 395 private static final String MESSAGE = "message"; 396 /** Name of the message element. */ 397 private static final String METADATA = "metadata"; 398 /** Name of the key attribute. */ 399 private static final String KEY = "key"; 400 401 /** 402 * Creates a new InternalLoader. 403 * @throws SAXException if an error occurs 404 * @throws ParserConfigurationException if an error occurs 405 */ 406 InternalLoader() 407 throws SAXException, ParserConfigurationException { 408 super(createIdToResourceNameMap()); 409 } 410 411 @Override 412 public void startElement(String uri, 413 String localName, 414 String qName, 415 Attributes attributes) 416 throws SAXException { 417 if (qName.equals(MODULE)) { 418 //create configuration 419 final String name = attributes.getValue(NAME); 420 final DefaultConfiguration conf = 421 new DefaultConfiguration(name); 422 423 if (configuration == null) { 424 configuration = conf; 425 } 426 427 //add configuration to it's parent 428 if (!configStack.isEmpty()) { 429 final DefaultConfiguration top = 430 configStack.peek(); 431 top.addChild(conf); 432 } 433 434 configStack.push(conf); 435 } 436 else if (qName.equals(PROPERTY)) { 437 //extract value and name 438 final String value; 439 try { 440 value = replaceProperties(attributes.getValue(VALUE), 441 overridePropsResolver, attributes.getValue(DEFAULT)); 442 } 443 catch (final CheckstyleException ex) { 444 throw new SAXException(ex); 445 } 446 final String name = attributes.getValue(NAME); 447 448 //add to attributes of configuration 449 final DefaultConfiguration top = 450 configStack.peek(); 451 top.addAttribute(name, value); 452 } 453 else if (qName.equals(MESSAGE)) { 454 //extract key and value 455 final String key = attributes.getValue(KEY); 456 final String value = attributes.getValue(VALUE); 457 458 //add to messages of configuration 459 final DefaultConfiguration top = configStack.peek(); 460 top.addMessage(key, value); 461 } 462 else { 463 if (!qName.equals(METADATA)) { 464 throw new IllegalStateException("Unknown name:" + qName + "."); 465 } 466 } 467 } 468 469 @Override 470 public void endElement(String uri, 471 String localName, 472 String qName) { 473 if (qName.equals(MODULE)) { 474 475 final Configuration recentModule = 476 configStack.pop(); 477 478 // remove modules with severity ignore if these modules should 479 // be omitted 480 SeverityLevel level = null; 481 try { 482 final String severity = recentModule.getAttribute(SEVERITY); 483 level = SeverityLevel.getInstance(severity); 484 } 485 catch (final CheckstyleException e) { 486 LOG.debug("Severity not set, ignoring exception", e); 487 } 488 489 // omit this module if these should be omitted and the module 490 // has the severity 'ignore' 491 final boolean omitModule = omitIgnoredModules 492 && level == SeverityLevel.IGNORE; 493 494 if (omitModule && !configStack.isEmpty()) { 495 final DefaultConfiguration parentModule = 496 configStack.peek(); 497 parentModule.removeChild(recentModule); 498 } 499 } 500 } 501 } 502}