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.checks.imports;
021
022import java.io.IOException;
023import java.io.InputStream;
024import java.net.MalformedURLException;
025import java.net.URI;
026import java.util.ArrayDeque;
027import java.util.Deque;
028import java.util.HashMap;
029import java.util.Map;
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.XmlLoader;
038import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
039
040/**
041 * Responsible for loading the contents of an import control configuration file.
042 */
043public final class ImportControlLoader extends XmlLoader {
044
045    /** The public ID for the configuration dtd. */
046    private static final String DTD_PUBLIC_ID_1_0 =
047        "-//Puppy Crawl//DTD Import Control 1.0//EN";
048
049    /** The new public ID for version 1_0 of the configuration dtd. */
050    private static final String DTD_PUBLIC_CS_ID_1_0 =
051        "-//Checkstyle//DTD ImportControl Configuration 1.0//EN";
052
053    /** The public ID for the configuration dtd. */
054    private static final String DTD_PUBLIC_ID_1_1 =
055        "-//Puppy Crawl//DTD Import Control 1.1//EN";
056
057    /** The new public ID for version 1_1 of the configuration dtd. */
058    private static final String DTD_PUBLIC_CS_ID_1_1 =
059        "-//Checkstyle//DTD ImportControl Configuration 1.1//EN";
060
061    /** The public ID for the configuration dtd. */
062    private static final String DTD_PUBLIC_ID_1_2 =
063        "-//Puppy Crawl//DTD Import Control 1.2//EN";
064
065    /** The new public ID for version 1_2 of the configuration dtd. */
066    private static final String DTD_PUBLIC_CS_ID_1_2 =
067        "-//Checkstyle//DTD ImportControl Configuration 1.2//EN";
068
069    /** The public ID for the configuration dtd. */
070    private static final String DTD_PUBLIC_ID_1_3 =
071        "-//Puppy Crawl//DTD Import Control 1.3//EN";
072
073    /** The new public ID for version 1_3 of the configuration dtd. */
074    private static final String DTD_PUBLIC_CS_ID_1_3 =
075        "-//Checkstyle//DTD ImportControl Configuration 1.3//EN";
076
077    /** The public ID for the configuration dtd. */
078    private static final String DTD_PUBLIC_ID_1_4 =
079        "-//Puppy Crawl//DTD Import Control 1.4//EN";
080
081    /** The new public ID for version 1_4 of the configuration dtd. */
082    private static final String DTD_PUBLIC_CS_ID_1_4 =
083        "-//Checkstyle//DTD ImportControl Configuration 1.4//EN";
084
085    /** The resource for the configuration dtd. */
086    private static final String DTD_RESOURCE_NAME_1_0 =
087        "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_0.dtd";
088
089    /** The resource for the configuration dtd. */
090    private static final String DTD_RESOURCE_NAME_1_1 =
091        "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_1.dtd";
092
093    /** The resource for the configuration dtd. */
094    private static final String DTD_RESOURCE_NAME_1_2 =
095        "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_2.dtd";
096
097    /** The resource for the configuration dtd. */
098    private static final String DTD_RESOURCE_NAME_1_3 =
099        "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_3.dtd";
100
101    /** The resource for the configuration dtd. */
102    private static final String DTD_RESOURCE_NAME_1_4 =
103        "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_4.dtd";
104
105    /** The map to look up the resource name by the id. */
106    private static final Map<String, String> DTD_RESOURCE_BY_ID = new HashMap<>();
107
108    /** Name for attribute 'pkg'. */
109    private static final String PKG_ATTRIBUTE_NAME = "pkg";
110
111    /** Name for attribute 'name'. */
112    private static final String NAME_ATTRIBUTE_NAME = "name";
113
114    /** Name for attribute 'strategyOnMismatch'. */
115    private static final String STRATEGY_ON_MISMATCH_ATTRIBUTE_NAME = "strategyOnMismatch";
116
117    /** Value "allowed" for attribute 'strategyOnMismatch'. */
118    private static final String STRATEGY_ON_MISMATCH_ALLOWED_VALUE = "allowed";
119
120    /** Value "disallowed" for attribute 'strategyOnMismatch'. */
121    private static final String STRATEGY_ON_MISMATCH_DISALLOWED_VALUE = "disallowed";
122
123    /** Qualified name for element 'subpackage'. */
124    private static final String SUBPACKAGE_ELEMENT_NAME = "subpackage";
125
126    /** Qualified name for element 'file'. */
127    private static final String FILE_ELEMENT_NAME = "file";
128
129    /** Qualified name for element 'allow'. */
130    private static final String ALLOW_ELEMENT_NAME = "allow";
131
132    /** Used to hold the {@link AbstractImportControl} objects. */
133    private final Deque<AbstractImportControl> stack = new ArrayDeque<>();
134
135    static {
136        DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_ID_1_0, DTD_RESOURCE_NAME_1_0);
137        DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_ID_1_1, DTD_RESOURCE_NAME_1_1);
138        DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_ID_1_2, DTD_RESOURCE_NAME_1_2);
139        DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_ID_1_3, DTD_RESOURCE_NAME_1_3);
140        DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_ID_1_4, DTD_RESOURCE_NAME_1_4);
141        DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_CS_ID_1_0, DTD_RESOURCE_NAME_1_0);
142        DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_CS_ID_1_1, DTD_RESOURCE_NAME_1_1);
143        DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_CS_ID_1_2, DTD_RESOURCE_NAME_1_2);
144        DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_CS_ID_1_3, DTD_RESOURCE_NAME_1_3);
145        DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_CS_ID_1_4, DTD_RESOURCE_NAME_1_4);
146    }
147
148    /**
149     * Constructs an instance.
150     *
151     * @throws ParserConfigurationException if an error occurs.
152     * @throws SAXException if an error occurs.
153     */
154    private ImportControlLoader() throws ParserConfigurationException,
155            SAXException {
156        super(DTD_RESOURCE_BY_ID);
157    }
158
159    @Override
160    public void startElement(String namespaceUri,
161                             String localName,
162                             String qName,
163                             Attributes attributes)
164            throws SAXException {
165        if ("import-control".equals(qName)) {
166            final String pkg = safeGet(attributes, PKG_ATTRIBUTE_NAME);
167            final MismatchStrategy strategyOnMismatch = getStrategyForImportControl(attributes);
168            final boolean regex = containsRegexAttribute(attributes);
169            stack.push(new PkgImportControl(pkg, regex, strategyOnMismatch));
170        }
171        else if (SUBPACKAGE_ELEMENT_NAME.equals(qName)) {
172            final String name = safeGet(attributes, NAME_ATTRIBUTE_NAME);
173            final MismatchStrategy strategyOnMismatch = getStrategyForSubpackage(attributes);
174            final boolean regex = containsRegexAttribute(attributes);
175            final PkgImportControl parentImportControl = (PkgImportControl) stack.peek();
176            final AbstractImportControl importControl = new PkgImportControl(parentImportControl,
177                    name, regex, strategyOnMismatch);
178            parentImportControl.addChild(importControl);
179            stack.push(importControl);
180        }
181        else if (FILE_ELEMENT_NAME.equals(qName)) {
182            final String name = safeGet(attributes, NAME_ATTRIBUTE_NAME);
183            final boolean regex = containsRegexAttribute(attributes);
184            final PkgImportControl parentImportControl = (PkgImportControl) stack.peek();
185            final AbstractImportControl importControl = new FileImportControl(parentImportControl,
186                    name, regex);
187            parentImportControl.addChild(importControl);
188            stack.push(importControl);
189        }
190        else {
191            final AbstractImportRule rule = createImportRule(qName, attributes);
192            stack.peek().addImportRule(rule);
193        }
194    }
195
196    /**
197     * Constructs an instance of an import rule based on the given {@code name} and
198     * {@code attributes}.
199     *
200     * @param qName The qualified name.
201     * @param attributes The attributes attached to the element.
202     * @return The created import rule.
203     * @throws SAXException if an error occurs.
204     */
205    private static AbstractImportRule createImportRule(String qName, Attributes attributes)
206            throws SAXException {
207        // Need to handle either "pkg" or "class" attribute.
208        // May have "exact-match" for "pkg"
209        // May have "local-only"
210        final boolean isAllow = ALLOW_ELEMENT_NAME.equals(qName);
211        final boolean isLocalOnly = attributes.getValue("local-only") != null;
212        final String pkg = attributes.getValue(PKG_ATTRIBUTE_NAME);
213        final boolean regex = containsRegexAttribute(attributes);
214        final AbstractImportRule rule;
215        if (pkg == null) {
216            // handle class names which can be normal class names or regular
217            // expressions
218            final String clazz = safeGet(attributes, "class");
219            rule = new ClassImportRule(isAllow, isLocalOnly, clazz, regex);
220        }
221        else {
222            final boolean exactMatch =
223                    attributes.getValue("exact-match") != null;
224            rule = new PkgImportRule(isAllow, isLocalOnly, pkg, exactMatch, regex);
225        }
226        return rule;
227    }
228
229    /**
230     * Check if the given attributes contain the regex attribute.
231     *
232     * @param attributes the attributes.
233     * @return if the regex attribute is contained.
234     */
235    private static boolean containsRegexAttribute(Attributes attributes) {
236        return attributes.getValue("regex") != null;
237    }
238
239    @Override
240    public void endElement(String namespaceUri, String localName,
241        String qName) {
242        if (SUBPACKAGE_ELEMENT_NAME.equals(qName) || FILE_ELEMENT_NAME.equals(qName)) {
243            stack.pop();
244        }
245    }
246
247    /**
248     * Loads the import control file from a file.
249     *
250     * @param uri the uri of the file to load.
251     * @return the root {@link PkgImportControl} object.
252     * @throws CheckstyleException if an error occurs.
253     */
254    public static PkgImportControl load(URI uri) throws CheckstyleException {
255        return loadUri(uri);
256    }
257
258    /**
259     * Loads the import control file from a {@link InputSource}.
260     *
261     * @param source the source to load from.
262     * @param uri uri of the source being loaded.
263     * @return the root {@link PkgImportControl} object.
264     * @throws CheckstyleException if an error occurs.
265     */
266    private static PkgImportControl load(InputSource source,
267        URI uri) throws CheckstyleException {
268        try {
269            final ImportControlLoader loader = new ImportControlLoader();
270            loader.parseInputSource(source);
271            return loader.getRoot();
272        }
273        catch (ParserConfigurationException | SAXException ex) {
274            throw new CheckstyleException("unable to parse " + uri
275                    + " - " + ex.getMessage(), ex);
276        }
277        catch (IOException ex) {
278            throw new CheckstyleException("unable to read " + uri, ex);
279        }
280    }
281
282    /**
283     * Loads the import control file from a URI.
284     *
285     * @param uri the uri of the file to load.
286     * @return the root {@link PkgImportControl} object.
287     * @throws CheckstyleException if an error occurs.
288     */
289    private static PkgImportControl loadUri(URI uri) throws CheckstyleException {
290        try (InputStream inputStream = uri.toURL().openStream()) {
291            final InputSource source = new InputSource(inputStream);
292            return load(source, uri);
293        }
294        catch (MalformedURLException ex) {
295            throw new CheckstyleException("syntax error in url " + uri, ex);
296        }
297        catch (IOException ex) {
298            throw new CheckstyleException("unable to find " + uri, ex);
299        }
300    }
301
302    /**
303     * Returns root PkgImportControl.
304     *
305     * @return the root {@link PkgImportControl} object loaded.
306     */
307    private PkgImportControl getRoot() {
308        return (PkgImportControl) stack.peek();
309    }
310
311    /**
312     * Utility to get a strategyOnMismatch property for "import-control" tag.
313     *
314     * @param attributes collect to get attribute from.
315     * @return the value of the attribute.
316     */
317    private static MismatchStrategy getStrategyForImportControl(Attributes attributes) {
318        final String returnValue = attributes.getValue(STRATEGY_ON_MISMATCH_ATTRIBUTE_NAME);
319        MismatchStrategy strategyOnMismatch = MismatchStrategy.DISALLOWED;
320        if (STRATEGY_ON_MISMATCH_ALLOWED_VALUE.equals(returnValue)) {
321            strategyOnMismatch = MismatchStrategy.ALLOWED;
322        }
323        return strategyOnMismatch;
324    }
325
326    /**
327     * Utility to get a strategyOnMismatch property for "subpackage" tag.
328     *
329     * @param attributes collect to get attribute from.
330     * @return the value of the attribute.
331     */
332    private static MismatchStrategy getStrategyForSubpackage(Attributes attributes) {
333        final String returnValue = attributes.getValue(STRATEGY_ON_MISMATCH_ATTRIBUTE_NAME);
334        MismatchStrategy strategyOnMismatch = MismatchStrategy.DELEGATE_TO_PARENT;
335        if (STRATEGY_ON_MISMATCH_ALLOWED_VALUE.equals(returnValue)) {
336            strategyOnMismatch = MismatchStrategy.ALLOWED;
337        }
338        else if (STRATEGY_ON_MISMATCH_DISALLOWED_VALUE.equals(returnValue)) {
339            strategyOnMismatch = MismatchStrategy.DISALLOWED;
340        }
341        return strategyOnMismatch;
342    }
343
344    /**
345     * Utility to safely get an attribute. If it does not exist an exception
346     * is thrown.
347     *
348     * @param attributes collect to get attribute from.
349     * @param name name of the attribute to get.
350     * @return the value of the attribute.
351     * @throws SAXException if the attribute does not exist.
352     */
353    private static String safeGet(Attributes attributes, String name)
354            throws SAXException {
355        final String returnValue = attributes.getValue(name);
356        if (returnValue == null) {
357            // -@cs[IllegalInstantiation] SAXException is in the overridden method signature
358            // of the only method which calls the current one
359            throw new SAXException("missing attribute " + name);
360        }
361        return returnValue;
362    }
363
364}