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