////////////////////////////////////////////////////////////////////////////////
// checkstyle: Checks Java source code for adherence to a set of rules.
// Copyright (C) 2001-2014 Oliver Burn
//
// 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 com.google.common.collect.Lists;
import com.google.common.collect.Maps;
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.FastStack;
import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Loads a configuration from a standard configuration XML file.
*
* @author Oliver Burn
* @version 1.0
*/
public final class ConfigurationLoader
{
/** 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_RESOURCE_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_RESOURCE_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_RESOURCE_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_RESOURCE_NAME_1_3 =
"com/puppycrawl/tools/checkstyle/configuration_1_3.dtd";
/**
* 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 key attribute */
private static final String KEY = "key";
/**
* Creates a new InternalLoader.
* @throws SAXException if an error occurs
* @throws ParserConfigurationException if an error occurs
*/
private InternalLoader()
throws SAXException, ParserConfigurationException
{
// super(DTD_PUBLIC_ID_1_1, DTD_RESOURCE_NAME_1_1);
super(createIdToResourceNameMap());
}
@Override
public void startElement(String aNamespaceURI,
String aLocalName,
String aQName,
Attributes aAtts)
throws SAXException
{
// TODO: debug logging for support purposes
if (aQName.equals(MODULE)) {
//create configuration
final String name = aAtts.getValue(NAME);
final DefaultConfiguration conf =
new DefaultConfiguration(name);
if (mConfiguration == null) {
mConfiguration = conf;
}
//add configuration to it's parent
if (!mConfigStack.isEmpty()) {
final DefaultConfiguration top =
mConfigStack.peek();
top.addChild(conf);
}
mConfigStack.push(conf);
}
else if (aQName.equals(PROPERTY)) {
//extract name and value
final String name = aAtts.getValue(NAME);
final String value;
try {
value = replaceProperties(aAtts.getValue(VALUE),
mOverridePropsResolver, aAtts.getValue(DEFAULT));
}
catch (final CheckstyleException ex) {
throw new SAXException(ex.getMessage());
}
//add to attributes of configuration
final DefaultConfiguration top =
mConfigStack.peek();
top.addAttribute(name, value);
}
else if (aQName.equals(MESSAGE)) {
//extract key and value
final String key = aAtts.getValue(KEY);
final String value = aAtts.getValue(VALUE);
//add to messages of configuration
final DefaultConfiguration top = mConfigStack.peek();
top.addMessage(key, value);
}
}
@Override
public void endElement(String aNamespaceURI,
String aLocalName,
String aQName)
throws SAXException
{
if (aQName.equals(MODULE)) {
final Configuration recentModule =
mConfigStack.pop();
// remove modules with severity ignore if these modules should
// be omitted
SeverityLevel level = null;
try {
final String severity = recentModule.getAttribute(SEVERITY);
level = SeverityLevel.getInstance(severity);
}
catch (final CheckstyleException e) {
//severity not set -> ignore
;
}
// omit this module if these should be omitted and the module
// has the severity 'ignore'
final boolean omitModule = mOmitIgnoredModules
&& SeverityLevel.IGNORE.equals(level);
if (omitModule && !mConfigStack.isEmpty()) {
final DefaultConfiguration parentModule =
mConfigStack.peek();
parentModule.removeChild(recentModule);
}
}
}
}
/** the SAX document handler */
private final InternalLoader mSaxHandler;
/** property resolver **/
private final PropertyResolver mOverridePropsResolver;
/** the loaded configurations **/
private final FastStack<DefaultConfiguration> mConfigStack =
FastStack.newInstance();
/** the Configuration that is being built */
private Configuration mConfiguration;
/** flags if modules with the severity 'ignore' should be omitted. */
private final boolean mOmitIgnoredModules;
/**
* 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 = Maps.newHashMap();
map.put(DTD_PUBLIC_ID_1_0, DTD_RESOURCE_NAME_1_0);
map.put(DTD_PUBLIC_ID_1_1, DTD_RESOURCE_NAME_1_1);
map.put(DTD_PUBLIC_ID_1_2, DTD_RESOURCE_NAME_1_2);
map.put(DTD_PUBLIC_ID_1_3, DTD_RESOURCE_NAME_1_3);
return map;
}
/**
* Creates a new <code>ConfigurationLoader</code> instance.
* @param aOverrideProps resolver for overriding properties
* @param aOmitIgnoredModules <code>true</code> if ignored modules should be
* omitted
* @throws ParserConfigurationException if an error occurs
* @throws SAXException if an error occurs
*/
private ConfigurationLoader(final PropertyResolver aOverrideProps,
final boolean aOmitIgnoredModules)
throws ParserConfigurationException, SAXException
{
mSaxHandler = new InternalLoader();
mOverridePropsResolver = aOverrideProps;
mOmitIgnoredModules = aOmitIgnoredModules;
}
/**
* Parses the specified input source loading the configuration information.
* The stream wrapped inside the source, if any, is NOT
* explicitely closed after parsing, it is the responsibility of
* the caller to close the stream.
*
* @param aSource the source that contains the configuration data
* @throws IOException if an error occurs
* @throws SAXException if an error occurs
*/
private void parseInputSource(InputSource aSource)
throws IOException, SAXException
{
mSaxHandler.parseInputSource(aSource);
}
/**
* Returns the module configurations in a specified file.
* @param aConfig location of config file, can be either a URL or a filename
* @param aOverridePropsResolver overriding properties
* @return the check configurations
* @throws CheckstyleException if an error occurs
*/
public static Configuration loadConfiguration(String aConfig,
PropertyResolver aOverridePropsResolver) throws CheckstyleException
{
return loadConfiguration(aConfig, aOverridePropsResolver, false);
}
/**
* Returns the module configurations in a specified file.
*
* @param aConfig location of config file, can be either a URL or a filename
* @param aOverridePropsResolver overriding properties
* @param aOmitIgnoredModules <code>true</code> if modules with severity
* 'ignore' should be omitted, <code>false</code> otherwise
* @return the check configurations
* @throws CheckstyleException if an error occurs
*/
public static Configuration loadConfiguration(String aConfig,
PropertyResolver aOverridePropsResolver, boolean aOmitIgnoredModules)
throws CheckstyleException
{
try {
// figure out if this is a File or a URL
URI uri;
try {
final URL url = new URL(aConfig);
uri = url.toURI();
}
catch (final MalformedURLException ex) {
uri = null;
}
catch (final URISyntaxException ex) {
// URL violating RFC 2396
uri = null;
}
if (uri == null) {
final File file = new File(aConfig);
if (file.exists()) {
uri = file.toURI();
}
else {
// check to see if the file is in the classpath
try {
final URL configUrl = ConfigurationLoader.class
.getResource(aConfig);
if (configUrl == null) {
throw new FileNotFoundException(aConfig);
}
uri = configUrl.toURI();
}
catch (final URISyntaxException e) {
throw new FileNotFoundException(aConfig);
}
}
}
final InputSource source = new InputSource(uri.toString());
return loadConfiguration(source, aOverridePropsResolver,
aOmitIgnoredModules);
}
catch (final FileNotFoundException e) {
throw new CheckstyleException("unable to find " + aConfig, e);
}
catch (final CheckstyleException e) {
//wrap again to add file name info
throw new CheckstyleException("unable to read " + aConfig + " - "
+ e.getMessage(), e);
}
}
/**
* Returns the module configurations from a specified input stream.
* Note that clients are required to close the given stream by themselves
*
* @param aConfigStream the input stream to the Checkstyle configuration
* @param aOverridePropsResolver overriding properties
* @param aOmitIgnoredModules <code>true</code> if modules with severity
* 'ignore' should be omitted, <code>false</code> 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
*/
@Deprecated
public static Configuration loadConfiguration(InputStream aConfigStream,
PropertyResolver aOverridePropsResolver, boolean aOmitIgnoredModules)
throws CheckstyleException
{
return loadConfiguration(new InputSource(aConfigStream),
aOverridePropsResolver, aOmitIgnoredModules);
}
/**
* 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 aConfigSource the input stream to the Checkstyle configuration
* @param aOverridePropsResolver overriding properties
* @param aOmitIgnoredModules <code>true</code> if modules with severity
* 'ignore' should be omitted, <code>false</code> otherwise
* @return the check configurations
* @throws CheckstyleException if an error occurs
*/
public static Configuration loadConfiguration(InputSource aConfigSource,
PropertyResolver aOverridePropsResolver, boolean aOmitIgnoredModules)
throws CheckstyleException
{
try {
final ConfigurationLoader loader =
new ConfigurationLoader(aOverridePropsResolver,
aOmitIgnoredModules);
loader.parseInputSource(aConfigSource);
return loader.getConfiguration();
}
catch (final ParserConfigurationException e) {
throw new CheckstyleException(
"unable to parse configuration stream", e);
}
catch (final SAXParseException e) {
throw new CheckstyleException("unable to parse configuration stream"
+ " - " + e.getMessage() + ":" + e.getLineNumber()
+ ":" + e.getColumnNumber(), e);
}
catch (final SAXException e) {
throw new CheckstyleException("unable to parse configuration stream"
+ " - " + e.getMessage(), e);
}
catch (final IOException e) {
throw new CheckstyleException("unable to read from stream", e);
}
}
/**
* Returns the configuration in the last file parsed.
* @return Configuration object
*/
private Configuration getConfiguration()
{
return mConfiguration;
}
/**
* Replaces <code>${xxx}</code> style constructions in the given value
* with the string value of the corresponding data types.
*
* The method is package visible to facilitate testing.
*
* @param aValue The string to be scanned for property references.
* May be <code>null</code>, in which case this
* method returns immediately with no effect.
* @param aProps Mapping (String to String) of property names to their
* values. Must not be <code>null</code>.
* @param aDefaultValue default to use if one of the properties in aValue
* cannot be resolved from aProps.
*
* @throws CheckstyleException if the string contains an opening
* <code>${</code> without a closing
* <code>}</code>
* @return the original string with the properties replaced, or
* <code>null</code> if the original string is <code>null</code>.
*
* Code copied from ant -
* http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java
*/
// Package visible for testing purposes
static String replaceProperties(
String aValue, PropertyResolver aProps, String aDefaultValue)
throws CheckstyleException
{
if (aValue == null) {
return null;
}
final List<String> fragments = Lists.newArrayList();
final List<String> propertyRefs = Lists.newArrayList();
parsePropertyString(aValue, fragments, propertyRefs);
final StringBuffer sb = new StringBuffer();
final Iterator<String> i = fragments.iterator();
final Iterator<String> j = propertyRefs.iterator();
while (i.hasNext()) {
String fragment = i.next();
if (fragment == null) {
final String propertyName = j.next();
fragment = aProps.resolve(propertyName);
if (fragment == null) {
if (aDefaultValue != null) {
return aDefaultValue;
}
throw new CheckstyleException(
"Property ${" + propertyName + "} has not been set");
}
}
sb.append(fragment);
}
return sb.toString();
}
/**
* Parses a string containing <code>${xxx}</code> 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</code> entries in the first list indicate a property
* reference from the second list.
*
* @param aValue Text to parse. Must not be <code>null</code>.
* @param aFragments List to add text fragments to.
* Must not be <code>null</code>.
* @param aPropertyRefs List to add property names to.
* Must not be <code>null</code>.
*
* @throws CheckstyleException if the string contains an opening
* <code>${</code> without a closing
* <code>}</code>
* Code copied from ant -
* http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java
*/
private static void parsePropertyString(String aValue,
List<String> aFragments,
List<String> aPropertyRefs)
throws CheckstyleException
{
int prev = 0;
int pos;
//search for the next instance of $ from the 'prev' position
while ((pos = aValue.indexOf("$", prev)) >= 0) {
//if there was any text before this, add it as a fragment
//TODO, this check could be modified to go if pos>prev;
//seems like this current version could stick empty strings
//into the list
if (pos > 0) {
aFragments.add(aValue.substring(prev, pos));
}
//if we are at the end of the string, we tack on a $
//then move past it
if (pos == (aValue.length() - 1)) {
aFragments.add("$");
prev = pos + 1;
}
else if (aValue.charAt(pos + 1) != '{') {
//peek ahead to see if the next char is a property or not
//not a property: insert the char as a literal
/*
fragments.addElement(value.substring(pos + 1, pos + 2));
prev = pos + 2;
*/
if (aValue.charAt(pos + 1) == '$') {
//backwards compatibility two $ map to one mode
aFragments.add("$");
prev = pos + 2;
}
else {
//new behaviour: $X maps to $X for all values of X!='$'
aFragments.add(aValue.substring(pos, pos + 2));
prev = pos + 2;
}
}
else {
//property found, extract its name or bail on a typo
final int endName = aValue.indexOf('}', pos);
if (endName < 0) {
throw new CheckstyleException("Syntax error in property: "
+ aValue);
}
final String propertyName = aValue.substring(pos + 2, endName);
aFragments.add(null);
aPropertyRefs.add(propertyName);
prev = endName + 1;
}
}
//no more $ signs found
//if there is any tail to the file, append it
if (prev < aValue.length()) {
aFragments.add(aValue.substring(prev));
}
}
}