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}