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