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