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