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