001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2014  Oliver Burn
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////////////////////////////////////////////////////////////////////////////////
019package com.puppycrawl.tools.checkstyle;
020
021import com.google.common.collect.Lists;
022import com.google.common.collect.Maps;
023import com.puppycrawl.tools.checkstyle.api.AbstractLoader;
024import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
025import com.puppycrawl.tools.checkstyle.api.Configuration;
026import com.puppycrawl.tools.checkstyle.api.FastStack;
027import com.puppycrawl.tools.checkstyle.api.SeverityLevel;
028import org.xml.sax.Attributes;
029import org.xml.sax.InputSource;
030import org.xml.sax.SAXException;
031import org.xml.sax.SAXParseException;
032
033import javax.xml.parsers.ParserConfigurationException;
034import java.io.File;
035import java.io.FileNotFoundException;
036import java.io.IOException;
037import java.io.InputStream;
038import java.net.MalformedURLException;
039import java.net.URI;
040import java.net.URISyntaxException;
041import java.net.URL;
042import java.util.Iterator;
043import java.util.List;
044import java.util.Map;
045
046/**
047 * Loads a configuration from a standard configuration XML file.
048 *
049 * @author Oliver Burn
050 * @version 1.0
051 */
052public final class ConfigurationLoader
053{
054    /** the public ID for version 1_0 of the configuration dtd */
055    private static final String DTD_PUBLIC_ID_1_0 =
056        "-//Puppy Crawl//DTD Check Configuration 1.0//EN";
057
058    /** the resource for version 1_0 of the configuration dtd */
059    private static final String DTD_RESOURCE_NAME_1_0 =
060        "com/puppycrawl/tools/checkstyle/configuration_1_0.dtd";
061
062    /** the public ID for version 1_1 of the configuration dtd */
063    private static final String DTD_PUBLIC_ID_1_1 =
064        "-//Puppy Crawl//DTD Check Configuration 1.1//EN";
065
066    /** the resource for version 1_1 of the configuration dtd */
067    private static final String DTD_RESOURCE_NAME_1_1 =
068        "com/puppycrawl/tools/checkstyle/configuration_1_1.dtd";
069
070    /** the public ID for version 1_2 of the configuration dtd */
071    private static final String DTD_PUBLIC_ID_1_2 =
072        "-//Puppy Crawl//DTD Check Configuration 1.2//EN";
073
074    /** the resource for version 1_2 of the configuration dtd */
075    private static final String DTD_RESOURCE_NAME_1_2 =
076        "com/puppycrawl/tools/checkstyle/configuration_1_2.dtd";
077
078    /** the public ID for version 1_3 of the configuration dtd */
079    private static final String DTD_PUBLIC_ID_1_3 =
080        "-//Puppy Crawl//DTD Check Configuration 1.3//EN";
081
082    /** the resource for version 1_3 of the configuration dtd */
083    private static final String DTD_RESOURCE_NAME_1_3 =
084        "com/puppycrawl/tools/checkstyle/configuration_1_3.dtd";
085
086    /**
087     * Implements the SAX document handler interfaces, so they do not
088     * appear in the public API of the ConfigurationLoader.
089     */
090    private final class InternalLoader
091        extends AbstractLoader
092    {
093        /** module elements */
094        private static final String MODULE = "module";
095        /** name attribute */
096        private static final String NAME = "name";
097        /** property element */
098        private static final String PROPERTY = "property";
099        /** 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}