001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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
222        final boolean isAllow = ALLOW_ELEMENT_NAME.equals(qName);
223        final boolean isLocalOnly = attributes.getValue("local-only") != null;
224        final String pkg = attributes.getValue(PKG_ATTRIBUTE_NAME);
225        final String module = attributes.getValue("module");
226        final boolean regex = containsRegexAttribute(attributes);
227        final AbstractImportRule rule;
228
229        if (module != null) {
230            rule = new ModuleImportRule(isAllow, isLocalOnly, module, regex);
231        }
232        else if (pkg != null) {
233            final boolean exactMatch = attributes.getValue("exact-match") != null;
234            rule = new PkgImportRule(isAllow, isLocalOnly, pkg, exactMatch, regex);
235        }
236        else {
237            final String clazz = safeGet(attributes, "class");
238            rule = new ClassImportRule(isAllow, isLocalOnly, clazz, regex);
239        }
240        return rule;
241    }
242
243    /**
244     * Check if the given attributes contain the regex attribute.
245     *
246     * @param attributes the attributes.
247     * @return if the regex attribute is contained.
248     */
249    private static boolean containsRegexAttribute(Attributes attributes) {
250        return attributes.getValue("regex") != null;
251    }
252
253    @Override
254    public void endElement(String namespaceUri, String localName,
255        String qName) {
256        if (SUBPACKAGE_ELEMENT_NAME.equals(qName) || FILE_ELEMENT_NAME.equals(qName)) {
257            stack.pop();
258        }
259    }
260
261    /**
262     * Loads the import control file from a file.
263     *
264     * @param uri the uri of the file to load.
265     * @return the root {@link PkgImportControl} object.
266     * @throws CheckstyleException if an error occurs.
267     */
268    public static PkgImportControl load(URI uri) throws CheckstyleException {
269        return loadUri(uri);
270    }
271
272    /**
273     * Loads the import control file from a {@link InputSource}.
274     *
275     * @param source the source to load from.
276     * @param uri uri of the source being loaded.
277     * @return the root {@link PkgImportControl} object.
278     * @throws CheckstyleException if an error occurs.
279     */
280    private static PkgImportControl load(InputSource source,
281        URI uri) throws CheckstyleException {
282        try {
283            final ImportControlLoader loader = new ImportControlLoader();
284            loader.parseInputSource(source);
285            return loader.getRoot();
286        }
287        catch (ParserConfigurationException | SAXException exc) {
288            throw new CheckstyleException("unable to parse " + uri, exc);
289        }
290        catch (IOException exc) {
291            throw new CheckstyleException("unable to read " + uri, exc);
292        }
293    }
294
295    /**
296     * Loads the import control file from a URI.
297     *
298     * @param uri the uri of the file to load.
299     * @return the root {@link PkgImportControl} object.
300     * @throws CheckstyleException if an error occurs.
301     */
302    private static PkgImportControl loadUri(URI uri) throws CheckstyleException {
303        try (InputStream inputStream = uri.toURL().openStream()) {
304            final InputSource source = new InputSource(inputStream);
305            return load(source, uri);
306        }
307        catch (MalformedURLException exc) {
308            throw new CheckstyleException("syntax error in url " + uri, exc);
309        }
310        catch (IOException exc) {
311            throw new CheckstyleException("unable to find " + uri, exc);
312        }
313    }
314
315    /**
316     * Returns root PkgImportControl.
317     *
318     * @return the root {@link PkgImportControl} object loaded.
319     */
320    private PkgImportControl getRoot() {
321        return (PkgImportControl) stack.peek();
322    }
323
324    /**
325     * Utility to get a strategyOnMismatch property for "import-control" tag.
326     *
327     * @param attributes collect to get attribute from.
328     * @return the value of the attribute.
329     */
330    private static MismatchStrategy getStrategyForImportControl(Attributes attributes) {
331        final String returnValue = attributes.getValue(STRATEGY_ON_MISMATCH_ATTRIBUTE_NAME);
332        MismatchStrategy strategyOnMismatch = MismatchStrategy.DISALLOWED;
333        if (STRATEGY_ON_MISMATCH_ALLOWED_VALUE.equals(returnValue)) {
334            strategyOnMismatch = MismatchStrategy.ALLOWED;
335        }
336        return strategyOnMismatch;
337    }
338
339    /**
340     * Utility to get a strategyOnMismatch property for "subpackage" tag.
341     *
342     * @param attributes collect to get attribute from.
343     * @return the value of the attribute.
344     */
345    private static MismatchStrategy getStrategyForSubpackage(Attributes attributes) {
346        final String returnValue = attributes.getValue(STRATEGY_ON_MISMATCH_ATTRIBUTE_NAME);
347        MismatchStrategy strategyOnMismatch = MismatchStrategy.DELEGATE_TO_PARENT;
348        if (STRATEGY_ON_MISMATCH_ALLOWED_VALUE.equals(returnValue)) {
349            strategyOnMismatch = MismatchStrategy.ALLOWED;
350        }
351        else if (STRATEGY_ON_MISMATCH_DISALLOWED_VALUE.equals(returnValue)) {
352            strategyOnMismatch = MismatchStrategy.DISALLOWED;
353        }
354        return strategyOnMismatch;
355    }
356
357    /**
358     * Utility to safely get an attribute. If it does not exist an exception
359     * is thrown.
360     *
361     * @param attributes collect to get attribute from.
362     * @param name name of the attribute to get.
363     * @return the value of the attribute.
364     * @throws SAXException if the attribute does not exist.
365     */
366    private static String safeGet(Attributes attributes, String name)
367            throws SAXException {
368        final String returnValue = attributes.getValue(name);
369        if (returnValue == null) {
370            // -@cs[IllegalInstantiation] SAXException is in the overridden method signature
371            // of the only method which calls the current one
372            throw new SAXException("missing attribute " + name);
373        }
374        return returnValue;
375    }
376
377}