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
289                    + " - " + exc.getMessage(), exc);
290        }
291        catch (IOException exc) {
292            throw new CheckstyleException("unable to read " + uri, exc);
293        }
294    }
295
296    /**
297     * Loads the import control file from a URI.
298     *
299     * @param uri the uri of the file to load.
300     * @return the root {@link PkgImportControl} object.
301     * @throws CheckstyleException if an error occurs.
302     */
303    private static PkgImportControl loadUri(URI uri) throws CheckstyleException {
304        try (InputStream inputStream = uri.toURL().openStream()) {
305            final InputSource source = new InputSource(inputStream);
306            return load(source, uri);
307        }
308        catch (MalformedURLException exc) {
309            throw new CheckstyleException("syntax error in url " + uri, exc);
310        }
311        catch (IOException exc) {
312            throw new CheckstyleException("unable to find " + uri, exc);
313        }
314    }
315
316    /**
317     * Returns root PkgImportControl.
318     *
319     * @return the root {@link PkgImportControl} object loaded.
320     */
321    private PkgImportControl getRoot() {
322        return (PkgImportControl) stack.peek();
323    }
324
325    /**
326     * Utility to get a strategyOnMismatch property for "import-control" tag.
327     *
328     * @param attributes collect to get attribute from.
329     * @return the value of the attribute.
330     */
331    private static MismatchStrategy getStrategyForImportControl(Attributes attributes) {
332        final String returnValue = attributes.getValue(STRATEGY_ON_MISMATCH_ATTRIBUTE_NAME);
333        MismatchStrategy strategyOnMismatch = MismatchStrategy.DISALLOWED;
334        if (STRATEGY_ON_MISMATCH_ALLOWED_VALUE.equals(returnValue)) {
335            strategyOnMismatch = MismatchStrategy.ALLOWED;
336        }
337        return strategyOnMismatch;
338    }
339
340    /**
341     * Utility to get a strategyOnMismatch property for "subpackage" tag.
342     *
343     * @param attributes collect to get attribute from.
344     * @return the value of the attribute.
345     */
346    private static MismatchStrategy getStrategyForSubpackage(Attributes attributes) {
347        final String returnValue = attributes.getValue(STRATEGY_ON_MISMATCH_ATTRIBUTE_NAME);
348        MismatchStrategy strategyOnMismatch = MismatchStrategy.DELEGATE_TO_PARENT;
349        if (STRATEGY_ON_MISMATCH_ALLOWED_VALUE.equals(returnValue)) {
350            strategyOnMismatch = MismatchStrategy.ALLOWED;
351        }
352        else if (STRATEGY_ON_MISMATCH_DISALLOWED_VALUE.equals(returnValue)) {
353            strategyOnMismatch = MismatchStrategy.DISALLOWED;
354        }
355        return strategyOnMismatch;
356    }
357
358    /**
359     * Utility to safely get an attribute. If it does not exist an exception
360     * is thrown.
361     *
362     * @param attributes collect to get attribute from.
363     * @param name name of the attribute to get.
364     * @return the value of the attribute.
365     * @throws SAXException if the attribute does not exist.
366     */
367    private static String safeGet(Attributes attributes, String name)
368            throws SAXException {
369        final String returnValue = attributes.getValue(name);
370        if (returnValue == null) {
371            // -@cs[IllegalInstantiation] SAXException is in the overridden method signature
372            // of the only method which calls the current one
373            throw new SAXException("missing attribute " + name);
374        }
375        return returnValue;
376    }
377
378}