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