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