View Javadoc
1   ///////////////////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3   // Copyright (C) 2001-2025 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ///////////////////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle;
21  
22  import java.io.IOException;
23  import java.util.ArrayDeque;
24  import java.util.ArrayList;
25  import java.util.Arrays;
26  import java.util.Collection;
27  import java.util.Deque;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Locale;
31  import java.util.Map;
32  import java.util.Optional;
33  
34  import javax.xml.parsers.ParserConfigurationException;
35  
36  import org.xml.sax.Attributes;
37  import org.xml.sax.InputSource;
38  import org.xml.sax.SAXException;
39  import org.xml.sax.SAXParseException;
40  
41  import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
42  import com.puppycrawl.tools.checkstyle.api.Configuration;
43  import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
44  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
45  
46  /**
47   * Loads a configuration from a standard configuration XML file.
48   *
49   */
50  public final class ConfigurationLoader {
51  
52      /**
53       * Enum to specify behaviour regarding ignored modules.
54       */
55      public enum IgnoredModulesOptions {
56  
57          /**
58           * Omit ignored modules.
59           */
60          OMIT,
61  
62          /**
63           * Execute ignored modules.
64           */
65          EXECUTE,
66  
67      }
68  
69      /** The new public ID for version 1_3 of the configuration dtd. */
70      public static final String DTD_PUBLIC_CS_ID_1_3 =
71          "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN";
72  
73      /** The resource for version 1_3 of the configuration dtd. */
74      public static final String DTD_CONFIGURATION_NAME_1_3 =
75          "com/puppycrawl/tools/checkstyle/configuration_1_3.dtd";
76  
77      /** Format of message for sax parse exception. */
78      private static final String SAX_PARSE_EXCEPTION_FORMAT = "%s - %s:%s:%s";
79  
80      /** The public ID for version 1_0 of the configuration dtd. */
81      private static final String DTD_PUBLIC_ID_1_0 =
82          "-//Puppy Crawl//DTD Check Configuration 1.0//EN";
83  
84      /** The new public ID for version 1_0 of the configuration dtd. */
85      private static final String DTD_PUBLIC_CS_ID_1_0 =
86          "-//Checkstyle//DTD Checkstyle Configuration 1.0//EN";
87  
88      /** The resource for version 1_0 of the configuration dtd. */
89      private static final String DTD_CONFIGURATION_NAME_1_0 =
90          "com/puppycrawl/tools/checkstyle/configuration_1_0.dtd";
91  
92      /** The public ID for version 1_1 of the configuration dtd. */
93      private static final String DTD_PUBLIC_ID_1_1 =
94          "-//Puppy Crawl//DTD Check Configuration 1.1//EN";
95  
96      /** The new public ID for version 1_1 of the configuration dtd. */
97      private static final String DTD_PUBLIC_CS_ID_1_1 =
98          "-//Checkstyle//DTD Checkstyle Configuration 1.1//EN";
99  
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 }