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