1 ////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code for adherence to a set of rules.
3 // Copyright (C) 2001-2015 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 package com.puppycrawl.tools.checkstyle;
20
21 import com.google.common.collect.Lists;
22 import com.google.common.collect.Maps;
23 import com.puppycrawl.tools.checkstyle.api.AbstractLoader;
24 import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
25 import com.puppycrawl.tools.checkstyle.api.Configuration;
26 import com.puppycrawl.tools.checkstyle.api.FastStack;
27 import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
28 import org.xml.sax.Attributes;
29 import org.xml.sax.InputSource;
30 import org.xml.sax.SAXException;
31 import org.xml.sax.SAXParseException;
32
33 import javax.xml.parsers.ParserConfigurationException;
34 import java.io.File;
35 import java.io.FileNotFoundException;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.net.MalformedURLException;
39 import java.net.URI;
40 import java.net.URISyntaxException;
41 import java.net.URL;
42 import java.util.Iterator;
43 import java.util.List;
44 import java.util.Map;
45
46 /**
47 * Loads a configuration from a standard configuration XML file.
48 *
49 * @author Oliver Burn
50 */
51 public final class ConfigurationLoader
52 {
53 /** the public ID for version 1_0 of the configuration dtd */
54 private static final String DTD_PUBLIC_ID_1_0 =
55 "-//Puppy Crawl//DTD Check Configuration 1.0//EN";
56
57 /** the resource for version 1_0 of the configuration dtd */
58 private static final String DTD_RESOURCE_NAME_1_0 =
59 "com/puppycrawl/tools/checkstyle/configuration_1_0.dtd";
60
61 /** the public ID for version 1_1 of the configuration dtd */
62 private static final String DTD_PUBLIC_ID_1_1 =
63 "-//Puppy Crawl//DTD Check Configuration 1.1//EN";
64
65 /** the resource for version 1_1 of the configuration dtd */
66 private static final String DTD_RESOURCE_NAME_1_1 =
67 "com/puppycrawl/tools/checkstyle/configuration_1_1.dtd";
68
69 /** the public ID for version 1_2 of the configuration dtd */
70 private static final String DTD_PUBLIC_ID_1_2 =
71 "-//Puppy Crawl//DTD Check Configuration 1.2//EN";
72
73 /** the resource for version 1_2 of the configuration dtd */
74 private static final String DTD_RESOURCE_NAME_1_2 =
75 "com/puppycrawl/tools/checkstyle/configuration_1_2.dtd";
76
77 /** the public ID for version 1_3 of the configuration dtd */
78 private static final String DTD_PUBLIC_ID_1_3 =
79 "-//Puppy Crawl//DTD Check Configuration 1.3//EN";
80
81 /** the resource for version 1_3 of the configuration dtd */
82 private static final String DTD_RESOURCE_NAME_1_3 =
83 "com/puppycrawl/tools/checkstyle/configuration_1_3.dtd";
84
85 /**
86 * Implements the SAX document handler interfaces, so they do not
87 * appear in the public API of the ConfigurationLoader.
88 */
89 private final class InternalLoader
90 extends AbstractLoader
91 {
92 /** module elements */
93 private static final String MODULE = "module";
94 /** name attribute */
95 private static final String NAME = "name";
96 /** property element */
97 private static final String PROPERTY = "property";
98 /** value attribute */
99 private static final String VALUE = "value";
100 /** default attribute */
101 private static final String DEFAULT = "default";
102 /** name of the severity property */
103 private static final String SEVERITY = "severity";
104 /** name of the message element */
105 private static final String MESSAGE = "message";
106 /** name of the key attribute */
107 private static final String KEY = "key";
108
109 /**
110 * Creates a new InternalLoader.
111 * @throws SAXException if an error occurs
112 * @throws ParserConfigurationException if an error occurs
113 */
114 private InternalLoader()
115 throws SAXException, ParserConfigurationException
116 {
117 // super(DTD_PUBLIC_ID_1_1, DTD_RESOURCE_NAME_1_1);
118 super(createIdToResourceNameMap());
119 }
120
121 @Override
122 public void startElement(String namespaceURI,
123 String localName,
124 String qName,
125 Attributes atts)
126 throws SAXException
127 {
128 // TODO: debug logging for support purposes
129 if (qName.equals(MODULE)) {
130 //create configuration
131 final String name = atts.getValue(NAME);
132 final DefaultConfiguration conf =
133 new DefaultConfiguration(name);
134
135 if (configuration == null) {
136 configuration = conf;
137 }
138
139 //add configuration to it's parent
140 if (!configStack.isEmpty()) {
141 final DefaultConfiguration top =
142 configStack.peek();
143 top.addChild(conf);
144 }
145
146 configStack.push(conf);
147 }
148 else if (qName.equals(PROPERTY)) {
149 //extract name and value
150 final String name = atts.getValue(NAME);
151 final String value;
152 try {
153 value = replaceProperties(atts.getValue(VALUE),
154 overridePropsResolver, atts.getValue(DEFAULT));
155 }
156 catch (final CheckstyleException ex) {
157 throw new SAXException(ex.getMessage());
158 }
159
160 //add to attributes of configuration
161 final DefaultConfiguration top =
162 configStack.peek();
163 top.addAttribute(name, value);
164 }
165 else if (qName.equals(MESSAGE)) {
166 //extract key and value
167 final String key = atts.getValue(KEY);
168 final String value = atts.getValue(VALUE);
169
170 //add to messages of configuration
171 final DefaultConfiguration top = configStack.peek();
172 top.addMessage(key, value);
173 }
174 }
175
176 @Override
177 public void endElement(String namespaceURI,
178 String localName,
179 String qName)
180 throws SAXException
181 {
182 if (qName.equals(MODULE)) {
183
184 final Configuration recentModule =
185 configStack.pop();
186
187 // remove modules with severity ignore if these modules should
188 // be omitted
189 SeverityLevel level = null;
190 try {
191 final String severity = recentModule.getAttribute(SEVERITY);
192 level = SeverityLevel.getInstance(severity);
193 }
194 catch (final CheckstyleException e) {
195 //severity not set -> ignore
196 }
197
198 // omit this module if these should be omitted and the module
199 // has the severity 'ignore'
200 final boolean omitModule = omitIgnoredModules
201 && SeverityLevel.IGNORE == level;
202
203 if (omitModule && !configStack.isEmpty()) {
204 final DefaultConfiguration parentModule =
205 configStack.peek();
206 parentModule.removeChild(recentModule);
207 }
208 }
209 }
210
211 }
212
213 /** the SAX document handler */
214 private final InternalLoader saxHandler;
215
216 /** property resolver **/
217 private final PropertyResolver overridePropsResolver;
218 /** the loaded configurations **/
219 private final FastStack<DefaultConfiguration> configStack =
220 FastStack.newInstance();
221 /** the Configuration that is being built */
222 private Configuration configuration;
223
224 /** flags if modules with the severity 'ignore' should be omitted. */
225 private final boolean omitIgnoredModules;
226
227 /**
228 * Creates mapping between local resources and dtd ids.
229 * @return map between local resources and dtd ids.
230 */
231 private static Map<String, String> createIdToResourceNameMap()
232 {
233 final Map<String, String> map = Maps.newHashMap();
234 map.put(DTD_PUBLIC_ID_1_0, DTD_RESOURCE_NAME_1_0);
235 map.put(DTD_PUBLIC_ID_1_1, DTD_RESOURCE_NAME_1_1);
236 map.put(DTD_PUBLIC_ID_1_2, DTD_RESOURCE_NAME_1_2);
237 map.put(DTD_PUBLIC_ID_1_3, DTD_RESOURCE_NAME_1_3);
238 return map;
239 }
240
241 /**
242 * Creates a new <code>ConfigurationLoader</code> instance.
243 * @param overrideProps resolver for overriding properties
244 * @param omitIgnoredModules <code>true</code> if ignored modules should be
245 * omitted
246 * @throws ParserConfigurationException if an error occurs
247 * @throws SAXException if an error occurs
248 */
249 private ConfigurationLoader(final PropertyResolver overrideProps,
250 final boolean omitIgnoredModules)
251 throws ParserConfigurationException, SAXException
252 {
253 saxHandler = new InternalLoader();
254 overridePropsResolver = overrideProps;
255 this.omitIgnoredModules = omitIgnoredModules;
256 }
257
258 /**
259 * Parses the specified input source loading the configuration information.
260 * The stream wrapped inside the source, if any, is NOT
261 * explicitely closed after parsing, it is the responsibility of
262 * the caller to close the stream.
263 *
264 * @param source the source that contains the configuration data
265 * @throws IOException if an error occurs
266 * @throws SAXException if an error occurs
267 */
268 private void parseInputSource(InputSource source)
269 throws IOException, SAXException
270 {
271 saxHandler.parseInputSource(source);
272 }
273
274 /**
275 * Returns the module configurations in a specified file.
276 * @param config location of config file, can be either a URL or a filename
277 * @param overridePropsResolver overriding properties
278 * @return the check configurations
279 * @throws CheckstyleException if an error occurs
280 */
281 public static Configuration loadConfiguration(String config,
282 PropertyResolver overridePropsResolver) throws CheckstyleException
283 {
284 return loadConfiguration(config, overridePropsResolver, false);
285 }
286
287 /**
288 * Returns the module configurations in a specified file.
289 *
290 * @param config location of config file, can be either a URL or a filename
291 * @param overridePropsResolver overriding properties
292 * @param omitIgnoredModules <code>true</code> if modules with severity
293 * 'ignore' should be omitted, <code>false</code> otherwise
294 * @return the check configurations
295 * @throws CheckstyleException if an error occurs
296 */
297 public static Configuration loadConfiguration(String config,
298 PropertyResolver overridePropsResolver, boolean omitIgnoredModules)
299 throws CheckstyleException
300 {
301 try {
302 // figure out if this is a File or a URL
303 URI uri;
304 try {
305 final URL url = new URL(config);
306 uri = url.toURI();
307 }
308 catch (final MalformedURLException ex) {
309 uri = null;
310 }
311 catch (final URISyntaxException ex) {
312 // URL violating RFC 2396
313 uri = null;
314 }
315 if (uri == null) {
316 final File file = new File(config);
317 if (file.exists()) {
318 uri = file.toURI();
319 }
320 else {
321 // check to see if the file is in the classpath
322 try {
323 final URL configUrl = ConfigurationLoader.class
324 .getResource(config);
325 if (configUrl == null) {
326 throw new FileNotFoundException(config);
327 }
328 uri = configUrl.toURI();
329 }
330 catch (final URISyntaxException e) {
331 throw new FileNotFoundException(config);
332 }
333 }
334 }
335 final InputSource source = new InputSource(uri.toString());
336 return loadConfiguration(source, overridePropsResolver,
337 omitIgnoredModules);
338 }
339 catch (final FileNotFoundException e) {
340 throw new CheckstyleException("unable to find " + config, e);
341 }
342 catch (final CheckstyleException e) {
343 //wrap again to add file name info
344 throw new CheckstyleException("unable to read " + config + " - "
345 + e.getMessage(), e);
346 }
347 }
348
349 /**
350 * Returns the module configurations from a specified input stream.
351 * Note that clients are required to close the given stream by themselves
352 *
353 * @param configStream the input stream to the Checkstyle configuration
354 * @param overridePropsResolver overriding properties
355 * @param omitIgnoredModules <code>true</code> if modules with severity
356 * 'ignore' should be omitted, <code>false</code> otherwise
357 * @return the check configurations
358 * @throws CheckstyleException if an error occurs
359 *
360 * @deprecated As this method does not provide a valid system ID,
361 * preventing resolution of external entities, a
362 * {@link #loadConfiguration(InputSource,PropertyResolver,boolean)
363 * version using an InputSource}
364 * should be used instead
365 */
366 @Deprecated
367 public static Configuration loadConfiguration(InputStream configStream,
368 PropertyResolver overridePropsResolver, boolean omitIgnoredModules)
369 throws CheckstyleException
370 {
371 return loadConfiguration(new InputSource(configStream),
372 overridePropsResolver, omitIgnoredModules);
373 }
374
375 /**
376 * Returns the module configurations from a specified input source.
377 * Note that if the source does wrap an open byte or character
378 * stream, clients are required to close that stream by themselves
379 *
380 * @param configSource the input stream to the Checkstyle configuration
381 * @param overridePropsResolver overriding properties
382 * @param omitIgnoredModules <code>true</code> if modules with severity
383 * 'ignore' should be omitted, <code>false</code> otherwise
384 * @return the check configurations
385 * @throws CheckstyleException if an error occurs
386 */
387 public static Configuration loadConfiguration(InputSource configSource,
388 PropertyResolver overridePropsResolver, boolean omitIgnoredModules)
389 throws CheckstyleException
390 {
391 try {
392 final ConfigurationLoader loader =
393 new ConfigurationLoader(overridePropsResolver,
394 omitIgnoredModules);
395 loader.parseInputSource(configSource);
396 return loader.getConfiguration();
397 }
398 catch (final ParserConfigurationException e) {
399 throw new CheckstyleException(
400 "unable to parse configuration stream", e);
401 }
402 catch (final SAXParseException e) {
403 throw new CheckstyleException("unable to parse configuration stream"
404 + " - " + e.getMessage() + ":" + e.getLineNumber()
405 + ":" + e.getColumnNumber(), e);
406 }
407 catch (final SAXException e) {
408 throw new CheckstyleException("unable to parse configuration stream"
409 + " - " + e.getMessage(), e);
410 }
411 catch (final IOException e) {
412 throw new CheckstyleException("unable to read from stream", e);
413 }
414 }
415
416 /**
417 * Returns the configuration in the last file parsed.
418 * @return Configuration object
419 */
420 private Configuration getConfiguration()
421 {
422 return configuration;
423 }
424
425 /**
426 * Replaces <code>${xxx}</code> style constructions in the given value
427 * with the string value of the corresponding data types.
428 *
429 * The method is package visible to facilitate testing.
430 *
431 * @param value The string to be scanned for property references.
432 * May be <code>null</code>, in which case this
433 * method returns immediately with no effect.
434 * @param props Mapping (String to String) of property names to their
435 * values. Must not be <code>null</code>.
436 * @param defaultValue default to use if one of the properties in value
437 * cannot be resolved from props.
438 *
439 * @throws CheckstyleException if the string contains an opening
440 * <code>${</code> without a closing
441 * <code>}</code>
442 * @return the original string with the properties replaced, or
443 * <code>null</code> if the original string is <code>null</code>.
444 *
445 * Code copied from ant -
446 * http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java
447 */
448 // Package visible for testing purposes
449 static String replaceProperties(
450 String value, PropertyResolver props, String defaultValue)
451 throws CheckstyleException
452 {
453 if (value == null) {
454 return null;
455 }
456
457 final List<String> fragments = Lists.newArrayList();
458 final List<String> propertyRefs = Lists.newArrayList();
459 parsePropertyString(value, fragments, propertyRefs);
460
461 final StringBuilder sb = new StringBuilder();
462 final Iterator<String> i = fragments.iterator();
463 final Iterator<String> j = propertyRefs.iterator();
464 while (i.hasNext()) {
465 String fragment = i.next();
466 if (fragment == null) {
467 final String propertyName = j.next();
468 fragment = props.resolve(propertyName);
469 if (fragment == null) {
470 if (defaultValue != null) {
471 return defaultValue;
472 }
473 throw new CheckstyleException(
474 "Property ${" + propertyName + "} has not been set");
475 }
476 }
477 sb.append(fragment);
478 }
479
480 return sb.toString();
481 }
482
483 /**
484 * Parses a string containing <code>${xxx}</code> style property
485 * references into two lists. The first list is a collection
486 * of text fragments, while the other is a set of string property names.
487 * <code>null</code> entries in the first list indicate a property
488 * reference from the second list.
489 *
490 * @param value Text to parse. Must not be <code>null</code>.
491 * @param fragments List to add text fragments to.
492 * Must not be <code>null</code>.
493 * @param propertyRefs List to add property names to.
494 * Must not be <code>null</code>.
495 *
496 * @throws CheckstyleException if the string contains an opening
497 * <code>${</code> without a closing
498 * <code>}</code>
499 * Code copied from ant -
500 * http://cvs.apache.org/viewcvs/jakarta-ant/src/main/org/apache/tools/ant/ProjectHelper.java
501 */
502 private static void parsePropertyString(String value,
503 List<String> fragments,
504 List<String> propertyRefs)
505 throws CheckstyleException
506 {
507 int prev = 0;
508 int pos;
509 //search for the next instance of $ from the 'prev' position
510 while ((pos = value.indexOf("$", prev)) >= 0) {
511
512 //if there was any text before this, add it as a fragment
513 //TODO, this check could be modified to go if pos>prev;
514 //seems like this current version could stick empty strings
515 //into the list
516 if (pos > 0) {
517 fragments.add(value.substring(prev, pos));
518 }
519 //if we are at the end of the string, we tack on a $
520 //then move past it
521 if (pos == value.length() - 1) {
522 fragments.add("$");
523 prev = pos + 1;
524 }
525 else if (value.charAt(pos + 1) != '{') {
526 //peek ahead to see if the next char is a property or not
527 //not a property: insert the char as a literal
528 /*
529 fragments.addElement(value.substring(pos + 1, pos + 2));
530 prev = pos + 2;
531 */
532 if (value.charAt(pos + 1) == '$') {
533 //backwards compatibility two $ map to one mode
534 fragments.add("$");
535 prev = pos + 2;
536 }
537 else {
538 //new behaviour: $X maps to $X for all values of X!='$'
539 fragments.add(value.substring(pos, pos + 2));
540 prev = pos + 2;
541 }
542
543 }
544 else {
545 //property found, extract its name or bail on a typo
546 final int endName = value.indexOf('}', pos);
547 if (endName < 0) {
548 throw new CheckstyleException("Syntax error in property: "
549 + value);
550 }
551 final String propertyName = value.substring(pos + 2, endName);
552 fragments.add(null);
553 propertyRefs.add(propertyName);
554 prev = endName + 1;
555 }
556 }
557 //no more $ signs found
558 //if there is any tail to the file, append it
559 if (prev < value.length()) {
560 fragments.add(value.substring(prev));
561 }
562 }
563 }