001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2024 the original author or authors.
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///////////////////////////////////////////////////////////////////////////////////////////////
019
020package com.puppycrawl.tools.checkstyle.filters;
021
022import java.io.FileNotFoundException;
023import java.io.IOException;
024import java.net.URI;
025import java.util.HashMap;
026import java.util.HashSet;
027import java.util.Locale;
028import java.util.Map;
029import java.util.Set;
030import java.util.regex.PatternSyntaxException;
031
032import javax.xml.parsers.ParserConfigurationException;
033
034import org.xml.sax.Attributes;
035import org.xml.sax.InputSource;
036import org.xml.sax.SAXException;
037
038import com.puppycrawl.tools.checkstyle.TreeWalkerFilter;
039import com.puppycrawl.tools.checkstyle.XmlLoader;
040import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
041import com.puppycrawl.tools.checkstyle.api.FilterSet;
042import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
043
044/**
045 * Loads a filter chain of suppressions.
046 */
047public final class SuppressionsLoader
048    extends XmlLoader {
049
050    /** The public ID for the configuration dtd. */
051    private static final String DTD_PUBLIC_ID_1_0 =
052        "-//Puppy Crawl//DTD Suppressions 1.0//EN";
053    /** The new public ID for version 1_0 configuration dtd. */
054    private static final String DTD_PUBLIC_CS_ID_1_0 =
055        "-//Checkstyle//DTD SuppressionFilter Configuration 1.0//EN";
056    /** The resource for the configuration dtd. */
057    private static final String DTD_SUPPRESSIONS_NAME_1_0 =
058        "com/puppycrawl/tools/checkstyle/suppressions_1_0.dtd";
059    /** The public ID for the configuration dtd. */
060    private static final String DTD_PUBLIC_ID_1_1 =
061        "-//Puppy Crawl//DTD Suppressions 1.1//EN";
062    /** The new public ID for version 1_1 configuration dtd. */
063    private static final String DTD_PUBLIC_CS_ID_1_1 =
064        "-//Checkstyle//DTD SuppressionFilter Configuration 1.1//EN";
065    /** The resource for the configuration dtd. */
066    private static final String DTD_SUPPRESSIONS_NAME_1_1 =
067        "com/puppycrawl/tools/checkstyle/suppressions_1_1.dtd";
068    /** The public ID for the configuration dtd. */
069    private static final String DTD_PUBLIC_ID_1_2 =
070        "-//Puppy Crawl//DTD Suppressions 1.2//EN";
071    /** The new public ID for version 1_2 configuration dtd. */
072    private static final String DTD_PUBLIC_CS_ID_1_2 =
073        "-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN";
074    /** The resource for the configuration dtd. */
075    private static final String DTD_SUPPRESSIONS_NAME_1_2 =
076        "com/puppycrawl/tools/checkstyle/suppressions_1_2.dtd";
077    /** The public ID for the configuration dtd. */
078    private static final String DTD_PUBLIC_ID_1_1_XPATH =
079        "-//Puppy Crawl//DTD Suppressions Xpath Experimental 1.1//EN";
080    /** The new public ID for version 1_1 configuration dtd. */
081    private static final String DTD_PUBLIC_CS_ID_1_1_XPATH =
082        "-//Checkstyle//DTD SuppressionXpathFilter Experimental Configuration 1.1//EN";
083    /** The resource for the configuration dtd. */
084    private static final String DTD_SUPPRESSIONS_NAME_1_1_XPATH =
085        "com/puppycrawl/tools/checkstyle/suppressions_1_1_xpath_experimental.dtd";
086    /** The public ID for the configuration dtd. */
087    private static final String DTD_PUBLIC_ID_1_2_XPATH =
088        "-//Puppy Crawl//DTD Suppressions Xpath Experimental 1.2//EN";
089    /** The new public ID for version 1_2 configuration dtd. */
090    private static final String DTD_PUBLIC_CS_ID_1_2_XPATH =
091        "-//Checkstyle//DTD SuppressionXpathFilter Experimental Configuration 1.2//EN";
092    /** The resource for the configuration dtd. */
093    private static final String DTD_SUPPRESSIONS_NAME_1_2_XPATH =
094        "com/puppycrawl/tools/checkstyle/suppressions_1_2_xpath_experimental.dtd";
095    /** File search error message. **/
096    private static final String UNABLE_TO_FIND_ERROR_MESSAGE = "Unable to find: ";
097    /** String literal for attribute name. **/
098    private static final String ATTRIBUTE_NAME_FILES = "files";
099    /** String literal for attribute name. **/
100    private static final String ATTRIBUTE_NAME_CHECKS = "checks";
101    /** String literal for attribute name. **/
102    private static final String ATTRIBUTE_NAME_MESSAGE = "message";
103    /** String literal for attribute name. **/
104    private static final String ATTRIBUTE_NAME_ID = "id";
105    /** String literal for attribute name. **/
106    private static final String ATTRIBUTE_NAME_QUERY = "query";
107    /** String literal for attribute name. **/
108    private static final String ATTRIBUTE_NAME_LINES = "lines";
109    /** String literal for attribute name. **/
110    private static final String ATTRIBUTE_NAME_COLUMNS = "columns";
111
112    /**
113     * The filter chain to return in getAFilterChain(),
114     * configured during parsing.
115     */
116    private final FilterSet filterChain = new FilterSet();
117
118    /**
119     * The set of the {@code TreeWalkerFilter} filters. Being filled during parsing.
120     */
121    private final Set<TreeWalkerFilter> treeWalkerFilters = new HashSet<>();
122
123    /**
124     * Creates a new {@code SuppressionsLoader} instance.
125     *
126     * @throws ParserConfigurationException if an error occurs
127     * @throws SAXException if an error occurs
128     */
129    private SuppressionsLoader()
130            throws ParserConfigurationException, SAXException {
131        super(createIdToResourceNameMap());
132    }
133
134    @Override
135    public void startElement(String namespaceUri,
136                             String localName,
137                             String qName,
138                             Attributes attributes)
139            throws SAXException {
140        if ("suppress".equals(qName)) {
141            // add SuppressFilterElement filter to the filter chain
142            final SuppressFilterElement suppress = getSuppressElement(attributes);
143            filterChain.addFilter(suppress);
144        }
145        else if ("suppress-xpath".equals(qName)) {
146            final XpathFilterElement filter = getXpathFilter(attributes);
147            treeWalkerFilters.add(filter);
148        }
149    }
150
151    /**
152     * Returns the suppress element, initialized from given attributes.
153     *
154     * @param attributes the attributes of xml-tag "&lt;suppress&gt;&lt;/suppress&gt;",
155     *                   specified inside suppression file.
156     * @return the suppress element
157     * @throws SAXException if an error occurs.
158     */
159    private static SuppressFilterElement getSuppressElement(Attributes attributes)
160            throws SAXException {
161        final String checks = attributes.getValue(ATTRIBUTE_NAME_CHECKS);
162        final String modId = attributes.getValue(ATTRIBUTE_NAME_ID);
163        final String message = attributes.getValue(ATTRIBUTE_NAME_MESSAGE);
164        if (checks == null && modId == null && message == null) {
165            // -@cs[IllegalInstantiation] SAXException is in the overridden method signature
166            throw new SAXException("missing checks or id or message attribute");
167        }
168        final SuppressFilterElement suppress;
169        try {
170            final String files = attributes.getValue(ATTRIBUTE_NAME_FILES);
171            final String lines = attributes.getValue(ATTRIBUTE_NAME_LINES);
172            final String columns = attributes.getValue(ATTRIBUTE_NAME_COLUMNS);
173            suppress = new SuppressFilterElement(files, checks, message, modId, lines, columns);
174        }
175        catch (final PatternSyntaxException ex) {
176            // -@cs[IllegalInstantiation] SAXException is in the overridden method signature
177            throw new SAXException("invalid files or checks or message format", ex);
178        }
179        return suppress;
180    }
181
182    /**
183     * Returns the xpath filter, initialized from given attributes.
184     *
185     * @param attributes the attributes of xml-tag "&lt;suppress-xpath&gt;&lt;/suppress-xpath&gt;",
186     *                   specified inside suppression file.
187     * @return the xpath filter
188     * @throws SAXException if an error occurs.
189     */
190    private static XpathFilterElement getXpathFilter(Attributes attributes) throws SAXException {
191        final String checks = attributes.getValue(ATTRIBUTE_NAME_CHECKS);
192        final String modId = attributes.getValue(ATTRIBUTE_NAME_ID);
193        final String message = attributes.getValue(ATTRIBUTE_NAME_MESSAGE);
194        if (checks == null && modId == null && message == null) {
195            // -@cs[IllegalInstantiation] SAXException is in the overridden method signature
196            throw new SAXException("missing checks or id or message attribute for suppress-xpath");
197        }
198        final XpathFilterElement filter;
199        try {
200            final String files = attributes.getValue(ATTRIBUTE_NAME_FILES);
201            final String xpathQuery = attributes.getValue(ATTRIBUTE_NAME_QUERY);
202            filter = new XpathFilterElement(files, checks, message, modId, xpathQuery);
203        }
204        catch (final PatternSyntaxException ex) {
205            // -@cs[IllegalInstantiation] SAXException is in the overridden method signature
206            throw new SAXException("invalid files or checks or message format for suppress-xpath",
207                    ex);
208        }
209        return filter;
210    }
211
212    /**
213     * Returns the suppression filters in a specified file.
214     *
215     * @param filename name of the suppressions file.
216     * @return the filter chain of suppression elements specified in the file.
217     * @throws CheckstyleException if an error occurs.
218     */
219    public static FilterSet loadSuppressions(String filename)
220            throws CheckstyleException {
221        // figure out if this is a File or a URL
222        final URI uri = CommonUtil.getUriByFilename(filename);
223        final InputSource source = new InputSource(uri.toString());
224        return loadSuppressions(source, filename);
225    }
226
227    /**
228     * Returns the suppression filters in a specified source.
229     *
230     * @param source the source for the suppressions.
231     * @param sourceName the name of the source.
232     * @return the filter chain of suppression elements in source.
233     * @throws CheckstyleException if an error occurs.
234     */
235    private static FilterSet loadSuppressions(
236            InputSource source, String sourceName)
237            throws CheckstyleException {
238        return getSuppressionLoader(source, sourceName).filterChain;
239    }
240
241    /**
242     * Returns the suppression {@code TreeWalker} filters in a specified file.
243     *
244     * @param filename name of the suppressions file.
245     * @return the set of xpath suppression elements specified in the file.
246     * @throws CheckstyleException if an error occurs.
247     */
248    public static Set<TreeWalkerFilter> loadXpathSuppressions(String filename)
249            throws CheckstyleException {
250        // figure out if this is a File or a URL
251        final URI uri = CommonUtil.getUriByFilename(filename);
252        final InputSource source = new InputSource(uri.toString());
253        return loadXpathSuppressions(source, filename);
254    }
255
256    /**
257     * Returns the suppression {@code TreeWalker} filters in a specified source.
258     *
259     * @param source the source for the suppressions.
260     * @param sourceName the name of the source.
261     * @return the set of xpath suppression elements specified in source.
262     * @throws CheckstyleException if an error occurs.
263     */
264    private static Set<TreeWalkerFilter> loadXpathSuppressions(
265            InputSource source, String sourceName)
266            throws CheckstyleException {
267        return getSuppressionLoader(source, sourceName).treeWalkerFilters;
268    }
269
270    /**
271     * Parses specified source and returns the suppression loader.
272     *
273     * @param source the source for the suppressions.
274     * @param sourceName the name of the source.
275     * @return the suppression loader
276     * @throws CheckstyleException if an error occurs.
277     */
278    private static SuppressionsLoader getSuppressionLoader(InputSource source, String sourceName)
279            throws CheckstyleException {
280        try {
281            final SuppressionsLoader suppressionsLoader =
282                new SuppressionsLoader();
283            suppressionsLoader.parseInputSource(source);
284            return suppressionsLoader;
285        }
286        catch (final FileNotFoundException ex) {
287            throw new CheckstyleException(UNABLE_TO_FIND_ERROR_MESSAGE + sourceName, ex);
288        }
289        catch (final ParserConfigurationException | SAXException ex) {
290            final String message = String.format(Locale.ROOT, "Unable to parse %s - %s",
291                    sourceName, ex.getMessage());
292            throw new CheckstyleException(message, ex);
293        }
294        catch (final IOException ex) {
295            throw new CheckstyleException("Unable to read " + sourceName, ex);
296        }
297        catch (final NumberFormatException ex) {
298            final String message = String.format(Locale.ROOT, "Number format exception %s - %s",
299                    sourceName, ex.getMessage());
300            throw new CheckstyleException(message, ex);
301        }
302    }
303
304    /**
305     * Creates mapping between local resources and dtd ids.
306     *
307     * @return map between local resources and dtd ids.
308     */
309    private static Map<String, String> createIdToResourceNameMap() {
310        final Map<String, String> map = new HashMap<>();
311        map.put(DTD_PUBLIC_ID_1_0, DTD_SUPPRESSIONS_NAME_1_0);
312        map.put(DTD_PUBLIC_ID_1_1, DTD_SUPPRESSIONS_NAME_1_1);
313        map.put(DTD_PUBLIC_ID_1_2, DTD_SUPPRESSIONS_NAME_1_2);
314        map.put(DTD_PUBLIC_ID_1_1_XPATH, DTD_SUPPRESSIONS_NAME_1_1_XPATH);
315        map.put(DTD_PUBLIC_ID_1_2_XPATH, DTD_SUPPRESSIONS_NAME_1_2_XPATH);
316        map.put(DTD_PUBLIC_CS_ID_1_0, DTD_SUPPRESSIONS_NAME_1_0);
317        map.put(DTD_PUBLIC_CS_ID_1_1, DTD_SUPPRESSIONS_NAME_1_1);
318        map.put(DTD_PUBLIC_CS_ID_1_2, DTD_SUPPRESSIONS_NAME_1_2);
319        map.put(DTD_PUBLIC_CS_ID_1_1_XPATH, DTD_SUPPRESSIONS_NAME_1_1_XPATH);
320        map.put(DTD_PUBLIC_CS_ID_1_2_XPATH, DTD_SUPPRESSIONS_NAME_1_2_XPATH);
321        return map;
322    }
323
324}