//////////////////////////////////////////////////////////////////////////////// // checkstyle: Checks Java source code for adherence to a set of rules. // Copyright (C) 2001-2017 the original author or authors. // // This library is free software; you can redistribute it and/or // modify it under the terms of the GNU Lesser General Public // License as published by the Free Software Foundation; either // version 2.1 of the License, or (at your option) any later version. // // This library is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // Lesser General Public License for more details. // // You should have received a copy of the GNU Lesser General Public // License along with this library; if not, write to the Free Software // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA //////////////////////////////////////////////////////////////////////////////// package com.puppycrawl.tools.checkstyle.checks.imports; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URI; import java.util.ArrayDeque; import java.util.Deque; import java.util.HashMap; import java.util.Map; import javax.xml.parsers.ParserConfigurationException; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import com.puppycrawl.tools.checkstyle.api.AbstractLoader; import com.puppycrawl.tools.checkstyle.api.CheckstyleException; /** * Responsible for loading the contents of an import control configuration file. * @author Oliver Burn */ final class ImportControlLoader extends AbstractLoader { /** The public ID for the configuration dtd. */ private static final String DTD_PUBLIC_ID_1_0 = "-//Puppy Crawl//DTD Import Control 1.0//EN"; /** The public ID for the configuration dtd. */ private static final String DTD_PUBLIC_ID_1_1 = "-//Puppy Crawl//DTD Import Control 1.1//EN"; /** The public ID for the configuration dtd. */ private static final String DTD_PUBLIC_ID_1_2 = "-//Puppy Crawl//DTD Import Control 1.2//EN"; /** The resource for the configuration dtd. */ private static final String DTD_RESOURCE_NAME_1_0 = "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_0.dtd"; /** The resource for the configuration dtd. */ private static final String DTD_RESOURCE_NAME_1_1 = "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_1.dtd"; /** The resource for the configuration dtd. */ private static final String DTD_RESOURCE_NAME_1_2 = "com/puppycrawl/tools/checkstyle/checks/imports/import_control_1_2.dtd"; /** The map to lookup the resource name by the id. */ private static final Map<String, String> DTD_RESOURCE_BY_ID = new HashMap<>(); /** Name for attribute 'pkg'. */ private static final String PKG_ATTRIBUTE_NAME = "pkg"; /** Qualified name for element 'subpackage'. */ private static final String SUBPACKAGE_ELEMENT_NAME = "subpackage"; /** Qualified name for element 'allow'. */ private static final String ALLOW_ELEMENT_NAME = "allow"; /** Used to hold the {@link ImportControl} objects. */ private final Deque<ImportControl> stack = new ArrayDeque<>(); static { DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_ID_1_0, DTD_RESOURCE_NAME_1_0); DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_ID_1_1, DTD_RESOURCE_NAME_1_1); DTD_RESOURCE_BY_ID.put(DTD_PUBLIC_ID_1_2, DTD_RESOURCE_NAME_1_2); } /** * Constructs an instance. * @throws ParserConfigurationException if an error occurs. * @throws SAXException if an error occurs. */ private ImportControlLoader() throws ParserConfigurationException, SAXException { super(DTD_RESOURCE_BY_ID); } @Override public void startElement(final String namespaceUri, final String localName, final String qName, final Attributes attributes) throws SAXException { if ("import-control".equals(qName)) { final String pkg = safeGet(attributes, PKG_ATTRIBUTE_NAME); final boolean regex = containsRegexAttribute(attributes); stack.push(new ImportControl(pkg, regex)); } else if (SUBPACKAGE_ELEMENT_NAME.equals(qName)) { final String name = safeGet(attributes, "name"); final boolean regex = containsRegexAttribute(attributes); stack.push(new ImportControl(stack.peek(), name, regex)); } else if (ALLOW_ELEMENT_NAME.equals(qName) || "disallow".equals(qName)) { // Need to handle either "pkg" or "class" attribute. // May have "exact-match" for "pkg" // May have "local-only" final boolean isAllow = ALLOW_ELEMENT_NAME.equals(qName); final boolean isLocalOnly = attributes.getValue("local-only") != null; final String pkg = attributes.getValue(PKG_ATTRIBUTE_NAME); final boolean regex = containsRegexAttribute(attributes); final AbstractImportRule rule; if (pkg == null) { // handle class names which can be normal class names or regular // expressions final String clazz = safeGet(attributes, "class"); rule = new ClassImportRule(isAllow, isLocalOnly, clazz, regex); } else { final boolean exactMatch = attributes.getValue("exact-match") != null; rule = new PkgImportRule(isAllow, isLocalOnly, pkg, exactMatch, regex); } stack.peek().addImportRule(rule); } } /** * Check if the given attributes contain the regex attribute. * @param attributes the attributes. * @return if the regex attribute is contained. */ private static boolean containsRegexAttribute(final Attributes attributes) { return attributes.getValue("regex") != null; } @Override public void endElement(final String namespaceUri, final String localName, final String qName) { if (SUBPACKAGE_ELEMENT_NAME.equals(qName)) { stack.pop(); } } /** * Loads the import control file from a file. * @param uri the uri of the file to load. * @return the root {@link ImportControl} object. * @throws CheckstyleException if an error occurs. */ public static ImportControl load(final URI uri) throws CheckstyleException { InputStream inputStream = null; try { inputStream = uri.toURL().openStream(); final InputSource source = new InputSource(inputStream); return load(source, uri); } catch (final MalformedURLException ex) { throw new CheckstyleException("syntax error in url " + uri, ex); } catch (final IOException ex) { throw new CheckstyleException("unable to find " + uri, ex); } finally { closeStream(inputStream); } } /** * Loads the import control file from a {@link InputSource}. * @param source the source to load from. * @param uri uri of the source being loaded. * @return the root {@link ImportControl} object. * @throws CheckstyleException if an error occurs. */ private static ImportControl load(final InputSource source, final URI uri) throws CheckstyleException { try { final ImportControlLoader loader = new ImportControlLoader(); loader.parseInputSource(source); return loader.getRoot(); } catch (final ParserConfigurationException | SAXException ex) { throw new CheckstyleException("unable to parse " + uri + " - " + ex.getMessage(), ex); } catch (final IOException ex) { throw new CheckstyleException("unable to read " + uri, ex); } } /** * This method exists only due to bug in cobertura library * https://github.com/cobertura/cobertura/issues/170 * @param inputStream the InputStream to close * @throws CheckstyleException if an error occurs. */ private static void closeStream(InputStream inputStream) throws CheckstyleException { if (inputStream != null) { try { inputStream.close(); } catch (IOException ex) { throw new CheckstyleException("unable to close input stream", ex); } } } /** * @return the root {@link ImportControl} object loaded. */ private ImportControl getRoot() { return stack.peek(); } /** * Utility to safely get an attribute. If it does not exist an exception * is thrown. * @param attributes collect to get attribute from. * @param name name of the attribute to get. * @return the value of the attribute. * @throws SAXException if the attribute does not exist. */ private static String safeGet(final Attributes attributes, final String name) throws SAXException { final String returnValue = attributes.getValue(name); if (returnValue == null) { // -@cs[IllegalInstantiation] SAXException is in the overridden method signature // of the only method which calls the current one throw new SAXException("missing attribute " + name); } return returnValue; } }