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