001////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code for adherence to a set of rules.
003// Copyright (C) 2001-2020 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 lookup 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     * @throws ParserConfigurationException if an error occurs.
151     * @throws SAXException if an error occurs.
152     */
153    private ImportControlLoader() throws ParserConfigurationException,
154            SAXException {
155        super(DTD_RESOURCE_BY_ID);
156    }
157
158    @Override
159    public void startElement(String namespaceUri,
160                             String localName,
161                             String qName,
162                             Attributes attributes)
163            throws SAXException {
164        if ("import-control".equals(qName)) {
165            final String pkg = safeGet(attributes, PKG_ATTRIBUTE_NAME);
166            final MismatchStrategy strategyOnMismatch = getStrategyForImportControl(attributes);
167            final boolean regex = containsRegexAttribute(attributes);
168            stack.push(new PkgImportControl(pkg, regex, strategyOnMismatch));
169        }
170        else if (SUBPACKAGE_ELEMENT_NAME.equals(qName)) {
171            final String name = safeGet(attributes, NAME_ATTRIBUTE_NAME);
172            final MismatchStrategy strategyOnMismatch = getStrategyForSubpackage(attributes);
173            final boolean regex = containsRegexAttribute(attributes);
174            final PkgImportControl parentImportControl = (PkgImportControl) stack.peek();
175            final AbstractImportControl importControl = new PkgImportControl(parentImportControl,
176                    name, regex, strategyOnMismatch);
177            parentImportControl.addChild(importControl);
178            stack.push(importControl);
179        }
180        else if (FILE_ELEMENT_NAME.equals(qName)) {
181            final String name = safeGet(attributes, NAME_ATTRIBUTE_NAME);
182            final boolean regex = containsRegexAttribute(attributes);
183            final PkgImportControl parentImportControl = (PkgImportControl) stack.peek();
184            final AbstractImportControl importControl = new FileImportControl(parentImportControl,
185                    name, regex);
186            parentImportControl.addChild(importControl);
187            stack.push(importControl);
188        }
189        else if (ALLOW_ELEMENT_NAME.equals(qName) || "disallow".equals(qName)) {
190            final AbstractImportRule rule = createImportRule(qName, attributes);
191            stack.peek().addImportRule(rule);
192        }
193    }
194
195    /**
196     * Constructs an instance of an import rule based on the given {@code name} and
197     * {@code attributes}.
198     * @param qName The qualified name.
199     * @param attributes The attributes attached to the element.
200     * @return The created import rule.
201     * @throws SAXException if an error occurs.
202     */
203    private static AbstractImportRule createImportRule(String qName, Attributes attributes)
204            throws SAXException {
205        // Need to handle either "pkg" or "class" attribute.
206        // May have "exact-match" for "pkg"
207        // May have "local-only"
208        final boolean isAllow = ALLOW_ELEMENT_NAME.equals(qName);
209        final boolean isLocalOnly = attributes.getValue("local-only") != null;
210        final String pkg = attributes.getValue(PKG_ATTRIBUTE_NAME);
211        final boolean regex = containsRegexAttribute(attributes);
212        final AbstractImportRule rule;
213        if (pkg == null) {
214            // handle class names which can be normal class names or regular
215            // expressions
216            final String clazz = safeGet(attributes, "class");
217            rule = new ClassImportRule(isAllow, isLocalOnly, clazz, regex);
218        }
219        else {
220            final boolean exactMatch =
221                    attributes.getValue("exact-match") != null;
222            rule = new PkgImportRule(isAllow, isLocalOnly, pkg, exactMatch, regex);
223        }
224        return rule;
225    }
226
227    /**
228     * Check if the given attributes contain the regex attribute.
229     * @param attributes the attributes.
230     * @return if the regex attribute is contained.
231     */
232    private static boolean containsRegexAttribute(Attributes attributes) {
233        return attributes.getValue("regex") != null;
234    }
235
236    @Override
237    public void endElement(String namespaceUri, String localName,
238        String qName) {
239        if (SUBPACKAGE_ELEMENT_NAME.equals(qName) || FILE_ELEMENT_NAME.equals(qName)) {
240            stack.pop();
241        }
242    }
243
244    /**
245     * Loads the import control file from a file.
246     * @param uri the uri of the file to load.
247     * @return the root {@link PkgImportControl} object.
248     * @throws CheckstyleException if an error occurs.
249     */
250    public static PkgImportControl load(URI uri) throws CheckstyleException {
251        return loadUri(uri);
252    }
253
254    /**
255     * Loads the import control file from a {@link InputSource}.
256     * @param source the source to load from.
257     * @param uri uri of the source being loaded.
258     * @return the root {@link PkgImportControl} object.
259     * @throws CheckstyleException if an error occurs.
260     */
261    private static PkgImportControl load(InputSource source,
262        URI uri) throws CheckstyleException {
263        try {
264            final ImportControlLoader loader = new ImportControlLoader();
265            loader.parseInputSource(source);
266            return loader.getRoot();
267        }
268        catch (ParserConfigurationException | SAXException ex) {
269            throw new CheckstyleException("unable to parse " + uri
270                    + " - " + ex.getMessage(), ex);
271        }
272        catch (IOException ex) {
273            throw new CheckstyleException("unable to read " + uri, ex);
274        }
275    }
276
277    /**
278     * Loads the import control file from a URI.
279     * @param uri the uri of the file to load.
280     * @return the root {@link PkgImportControl} object.
281     * @throws CheckstyleException if an error occurs.
282     */
283    private static PkgImportControl loadUri(URI uri) throws CheckstyleException {
284        try (InputStream inputStream = uri.toURL().openStream()) {
285            final InputSource source = new InputSource(inputStream);
286            return load(source, uri);
287        }
288        catch (MalformedURLException ex) {
289            throw new CheckstyleException("syntax error in url " + uri, ex);
290        }
291        catch (IOException ex) {
292            throw new CheckstyleException("unable to find " + uri, ex);
293        }
294    }
295
296    /**
297     * Returns root PkgImportControl.
298     * @return the root {@link PkgImportControl} object loaded.
299     */
300    private PkgImportControl getRoot() {
301        return (PkgImportControl) stack.peek();
302    }
303
304    /**
305     * Utility to get a strategyOnMismatch property for "import-control" tag.
306     * @param attributes collect to get attribute from.
307     * @return the value of the attribute.
308     */
309    private static MismatchStrategy getStrategyForImportControl(Attributes attributes) {
310        final String returnValue = attributes.getValue(STRATEGY_ON_MISMATCH_ATTRIBUTE_NAME);
311        MismatchStrategy strategyOnMismatch = MismatchStrategy.DISALLOWED;
312        if (STRATEGY_ON_MISMATCH_ALLOWED_VALUE.equals(returnValue)) {
313            strategyOnMismatch = MismatchStrategy.ALLOWED;
314        }
315        return strategyOnMismatch;
316    }
317
318    /**
319     * Utility to get a strategyOnMismatch property for "subpackage" tag.
320     * @param attributes collect to get attribute from.
321     * @return the value of the attribute.
322     */
323    private static MismatchStrategy getStrategyForSubpackage(Attributes attributes) {
324        final String returnValue = attributes.getValue(STRATEGY_ON_MISMATCH_ATTRIBUTE_NAME);
325        MismatchStrategy strategyOnMismatch = MismatchStrategy.DELEGATE_TO_PARENT;
326        if (STRATEGY_ON_MISMATCH_ALLOWED_VALUE.equals(returnValue)) {
327            strategyOnMismatch = MismatchStrategy.ALLOWED;
328        }
329        else if (STRATEGY_ON_MISMATCH_DISALLOWED_VALUE.equals(returnValue)) {
330            strategyOnMismatch = MismatchStrategy.DISALLOWED;
331        }
332        return strategyOnMismatch;
333    }
334
335    /**
336     * Utility to safely get an attribute. If it does not exist an exception
337     * is thrown.
338     * @param attributes collect to get attribute from.
339     * @param name name of the attribute to get.
340     * @return the value of the attribute.
341     * @throws SAXException if the attribute does not exist.
342     */
343    private static String safeGet(Attributes attributes, String name)
344            throws SAXException {
345        final String returnValue = attributes.getValue(name);
346        if (returnValue == null) {
347            // -@cs[IllegalInstantiation] SAXException is in the overridden method signature
348            // of the only method which calls the current one
349            throw new SAXException("missing attribute " + name);
350        }
351        return returnValue;
352    }
353
354}