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