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