View Javadoc
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 }