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