001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2025 the original author or authors.
004//
005// This library is free software; you can redistribute it and/or
006// modify it under the terms of the GNU Lesser General Public
007// License as published by the Free Software Foundation; either
008// version 2.1 of the License, or (at your option) any later version.
009//
010// This library is distributed in the hope that it will be useful,
011// but WITHOUT ANY WARRANTY; without even the implied warranty of
012// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013// Lesser General Public License for more details.
014//
015// You should have received a copy of the GNU Lesser General Public
016// License along with this library; if not, write to the Free Software
017// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle;
021
022import java.io.IOException;
023import java.util.ArrayDeque;
024import java.util.ArrayList;
025import java.util.Arrays;
026import java.util.Collection;
027import java.util.Deque;
028import java.util.Iterator;
029import java.util.List;
030import java.util.Locale;
031import java.util.Map;
032import java.util.Optional;
033
034import javax.xml.parsers.ParserConfigurationException;
035
036import org.xml.sax.Attributes;
037import org.xml.sax.InputSource;
038import org.xml.sax.SAXException;
039import org.xml.sax.SAXParseException;
040
041import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
042import com.puppycrawl.tools.checkstyle.api.Configuration;
043import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
044import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
045
046/**
047 * Loads a configuration from a standard configuration XML file.
048 *
049 */
050public final class ConfigurationLoader {
051
052 /**
053 * Enum to specify behaviour regarding ignored modules.
054 */
055 public enum IgnoredModulesOptions {
056
057 /**
058 * Omit ignored modules.
059 */
060 OMIT,
061
062 /**
063 * Execute ignored modules.
064 */
065 EXECUTE,
066
067 }
068
069 /** The new public ID for version 1_3 of the configuration dtd. */
070 public static final String DTD_PUBLIC_CS_ID_1_3 =
071 "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN";
072
073 /** The resource for version 1_3 of the configuration dtd. */
074 public static final String DTD_CONFIGURATION_NAME_1_3 =
075 "com/puppycrawl/tools/checkstyle/configuration_1_3.dtd";
076
077 /** Format of message for sax parse exception. */
078 private static final String SAX_PARSE_EXCEPTION_FORMAT = "%s - %s:%s:%s";
079
080 /** The public ID for version 1_0 of the configuration dtd. */
081 private static final String DTD_PUBLIC_ID_1_0 =
082 "-//Puppy Crawl//DTD Check Configuration 1.0//EN";
083
084 /** The new public ID for version 1_0 of the configuration dtd. */
085 private static final String DTD_PUBLIC_CS_ID_1_0 =
086 "-//Checkstyle//DTD Checkstyle Configuration 1.0//EN";
087
088 /** The resource for version 1_0 of the configuration dtd. */
089 private static final String DTD_CONFIGURATION_NAME_1_0 =
090 "com/puppycrawl/tools/checkstyle/configuration_1_0.dtd";
091
092 /** The public ID for version 1_1 of the configuration dtd. */
093 private static final String DTD_PUBLIC_ID_1_1 =
094 "-//Puppy Crawl//DTD Check Configuration 1.1//EN";
095
096 /** The new public ID for version 1_1 of the configuration dtd. */
097 private static final String DTD_PUBLIC_CS_ID_1_1 =
098 "-//Checkstyle//DTD Checkstyle Configuration 1.1//EN";
099
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}