blob: d77e3d6202fdaa9139a755a4e014473a6d125fea [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2017 the original author or authors.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
////////////////////////////////////////////////////////////////////////////////
package com.puppycrawl.tools.checkstyle;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import com.puppycrawl.tools.checkstyle.api.AbstractLoader;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.Configuration;
import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
import com.puppycrawl.tools.checkstyle.utils.CommonUtils;
/**
* Loads a configuration from a standard configuration XML file.
*
* @author Oliver Burn
*/
public final class ConfigurationLoader {
/**
* Enum to specify behaviour regarding ignored modules.
*/
public enum IgnoredModulesOptions {
/**
* Omit ignored modules.
*/
OMIT,
/**
* Execute ignored modules.
*/
EXECUTE
}
/** Format of message for sax parse exception. */
private static final String SAX_PARSE_EXCEPTION_FORMAT = "%s - %s:%s:%s";
/** The public ID for version 1_0 of the configuration dtd. */
private static final String DTD_PUBLIC_ID_1_0 =
"-//Puppy Crawl//DTD Check Configuration 1.0//EN";
/** The resource for version 1_0 of the configuration dtd. */
private static final String DTD_CONFIGURATION_NAME_1_0 =
"com/puppycrawl/tools/checkstyle/configuration_1_0.dtd";
/** The public ID for version 1_1 of the configuration dtd. */
private static final String DTD_PUBLIC_ID_1_1 =
"-//Puppy Crawl//DTD Check Configuration 1.1//EN";
/** The resource for version 1_1 of the configuration dtd. */
private static final String DTD_CONFIGURATION_NAME_1_1 =
"com/puppycrawl/tools/checkstyle/configuration_1_1.dtd";
/** The public ID for version 1_2 of the configuration dtd. */
private static final String DTD_PUBLIC_ID_1_2 =
"-//Puppy Crawl//DTD Check Configuration 1.2//EN";
/** The resource for version 1_2 of the configuration dtd. */
private static final String DTD_CONFIGURATION_NAME_1_2 =
"com/puppycrawl/tools/checkstyle/configuration_1_2.dtd";
/** The public ID for version 1_3 of the configuration dtd. */
private static final String DTD_PUBLIC_ID_1_3 =
"-//Puppy Crawl//DTD Check Configuration 1.3//EN";
/** The resource for version 1_3 of the configuration dtd. */
private static final String DTD_CONFIGURATION_NAME_1_3 =
"com/puppycrawl/tools/checkstyle/configuration_1_3.dtd";
/** Prefix for the exception when unable to parse resource. */
private static final String UNABLE_TO_PARSE_EXCEPTION_PREFIX = "unable to parse"
+ " configuration stream";
/** Dollar sign literal. */
private static final char DOLLAR_SIGN = '$';
/** The SAX document handler. */
private final InternalLoader saxHandler;
/** Property resolver. **/
private final PropertyResolver overridePropsResolver;
/** The loaded configurations. **/
private final Deque<DefaultConfiguration> configStack = new ArrayDeque<>();
/** Flags if modules with the severity 'ignore' should be omitted. */
private final boolean omitIgnoredModules;
/** The thread mode configuration. */
private final ThreadModeSettings threadModeSettings;
/** The Configuration that is being built. */
private Configuration configuration;
/**
* Creates a new {@code ConfigurationLoader} instance.
* @param overrideProps resolver for overriding properties
* @param omitIgnoredModules {@code true} if ignored modules should be
* omitted
* @throws ParserConfigurationException if an error occurs
* @throws SAXException if an error occurs
*/
private ConfigurationLoader(final PropertyResolver overrideProps,
final boolean omitIgnoredModules)
throws ParserConfigurationException, SAXException {
this(overrideProps, omitIgnoredModules, ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE);
}
/**
* Creates a new {@code ConfigurationLoader} instance.
* @param overrideProps resolver for overriding properties
* @param omitIgnoredModules {@code true} if ignored modules should be
* omitted
* @param threadModeSettings the thread mode configuration
* @throws ParserConfigurationException if an error occurs
* @throws SAXException if an error occurs
*/
private ConfigurationLoader(final PropertyResolver overrideProps,
final boolean omitIgnoredModules,
final ThreadModeSettings threadModeSettings)
throws ParserConfigurationException, SAXException {
saxHandler = new InternalLoader();
overridePropsResolver = overrideProps;
this.omitIgnoredModules = omitIgnoredModules;
this.threadModeSettings = threadModeSettings;
}
/**
* Creates mapping between local resources and dtd ids.
* @return map between local resources and dtd ids.
*/
private static Map<String, String> createIdToResourceNameMap() {
final Map<String, String> map = new HashMap<>();
map.put(DTD_PUBLIC_ID_1_0, DTD_CONFIGURATION_NAME_1_0);
map.put(DTD_PUBLIC_ID_1_1, DTD_CONFIGURATION_NAME_1_1);
map.put(DTD_PUBLIC_ID_1_2, DTD_CONFIGURATION_NAME_1_2);
map.put(DTD_PUBLIC_ID_1_3, DTD_CONFIGURATION_NAME_1_3);
return map;
}
/**
* Parses the specified input source loading the configuration information.
* The stream wrapped inside the source, if any, is NOT
* explicitly closed after parsing, it is the responsibility of
* the caller to close the stream.
*
* @param source the source that contains the configuration data
* @throws IOException if an error occurs
* @throws SAXException if an error occurs
*/
private void parseInputSource(InputSource source)
throws IOException, SAXException {
saxHandler.parseInputSource(source);
}
/**
* Returns the module configurations in a specified file.
* @param config location of config file, can be either a URL or a filename
* @param overridePropsResolver overriding properties
* @return the check configurations
* @throws CheckstyleException if an error occurs
*/
public static Configuration loadConfiguration(String config,
PropertyResolver overridePropsResolver) throws CheckstyleException {
return loadConfiguration(config, overridePropsResolver, IgnoredModulesOptions.EXECUTE);
}
/**
* Returns the module configurations in a specified file.
* @param config location of config file, can be either a URL or a filename
* @param overridePropsResolver overriding properties
* @param threadModeSettings the thread mode configuration
* @return the check configurations
* @throws CheckstyleException if an error occurs
*/
public static Configuration loadConfiguration(String config,
PropertyResolver overridePropsResolver, ThreadModeSettings threadModeSettings)
throws CheckstyleException {
return loadConfiguration(config, overridePropsResolver,
IgnoredModulesOptions.EXECUTE, threadModeSettings);
}
/**
* Returns the module configurations in a specified file.
*
* @param config location of config file, can be either a URL or a filename
* @param overridePropsResolver overriding properties
* @param omitIgnoredModules {@code true} if modules with severity
* 'ignore' should be omitted, {@code false} otherwise
* @return the check configurations
* @throws CheckstyleException if an error occurs
* @deprecated in order to fullfil demands of BooleanParameter IDEA check.
* @noinspection BooleanParameter
*/
@Deprecated
public static Configuration loadConfiguration(String config,
PropertyResolver overridePropsResolver, boolean omitIgnoredModules)
throws CheckstyleException {
return loadConfiguration(config, overridePropsResolver, omitIgnoredModules,
ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE);
}
/**
* Returns the module configurations in a specified file.
*
* @param config location of config file, can be either a URL or a filename
* @param overridePropsResolver overriding properties
* @param omitIgnoredModules {@code true} if modules with severity
* 'ignore' should be omitted, {@code false} otherwise
* @param threadModeSettings the thread mode configuration
* @return the check configurations
* @throws CheckstyleException if an error occurs
* @deprecated in order to fullfil demands of BooleanParameter IDEA check.
* @noinspection BooleanParameter, WeakerAccess
*/
@Deprecated
public static Configuration loadConfiguration(String config,
PropertyResolver overridePropsResolver,
boolean omitIgnoredModules, ThreadModeSettings threadModeSettings)
throws CheckstyleException {
// figure out if this is a File or a URL
final URI uri = CommonUtils.getUriByFilename(config);
final InputSource source = new InputSource(uri.toString());
return loadConfiguration(source, overridePropsResolver,
omitIgnoredModules, threadModeSettings);
}
/**
* Returns the module configurations from a specified input stream.
* Note that clients are required to close the given stream by themselves
*
* @param configStream the input stream to the Checkstyle configuration
* @param overridePropsResolver overriding properties
* @param omitIgnoredModules {@code true} if modules with severity
* 'ignore' should be omitted, {@code false} otherwise
* @return the check configurations
* @throws CheckstyleException if an error occurs
*
* @deprecated As this method does not provide a valid system ID,
* preventing resolution of external entities, a
* {@link #loadConfiguration(InputSource,PropertyResolver,boolean)
* version using an InputSource}
* should be used instead
* @noinspection BooleanParameter
*/
@Deprecated
public static Configuration loadConfiguration(InputStream configStream,
PropertyResolver overridePropsResolver, boolean omitIgnoredModules)
throws CheckstyleException {
return loadConfiguration(new InputSource(configStream),
overridePropsResolver, omitIgnoredModules);
}
/**
* Returns the module configurations from a specified input source.
* Note that if the source does wrap an open byte or character
* stream, clients are required to close that stream by themselves
*
* @param configSource the input stream to the Checkstyle configuration
* @param overridePropsResolver overriding properties
* @param omitIgnoredModules {@code true} if modules with severity
* 'ignore' should be omitted, {@code false} otherwise
* @return the check configurations
* @throws CheckstyleException if an error occurs
* @deprecated in order to fullfil demands of BooleanParameter IDEA check.
* @noinspection BooleanParameter
*/
@Deprecated
public static Configuration loadConfiguration(InputSource configSource,
PropertyResolver overridePropsResolver, boolean omitIgnoredModules)
throws CheckstyleException {
return loadConfiguration(configSource, overridePropsResolver,
omitIgnoredModules, ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE);
}
/**
* Returns the module configurations from a specified input source.
* Note that if the source does wrap an open byte or character
* stream, clients are required to close that stream by themselves
*
* @param configSource the input stream to the Checkstyle configuration
* @param overridePropsResolver overriding properties
* @param omitIgnoredModules {@code true} if modules with severity
* 'ignore' should be omitted, {@code false} otherwise
* @param threadModeSettings the thread mode configuration
* @return the check configurations
* @throws CheckstyleException if an error occurs
* @deprecated in order to fullfil demands of BooleanParameter IDEA check.
* @noinspection BooleanParameter, WeakerAccess
*/
@Deprecated
public static Configuration loadConfiguration(InputSource configSource,
PropertyResolver overridePropsResolver,
boolean omitIgnoredModules, ThreadModeSettings threadModeSettings)
throws CheckstyleException {
try {
final ConfigurationLoader loader =
new ConfigurationLoader(overridePropsResolver,
omitIgnoredModules, threadModeSettings);
loader.parseInputSource(configSource);
return loader.configuration;
}
catch (final SAXParseException ex) {
final String message = String.format(Locale.ROOT, SAX_PARSE_EXCEPTION_FORMAT,
UNABLE_TO_PARSE_EXCEPTION_PREFIX,
ex.getMessage(), ex.getLineNumber(), ex.getColumnNumber());
throw new CheckstyleException(message, ex);
}
catch (final ParserConfigurationException | IOException | SAXException ex) {
throw new CheckstyleException(UNABLE_TO_PARSE_EXCEPTION_PREFIX, ex);
}
}
/**
* Returns the module configurations in a specified file.
*
* @param config location of config file, can be either a URL or a filename
* @param overridePropsResolver overriding properties
* @param ignoredModulesOptions {@code OMIT} if modules with severity
* 'ignore' should be omitted, {@code EXECUTE} otherwise
* @return the check configurations
* @throws CheckstyleException if an error occurs
*/
public static Configuration loadConfiguration(String config,
PropertyResolver overridePropsResolver,
IgnoredModulesOptions ignoredModulesOptions)
throws CheckstyleException {
return loadConfiguration(config, overridePropsResolver, ignoredModulesOptions,
ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE);
}
/**
* Returns the module configurations in a specified file.
*
* @param config location of config file, can be either a URL or a filename
* @param overridePropsResolver overriding properties
* @param ignoredModulesOptions {@code OMIT} if modules with severity
* 'ignore' should be omitted, {@code EXECUTE} otherwise
* @param threadModeSettings the thread mode configuration
* @return the check configurations
* @throws CheckstyleException if an error occurs
*/
public static Configuration loadConfiguration(String config,
PropertyResolver overridePropsResolver,
IgnoredModulesOptions ignoredModulesOptions,
ThreadModeSettings threadModeSettings)
throws CheckstyleException {
// figure out if this is a File or a URL
final URI uri = CommonUtils.getUriByFilename(config);
final InputSource source = new InputSource(uri.toString());
return loadConfiguration(source, overridePropsResolver,
ignoredModulesOptions, threadModeSettings);
}
/**
* Returns the module configurations from a specified input source.
* Note that if the source does wrap an open byte or character
* stream, clients are required to close that stream by themselves
*
* @param configSource the input stream to the Checkstyle configuration
* @param overridePropsResolver overriding properties
* @param ignoredModulesOptions {@code OMIT} if modules with severity
* 'ignore' should be omitted, {@code EXECUTE} otherwise
* @return the check configurations
* @throws CheckstyleException if an error occurs
*/
public static Configuration loadConfiguration(InputSource configSource,
PropertyResolver overridePropsResolver,
IgnoredModulesOptions ignoredModulesOptions)
throws CheckstyleException {
return loadConfiguration(configSource, overridePropsResolver,
ignoredModulesOptions, ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE);
}
/**
* Returns the module configurations from a specified input source.
* Note that if the source does wrap an open byte or character
* stream, clients are required to close that stream by themselves
*
* @param configSource the input stream to the Checkstyle configuration
* @param overridePropsResolver overriding properties
* @param ignoredModulesOptions {@code OMIT} if modules with severity
* 'ignore' should be omitted, {@code EXECUTE} otherwise
* @param threadModeSettings the thread mode configuration
* @return the check configurations
* @throws CheckstyleException if an error occurs
* @noinspection WeakerAccess
*/
public static Configuration loadConfiguration(InputSource configSource,
PropertyResolver overridePropsResolver,
IgnoredModulesOptions ignoredModulesOptions,
ThreadModeSettings threadModeSettings)
throws CheckstyleException {
try {
final boolean omitIgnoreModules = ignoredModulesOptions == IgnoredModulesOptions.OMIT;
final ConfigurationLoader loader =
new ConfigurationLoader(overridePropsResolver,
omitIgnoreModules, threadModeSettings);
loader.parseInputSource(configSource);
return loader.configuration;
}
catch (final SAXParseException ex) {
final String message = String.format(Locale.ROOT, SAX_PARSE_EXCEPTION_FORMAT,
UNABLE_TO_PARSE_EXCEPTION_PREFIX,
ex.getMessage(), ex.getLineNumber(), ex.getColumnNumber());
throw new CheckstyleException(message, ex);
}
catch (final ParserConfigurationException | IOException | SAXException ex) {
throw new CheckstyleException(UNABLE_TO_PARSE_EXCEPTION_PREFIX, ex);
}
}
/**
* Replaces {@code ${xxx}} style constructions in the given value
* with the string value of the corresponding data types.
*
* <p>Code copied from ant -
* http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java
*
* @param value The string to be scanned for property references.
* May be {@code null}, in which case this
* method returns immediately with no effect.
* @param props Mapping (String to String) of property names to their
* values. Must not be {@code null}.
* @param defaultValue default to use if one of the properties in value
* cannot be resolved from props.
*
* @return the original string with the properties replaced, or
* {@code null} if the original string is {@code null}.
* @throws CheckstyleException if the string contains an opening
* {@code ${} without a closing
* {@code }}
* @noinspection MethodWithMultipleReturnPoints
*/
private static String replaceProperties(
String value, PropertyResolver props, String defaultValue)
throws CheckstyleException {
if (value == null) {
return null;
}
final List<String> fragments = new ArrayList<>();
final List<String> propertyRefs = new ArrayList<>();
parsePropertyString(value, fragments, propertyRefs);
final StringBuilder sb = new StringBuilder(256);
final Iterator<String> fragmentsIterator = fragments.iterator();
final Iterator<String> propertyRefsIterator = propertyRefs.iterator();
while (fragmentsIterator.hasNext()) {
String fragment = fragmentsIterator.next();
if (fragment == null) {
final String propertyName = propertyRefsIterator.next();
fragment = props.resolve(propertyName);
if (fragment == null) {
if (defaultValue != null) {
sb.replace(0, sb.length(), defaultValue);
break;
}
throw new CheckstyleException(
"Property ${" + propertyName + "} has not been set");
}
}
sb.append(fragment);
}
return sb.toString();
}
/**
* Parses a string containing {@code ${xxx}} style property
* references into two lists. The first list is a collection
* of text fragments, while the other is a set of string property names.
* {@code null} entries in the first list indicate a property
* reference from the second list.
*
* <p>Code copied from ant -
* http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java
*
* @param value Text to parse. Must not be {@code null}.
* @param fragments List to add text fragments to.
* Must not be {@code null}.
* @param propertyRefs List to add property names to.
* Must not be {@code null}.
*
* @throws CheckstyleException if the string contains an opening
* {@code ${} without a closing
* {@code }}
*/
private static void parsePropertyString(String value,
List<String> fragments,
List<String> propertyRefs)
throws CheckstyleException {
int prev = 0;
//search for the next instance of $ from the 'prev' position
int pos = value.indexOf(DOLLAR_SIGN, prev);
while (pos >= 0) {
//if there was any text before this, add it as a fragment
if (pos > 0) {
fragments.add(value.substring(prev, pos));
}
//if we are at the end of the string, we tack on a $
//then move past it
if (pos == value.length() - 1) {
fragments.add(String.valueOf(DOLLAR_SIGN));
prev = pos + 1;
}
else if (value.charAt(pos + 1) == '{') {
//property found, extract its name or bail on a typo
final int endName = value.indexOf('}', pos);
if (endName == -1) {
throw new CheckstyleException("Syntax error in property: "
+ value);
}
final String propertyName = value.substring(pos + 2, endName);
fragments.add(null);
propertyRefs.add(propertyName);
prev = endName + 1;
}
else {
if (value.charAt(pos + 1) == DOLLAR_SIGN) {
//backwards compatibility two $ map to one mode
fragments.add(String.valueOf(DOLLAR_SIGN));
prev = pos + 2;
}
else {
//new behaviour: $X maps to $X for all values of X!='$'
fragments.add(value.substring(pos, pos + 2));
prev = pos + 2;
}
}
//search for the next instance of $ from the 'prev' position
pos = value.indexOf(DOLLAR_SIGN, prev);
}
//no more $ signs found
//if there is any tail to the file, append it
if (prev < value.length()) {
fragments.add(value.substring(prev));
}
}
/**
* Implements the SAX document handler interfaces, so they do not
* appear in the public API of the ConfigurationLoader.
*/
private final class InternalLoader
extends AbstractLoader {
/** Module elements. */
private static final String MODULE = "module";
/** Name attribute. */
private static final String NAME = "name";
/** Property element. */
private static final String PROPERTY = "property";
/** Value attribute. */
private static final String VALUE = "value";
/** Default attribute. */
private static final String DEFAULT = "default";
/** Name of the severity property. */
private static final String SEVERITY = "severity";
/** Name of the message element. */
private static final String MESSAGE = "message";
/** Name of the message element. */
private static final String METADATA = "metadata";
/** Name of the key attribute. */
private static final String KEY = "key";
/**
* Creates a new InternalLoader.
* @throws SAXException if an error occurs
* @throws ParserConfigurationException if an error occurs
*/
InternalLoader()
throws SAXException, ParserConfigurationException {
super(createIdToResourceNameMap());
}
@Override
public void startElement(String uri,
String localName,
String qName,
Attributes attributes)
throws SAXException {
if (qName.equals(MODULE)) {
//create configuration
final String originalName = attributes.getValue(NAME);
final String name = threadModeSettings.resolveName(originalName);
final DefaultConfiguration conf =
new DefaultConfiguration(name, threadModeSettings);
if (configuration == null) {
configuration = conf;
}
//add configuration to it's parent
if (!configStack.isEmpty()) {
final DefaultConfiguration top =
configStack.peek();
top.addChild(conf);
}
configStack.push(conf);
}
else if (qName.equals(PROPERTY)) {
//extract value and name
final String value;
try {
value = replaceProperties(attributes.getValue(VALUE),
overridePropsResolver, attributes.getValue(DEFAULT));
}
catch (final CheckstyleException ex) {
// -@cs[IllegalInstantiation] SAXException is in the overridden method signature
throw new SAXException(ex);
}
final String name = attributes.getValue(NAME);
//add to attributes of configuration
final DefaultConfiguration top =
configStack.peek();
top.addAttribute(name, value);
}
else if (qName.equals(MESSAGE)) {
//extract key and value
final String key = attributes.getValue(KEY);
final String value = attributes.getValue(VALUE);
//add to messages of configuration
final DefaultConfiguration top = configStack.peek();
top.addMessage(key, value);
}
else {
if (!qName.equals(METADATA)) {
throw new IllegalStateException("Unknown name:" + qName + ".");
}
}
}
@Override
public void endElement(String uri,
String localName,
String qName) throws SAXException {
if (qName.equals(MODULE)) {
final Configuration recentModule =
configStack.pop();
// get severity attribute if it exists
SeverityLevel level = null;
if (containsAttribute(recentModule, SEVERITY)) {
try {
final String severity = recentModule.getAttribute(SEVERITY);
level = SeverityLevel.getInstance(severity);
}
catch (final CheckstyleException ex) {
// -@cs[IllegalInstantiation] SAXException is in the overridden
// method signature
throw new SAXException(
"Problem during accessing '" + SEVERITY + "' attribute for "
+ recentModule.getName(), ex);
}
}
// omit this module if these should be omitted and the module
// has the severity 'ignore'
final boolean omitModule = omitIgnoredModules
&& level == SeverityLevel.IGNORE;
if (omitModule && !configStack.isEmpty()) {
final DefaultConfiguration parentModule =
configStack.peek();
parentModule.removeChild(recentModule);
}
}
}
/**
* Util method to recheck attribute in module.
* @param module module to check
* @param attributeName name of attribute in module to find
* @return true if attribute is present in module
*/
private boolean containsAttribute(Configuration module, String attributeName) {
final String[] names = module.getAttributeNames();
final Optional<String> result = Arrays.stream(names)
.filter(name -> name.equals(attributeName)).findFirst();
return result.isPresent();
}
}
}