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