001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2021 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.net.URI;
024import java.util.ArrayDeque;
025import java.util.ArrayList;
026import java.util.Arrays;
027import java.util.Deque;
028import java.util.HashMap;
029import java.util.Iterator;
030import java.util.List;
031import java.util.Locale;
032import java.util.Map;
033import java.util.Optional;
034
035import javax.xml.parsers.ParserConfigurationException;
036
037import org.xml.sax.Attributes;
038import org.xml.sax.InputSource;
039import org.xml.sax.SAXException;
040import org.xml.sax.SAXParseException;
041
042import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
043import com.puppycrawl.tools.checkstyle.api.Configuration;
044import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
045import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
046
047/**
048 * Loads a configuration from a standard configuration XML file.
049 *
050 */
051public final class ConfigurationLoader {
052
053    /**
054     * Enum to specify behaviour regarding ignored modules.
055     */
056    public enum IgnoredModulesOptions {
057
058        /**
059         * Omit ignored modules.
060         */
061        OMIT,
062
063        /**
064         * Execute ignored modules.
065         */
066        EXECUTE,
067
068    }
069
070    /** Format of message for sax parse exception. */
071    private static final String SAX_PARSE_EXCEPTION_FORMAT = "%s - %s:%s:%s";
072
073    /** The public ID for version 1_0 of the configuration dtd. */
074    private static final String DTD_PUBLIC_ID_1_0 =
075        "-//Puppy Crawl//DTD Check Configuration 1.0//EN";
076
077    /** The new public ID for version 1_0 of the configuration dtd. */
078    private static final String DTD_PUBLIC_CS_ID_1_0 =
079        "-//Checkstyle//DTD Checkstyle Configuration 1.0//EN";
080
081    /** The resource for version 1_0 of the configuration dtd. */
082    private static final String DTD_CONFIGURATION_NAME_1_0 =
083        "com/puppycrawl/tools/checkstyle/configuration_1_0.dtd";
084
085    /** The public ID for version 1_1 of the configuration dtd. */
086    private static final String DTD_PUBLIC_ID_1_1 =
087        "-//Puppy Crawl//DTD Check Configuration 1.1//EN";
088
089    /** The new public ID for version 1_1 of the configuration dtd. */
090    private static final String DTD_PUBLIC_CS_ID_1_1 =
091        "-//Checkstyle//DTD Checkstyle Configuration 1.1//EN";
092
093    /** The resource for version 1_1 of the configuration dtd. */
094    private static final String DTD_CONFIGURATION_NAME_1_1 =
095        "com/puppycrawl/tools/checkstyle/configuration_1_1.dtd";
096
097    /** The public ID for version 1_2 of the configuration dtd. */
098    private static final String DTD_PUBLIC_ID_1_2 =
099        "-//Puppy Crawl//DTD Check Configuration 1.2//EN";
100
101    /** The new public ID for version 1_2 of the configuration dtd. */
102    private static final String DTD_PUBLIC_CS_ID_1_2 =
103        "-//Checkstyle//DTD Checkstyle Configuration 1.2//EN";
104
105    /** The resource for version 1_2 of the configuration dtd. */
106    private static final String DTD_CONFIGURATION_NAME_1_2 =
107        "com/puppycrawl/tools/checkstyle/configuration_1_2.dtd";
108
109    /** The public ID for version 1_3 of the configuration dtd. */
110    private static final String DTD_PUBLIC_ID_1_3 =
111        "-//Puppy Crawl//DTD Check Configuration 1.3//EN";
112
113    /** The new public ID for version 1_3 of the configuration dtd. */
114    private static final String DTD_PUBLIC_CS_ID_1_3 =
115        "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN";
116
117    /** The resource for version 1_3 of the configuration dtd. */
118    private static final String DTD_CONFIGURATION_NAME_1_3 =
119        "com/puppycrawl/tools/checkstyle/configuration_1_3.dtd";
120
121    /** Prefix for the exception when unable to parse resource. */
122    private static final String UNABLE_TO_PARSE_EXCEPTION_PREFIX = "unable to parse"
123            + " configuration stream";
124
125    /** Dollar sign literal. */
126    private static final char DOLLAR_SIGN = '$';
127
128    /** The SAX document handler. */
129    private final InternalLoader saxHandler;
130
131    /** Property resolver. **/
132    private final PropertyResolver overridePropsResolver;
133    /** The loaded configurations. **/
134    private final Deque<DefaultConfiguration> configStack = new ArrayDeque<>();
135
136    /** Flags if modules with the severity 'ignore' should be omitted. */
137    private final boolean omitIgnoredModules;
138
139    /** The thread mode configuration. */
140    private final ThreadModeSettings threadModeSettings;
141
142    /** The Configuration that is being built. */
143    private Configuration configuration;
144
145    /**
146     * Creates a new {@code ConfigurationLoader} instance.
147     *
148     * @param overrideProps resolver for overriding properties
149     * @param omitIgnoredModules {@code true} if ignored modules should be
150     *         omitted
151     * @param threadModeSettings the thread mode configuration
152     * @throws ParserConfigurationException if an error occurs
153     * @throws SAXException if an error occurs
154     */
155    private ConfigurationLoader(final PropertyResolver overrideProps,
156                                final boolean omitIgnoredModules,
157                                final ThreadModeSettings threadModeSettings)
158            throws ParserConfigurationException, SAXException {
159        saxHandler = new InternalLoader();
160        overridePropsResolver = overrideProps;
161        this.omitIgnoredModules = omitIgnoredModules;
162        this.threadModeSettings = threadModeSettings;
163    }
164
165    /**
166     * Creates mapping between local resources and dtd ids. This method can't be
167     * moved to inner class because it must stay static because it is called
168     * from constructor and inner class isn't static.
169     *
170     * @return map between local resources and dtd ids.
171     * @noinspection MethodOnlyUsedFromInnerClass
172     */
173    private static Map<String, String> createIdToResourceNameMap() {
174        final Map<String, String> map = new HashMap<>();
175        map.put(DTD_PUBLIC_ID_1_0, DTD_CONFIGURATION_NAME_1_0);
176        map.put(DTD_PUBLIC_ID_1_1, DTD_CONFIGURATION_NAME_1_1);
177        map.put(DTD_PUBLIC_ID_1_2, DTD_CONFIGURATION_NAME_1_2);
178        map.put(DTD_PUBLIC_ID_1_3, DTD_CONFIGURATION_NAME_1_3);
179        map.put(DTD_PUBLIC_CS_ID_1_0, DTD_CONFIGURATION_NAME_1_0);
180        map.put(DTD_PUBLIC_CS_ID_1_1, DTD_CONFIGURATION_NAME_1_1);
181        map.put(DTD_PUBLIC_CS_ID_1_2, DTD_CONFIGURATION_NAME_1_2);
182        map.put(DTD_PUBLIC_CS_ID_1_3, DTD_CONFIGURATION_NAME_1_3);
183        return map;
184    }
185
186    /**
187     * Parses the specified input source loading the configuration information.
188     * The stream wrapped inside the source, if any, is NOT
189     * explicitly closed after parsing, it is the responsibility of
190     * the caller to close the stream.
191     *
192     * @param source the source that contains the configuration data
193     * @throws IOException if an error occurs
194     * @throws SAXException if an error occurs
195     */
196    private void parseInputSource(InputSource source)
197            throws IOException, SAXException {
198        saxHandler.parseInputSource(source);
199    }
200
201    /**
202     * Returns the module configurations in a specified file.
203     *
204     * @param config location of config file, can be either a URL or a filename
205     * @param overridePropsResolver overriding properties
206     * @return the check configurations
207     * @throws CheckstyleException if an error occurs
208     */
209    public static Configuration loadConfiguration(String config,
210            PropertyResolver overridePropsResolver) throws CheckstyleException {
211        return loadConfiguration(config, overridePropsResolver, IgnoredModulesOptions.EXECUTE);
212    }
213
214    /**
215     * Returns the module configurations in a specified file.
216     *
217     * @param config location of config file, can be either a URL or a filename
218     * @param overridePropsResolver overriding properties
219     * @param threadModeSettings the thread mode configuration
220     * @return the check configurations
221     * @throws CheckstyleException if an error occurs
222     */
223    public static Configuration loadConfiguration(String config,
224            PropertyResolver overridePropsResolver, ThreadModeSettings threadModeSettings)
225            throws CheckstyleException {
226        return loadConfiguration(config, overridePropsResolver,
227                IgnoredModulesOptions.EXECUTE, threadModeSettings);
228    }
229
230    /**
231     * Returns the module configurations in a specified file.
232     *
233     * @param config location of config file, can be either a URL or a filename
234     * @param overridePropsResolver overriding properties
235     * @param ignoredModulesOptions {@code OMIT} if modules with severity
236     *            'ignore' should be omitted, {@code EXECUTE} otherwise
237     * @return the check configurations
238     * @throws CheckstyleException if an error occurs
239     */
240    public static Configuration loadConfiguration(String config,
241                                                  PropertyResolver overridePropsResolver,
242                                                  IgnoredModulesOptions ignoredModulesOptions)
243            throws CheckstyleException {
244        return loadConfiguration(config, overridePropsResolver, ignoredModulesOptions,
245                ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE);
246    }
247
248    /**
249     * Returns the module configurations in a specified file.
250     *
251     * @param config location of config file, can be either a URL or a filename
252     * @param overridePropsResolver overriding properties
253     * @param ignoredModulesOptions {@code OMIT} if modules with severity
254     *            'ignore' should be omitted, {@code EXECUTE} otherwise
255     * @param threadModeSettings the thread mode configuration
256     * @return the check configurations
257     * @throws CheckstyleException if an error occurs
258     */
259    public static Configuration loadConfiguration(String config,
260                                                  PropertyResolver overridePropsResolver,
261                                                  IgnoredModulesOptions ignoredModulesOptions,
262                                                  ThreadModeSettings threadModeSettings)
263            throws CheckstyleException {
264        // figure out if this is a File or a URL
265        final URI uri = CommonUtil.getUriByFilename(config);
266        final InputSource source = new InputSource(uri.toString());
267        return loadConfiguration(source, overridePropsResolver,
268                ignoredModulesOptions, threadModeSettings);
269    }
270
271    /**
272     * Returns the module configurations from a specified input source.
273     * Note that if the source does wrap an open byte or character
274     * stream, clients are required to close that stream by themselves
275     *
276     * @param configSource the input stream to the Checkstyle configuration
277     * @param overridePropsResolver overriding properties
278     * @param ignoredModulesOptions {@code OMIT} if modules with severity
279     *            'ignore' should be omitted, {@code EXECUTE} otherwise
280     * @return the check configurations
281     * @throws CheckstyleException if an error occurs
282     */
283    public static Configuration loadConfiguration(InputSource configSource,
284                                                  PropertyResolver overridePropsResolver,
285                                                  IgnoredModulesOptions ignoredModulesOptions)
286            throws CheckstyleException {
287        return loadConfiguration(configSource, overridePropsResolver,
288                ignoredModulesOptions, ThreadModeSettings.SINGLE_THREAD_MODE_INSTANCE);
289    }
290
291    /**
292     * Returns the module configurations from a specified input source.
293     * Note that if the source does wrap an open byte or character
294     * stream, clients are required to close that stream by themselves
295     *
296     * @param configSource the input stream to the Checkstyle configuration
297     * @param overridePropsResolver overriding properties
298     * @param ignoredModulesOptions {@code OMIT} if modules with severity
299     *            'ignore' should be omitted, {@code EXECUTE} otherwise
300     * @param threadModeSettings the thread mode configuration
301     * @return the check configurations
302     * @throws CheckstyleException if an error occurs
303     * @noinspection WeakerAccess
304     */
305    public static Configuration loadConfiguration(InputSource configSource,
306                                                  PropertyResolver overridePropsResolver,
307                                                  IgnoredModulesOptions ignoredModulesOptions,
308                                                  ThreadModeSettings threadModeSettings)
309            throws CheckstyleException {
310        try {
311            final boolean omitIgnoreModules = ignoredModulesOptions == IgnoredModulesOptions.OMIT;
312            final ConfigurationLoader loader =
313                    new ConfigurationLoader(overridePropsResolver,
314                            omitIgnoreModules, threadModeSettings);
315            loader.parseInputSource(configSource);
316            return loader.configuration;
317        }
318        catch (final SAXParseException ex) {
319            final String message = String.format(Locale.ROOT, SAX_PARSE_EXCEPTION_FORMAT,
320                    UNABLE_TO_PARSE_EXCEPTION_PREFIX,
321                    ex.getMessage(), ex.getLineNumber(), ex.getColumnNumber());
322            throw new CheckstyleException(message, ex);
323        }
324        catch (final ParserConfigurationException | IOException | SAXException ex) {
325            throw new CheckstyleException(UNABLE_TO_PARSE_EXCEPTION_PREFIX, ex);
326        }
327    }
328
329    /**
330     * Replaces {@code ${xxx}} style constructions in the given value
331     * with the string value of the corresponding data types. This method must remain
332     * outside inner class for easier testing since inner class requires an instance.
333     *
334     * <p>Code copied from ant -
335     * http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java
336     *
337     * @param value The string to be scanned for property references.
338     *              May be {@code null}, in which case this
339     *              method returns immediately with no effect.
340     * @param props Mapping (String to String) of property names to their
341     *              values. Must not be {@code null}.
342     * @param defaultValue default to use if one of the properties in value
343     *              cannot be resolved from props.
344     *
345     * @return the original string with the properties replaced, or
346     *         {@code null} if the original string is {@code null}.
347     * @throws CheckstyleException if the string contains an opening
348     *                           {@code ${} without a closing
349     *                           {@code }}
350     * @noinspection MethodWithMultipleReturnPoints, MethodOnlyUsedFromInnerClass
351     */
352    private static String replaceProperties(
353            String value, PropertyResolver props, String defaultValue)
354            throws CheckstyleException {
355        if (value == null) {
356            return null;
357        }
358
359        final List<String> fragments = new ArrayList<>();
360        final List<String> propertyRefs = new ArrayList<>();
361        parsePropertyString(value, fragments, propertyRefs);
362
363        final StringBuilder sb = new StringBuilder(256);
364        final Iterator<String> fragmentsIterator = fragments.iterator();
365        final Iterator<String> propertyRefsIterator = propertyRefs.iterator();
366        while (fragmentsIterator.hasNext()) {
367            String fragment = fragmentsIterator.next();
368            if (fragment == null) {
369                final String propertyName = propertyRefsIterator.next();
370                fragment = props.resolve(propertyName);
371                if (fragment == null) {
372                    if (defaultValue != null) {
373                        sb.replace(0, sb.length(), defaultValue);
374                        break;
375                    }
376                    throw new CheckstyleException(
377                        "Property ${" + propertyName + "} has not been set");
378                }
379            }
380            sb.append(fragment);
381        }
382
383        return sb.toString();
384    }
385
386    /**
387     * Parses a string containing {@code ${xxx}} style property
388     * references into two lists. The first list is a collection
389     * of text fragments, while the other is a set of string property names.
390     * {@code null} entries in the first list indicate a property
391     * reference from the second list.
392     *
393     * <p>Code copied from ant -
394     * http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java
395     *
396     * @param value     Text to parse. Must not be {@code null}.
397     * @param fragments List to add text fragments to.
398     *                  Must not be {@code null}.
399     * @param propertyRefs List to add property names to.
400     *                     Must not be {@code null}.
401     *
402     * @throws CheckstyleException if the string contains an opening
403     *                           {@code ${} without a closing
404     *                           {@code }}
405     */
406    private static void parsePropertyString(String value,
407                                           List<String> fragments,
408                                           List<String> propertyRefs)
409            throws CheckstyleException {
410        int prev = 0;
411        // search for the next instance of $ from the 'prev' position
412        int pos = value.indexOf(DOLLAR_SIGN, prev);
413        while (pos >= 0) {
414            // if there was any text before this, add it as a fragment
415            if (pos > 0) {
416                fragments.add(value.substring(prev, pos));
417            }
418            // if we are at the end of the string, we tack on a $
419            // then move past it
420            if (pos == value.length() - 1) {
421                fragments.add(String.valueOf(DOLLAR_SIGN));
422                prev = pos + 1;
423            }
424            else if (value.charAt(pos + 1) == '{') {
425                // property found, extract its name or bail on a typo
426                final int endName = value.indexOf('}', pos);
427                if (endName == -1) {
428                    throw new CheckstyleException("Syntax error in property: "
429                                                    + value);
430                }
431                final String propertyName = value.substring(pos + 2, endName);
432                fragments.add(null);
433                propertyRefs.add(propertyName);
434                prev = endName + 1;
435            }
436            else {
437                if (value.charAt(pos + 1) == DOLLAR_SIGN) {
438                    // backwards compatibility two $ map to one mode
439                    fragments.add(String.valueOf(DOLLAR_SIGN));
440                }
441                else {
442                    // new behaviour: $X maps to $X for all values of X!='$'
443                    fragments.add(value.substring(pos, pos + 2));
444                }
445                prev = pos + 2;
446            }
447
448            // search for the next instance of $ from the 'prev' position
449            pos = value.indexOf(DOLLAR_SIGN, prev);
450        }
451        // no more $ signs found
452        // if there is any tail to the file, append it
453        if (prev < value.length()) {
454            fragments.add(value.substring(prev));
455        }
456    }
457
458    /**
459     * Implements the SAX document handler interfaces, so they do not
460     * appear in the public API of the ConfigurationLoader.
461     */
462    private final class InternalLoader
463        extends XmlLoader {
464
465        /** Module elements. */
466        private static final String MODULE = "module";
467        /** Name attribute. */
468        private static final String NAME = "name";
469        /** Property element. */
470        private static final String PROPERTY = "property";
471        /** Value attribute. */
472        private static final String VALUE = "value";
473        /** Default attribute. */
474        private static final String DEFAULT = "default";
475        /** Name of the severity property. */
476        private static final String SEVERITY = "severity";
477        /** Name of the message element. */
478        private static final String MESSAGE = "message";
479        /** Name of the message element. */
480        private static final String METADATA = "metadata";
481        /** Name of the key attribute. */
482        private static final String KEY = "key";
483
484        /**
485         * Creates a new InternalLoader.
486         *
487         * @throws SAXException if an error occurs
488         * @throws ParserConfigurationException if an error occurs
489         */
490        /* package */ InternalLoader()
491                throws SAXException, ParserConfigurationException {
492            super(createIdToResourceNameMap());
493        }
494
495        @Override
496        public void startElement(String uri,
497                                 String localName,
498                                 String qName,
499                                 Attributes attributes)
500                throws SAXException {
501            if (MODULE.equals(qName)) {
502                // create configuration
503                final String originalName = attributes.getValue(NAME);
504                final String name = threadModeSettings.resolveName(originalName);
505                final DefaultConfiguration conf =
506                    new DefaultConfiguration(name, threadModeSettings);
507
508                if (configuration == null) {
509                    configuration = conf;
510                }
511
512                // add configuration to it's parent
513                if (!configStack.isEmpty()) {
514                    final DefaultConfiguration top =
515                        configStack.peek();
516                    top.addChild(conf);
517                }
518
519                configStack.push(conf);
520            }
521            else if (PROPERTY.equals(qName)) {
522                // extract value and name
523                final String value;
524                try {
525                    value = replaceProperties(attributes.getValue(VALUE),
526                        overridePropsResolver, attributes.getValue(DEFAULT));
527                }
528                catch (final CheckstyleException ex) {
529                    // -@cs[IllegalInstantiation] SAXException is in the overridden method signature
530                    throw new SAXException(ex);
531                }
532                final String name = attributes.getValue(NAME);
533
534                // add to attributes of configuration
535                final DefaultConfiguration top =
536                    configStack.peek();
537                top.addProperty(name, value);
538            }
539            else if (MESSAGE.equals(qName)) {
540                // extract key and value
541                final String key = attributes.getValue(KEY);
542                final String value = attributes.getValue(VALUE);
543
544                // add to messages of configuration
545                final DefaultConfiguration top = configStack.peek();
546                top.addMessage(key, value);
547            }
548            else {
549                if (!METADATA.equals(qName)) {
550                    throw new IllegalStateException("Unknown name:" + qName + ".");
551                }
552            }
553        }
554
555        @Override
556        public void endElement(String uri,
557                               String localName,
558                               String qName) throws SAXException {
559            if (MODULE.equals(qName)) {
560                final Configuration recentModule =
561                    configStack.pop();
562
563                // get severity attribute if it exists
564                SeverityLevel level = null;
565                if (containsAttribute(recentModule, SEVERITY)) {
566                    try {
567                        final String severity = recentModule.getProperty(SEVERITY);
568                        level = SeverityLevel.getInstance(severity);
569                    }
570                    catch (final CheckstyleException ex) {
571                        // -@cs[IllegalInstantiation] SAXException is in the overridden
572                        // method signature
573                        throw new SAXException(
574                                "Problem during accessing '" + SEVERITY + "' attribute for "
575                                        + recentModule.getName(), ex);
576                    }
577                }
578
579                // omit this module if these should be omitted and the module
580                // has the severity 'ignore'
581                final boolean omitModule = omitIgnoredModules
582                    && level == SeverityLevel.IGNORE;
583
584                if (omitModule && !configStack.isEmpty()) {
585                    final DefaultConfiguration parentModule =
586                        configStack.peek();
587                    parentModule.removeChild(recentModule);
588                }
589            }
590        }
591
592        /**
593         * Util method to recheck attribute in module.
594         *
595         * @param module module to check
596         * @param attributeName name of attribute in module to find
597         * @return true if attribute is present in module
598         */
599        private boolean containsAttribute(Configuration module, String attributeName) {
600            final String[] names = module.getPropertyNames();
601            final Optional<String> result = Arrays.stream(names)
602                    .filter(name -> name.equals(attributeName)).findFirst();
603            return result.isPresent();
604        }
605
606    }
607
608}