//////////////////////////////////////////////////////////////////////////////// // 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.util.ArrayList; import java.util.Deque; import java.util.LinkedList; import java.util.List; import java.util.regex.Pattern; /** * Represents a tree of import rules for controlling whether packages or * classes are allowed to be used. Each instance must have a single parent or * be the root node. Each instance may have zero or more children. * * @author Oliver Burn */ class ImportControl { /** The package separator: "." */ private static final String DOT = "."; /** A pattern matching the package separator: "." */ private static final Pattern DOT_PATTERN = Pattern.compile(DOT, Pattern.LITERAL); /** The regex for the package separator: "\\.". */ private static final String DOT_REGEX = "\\."; /** List of {@link AbstractImportRule} objects to check. */ private final Deque<AbstractImportRule> rules = new LinkedList<>(); /** List of children {@link ImportControl} objects. */ private final List<ImportControl> children = new ArrayList<>(); /** The parent. Null indicates we are the root node. */ private final ImportControl parent; /** The full package name for the node. */ private final String fullPackage; /** * The regex pattern for partial match (exact and for subpackages) - only not * null if regex is true. */ private final Pattern patternForPartialMatch; /** The regex pattern for exact matches - only not null if regex is true. */ private final Pattern patternForExactMatch; /** If this package represents a regular expression. */ private final boolean regex; /** * Construct a root node. * @param pkgName the name of the package. * @param regex flags interpretation of pkgName as regex pattern. */ ImportControl(final String pkgName, final boolean regex) { parent = null; this.regex = regex; if (regex) { // ensure that fullPackage is a self-contained regular expression fullPackage = encloseInGroup(pkgName); patternForPartialMatch = createPatternForPartialMatch(fullPackage); patternForExactMatch = createPatternForExactMatch(fullPackage); } else { fullPackage = pkgName; patternForPartialMatch = null; patternForExactMatch = null; } } /** * Construct a child node. The concatenation of regular expressions needs special care: * see {@link #ensureSelfContainedRegex(String, boolean)} for more details. * @param parent the parent node. * @param subPkg the sub package name. * @param regex flags interpretation of subPkg as regex pattern. */ ImportControl(final ImportControl parent, final String subPkg, final boolean regex) { this.parent = parent; if (regex || parent.regex) { // regex gets inherited final String parentRegex = ensureSelfContainedRegex(parent.fullPackage, parent.regex); final String thisRegex = ensureSelfContainedRegex(subPkg, regex); fullPackage = parentRegex + DOT_REGEX + thisRegex; patternForPartialMatch = createPatternForPartialMatch(fullPackage); patternForExactMatch = createPatternForExactMatch(fullPackage); this.regex = true; } else { fullPackage = parent.fullPackage + DOT + subPkg; patternForPartialMatch = null; patternForExactMatch = null; this.regex = false; } parent.children.add(this); } /** * Returns a regex that is suitable for concatenation by 1) either converting a plain string * into a regular expression (handling special characters) or 2) by enclosing {@code input} in * a (non-capturing) group if {@code input} already is a regular expression. * * <p>1) When concatenating a non-regex package component (like "org.google") with a regex * component (like "[^.]+") the other component has to be converted into a regex too, see * {@link #toRegex(String)}. * * <p>2) The grouping is strictly necessary if a) {@code input} is a regular expression that b) * contains the alteration character ('|') and if c) the pattern is not already enclosed in a * group - as you see in this example: {@code parent="com|org", child="common|uncommon"} will * result in the pattern {@code "(?:org|com)\.(?common|uncommon)"} what will match * {@code "com.common"}, {@code "com.uncommon"}, {@code "org.common"}, and {@code * "org.uncommon"}. Without the grouping it would be {@code "com|org.common|uncommon"} which * would match {@code "com"}, {@code "org.common"}, and {@code "uncommon"}, which clearly is * undesirable. Adding the group fixes this. * * <p>For simplicity the grouping is added to regular expressions unconditionally. * * @param input the input string. * @param alreadyRegex signals if input already is a regular expression. * @return a regex string. */ private static String ensureSelfContainedRegex(final String input, final boolean alreadyRegex) { final String result; if (alreadyRegex) { result = encloseInGroup(input); } else { result = toRegex(input); } return result; } /** * Enclose {@code expression} in a (non-capturing) group. * @param expression the input regular expression * @return a grouped pattern. */ private static String encloseInGroup(String expression) { return "(?:" + expression + ")"; } /** * Converts a normal package name into a regex pattern by escaping all * special characters that may occur in a java package name. * @param input the input string. * @return a regex string. */ private static String toRegex(String input) { return DOT_PATTERN.matcher(input).replaceAll(DOT_REGEX); } /** * Creates a Pattern from {@code expression} that matches exactly and child packages. * @param expression a self-contained regular expression matching the full package exactly. * @return a Pattern. */ private static Pattern createPatternForPartialMatch(String expression) { // javadoc of encloseInGroup() explains how to concatenate regular expressions // no grouping needs to be added to fullPackage since this already have been done. return Pattern.compile(expression + "(?:\\..*)?"); } /** * Creates a Pattern from {@code expression}. * @param expression a self-contained regular expression matching the full package exactly. * @return a Pattern. */ private static Pattern createPatternForExactMatch(String expression) { return Pattern.compile(expression); } /** * Adds an {@link AbstractImportRule} to the node. * @param rule the rule to be added. */ protected void addImportRule(final AbstractImportRule rule) { rules.addFirst(rule); } /** * Search down the tree to locate the finest match for a supplied package. * @param forPkg the package to search for. * @return the finest match, or null if no match at all. */ public ImportControl locateFinest(final String forPkg) { ImportControl finestMatch = null; // Check if we are a match. if (matchesAtFront(forPkg)) { // If there won't be match so I am the best there is. finestMatch = this; // Check if any of the children match. for (ImportControl child : children) { final ImportControl match = child.locateFinest(forPkg); if (match != null) { finestMatch = match; break; } } } return finestMatch; } /** * Matches other package name exactly or partially at front. * @param pkg the package to compare with. * @return if it matches. */ private boolean matchesAtFront(final String pkg) { final boolean result; if (regex) { result = patternForPartialMatch.matcher(pkg).matches(); } else { result = matchesAtFrontNoRegex(pkg); } return result; } /** * Non-regex case. Ensure a trailing dot for subpackages, i.e. "com.puppy" * will match "com.puppy.crawl" but not "com.puppycrawl.tools". * @param pkg the package to compare with. * @return if it matches. */ private boolean matchesAtFrontNoRegex(final String pkg) { return pkg.startsWith(fullPackage) && (pkg.length() == fullPackage.length() || pkg.charAt(fullPackage.length()) == '.'); } /** * Returns whether a package or class is allowed to be imported. * The algorithm checks with the current node for a result, and if none is * found then calls its parent looking for a match. This will recurse * looking for match. If there is no clear result then * {@link AccessResult#UNKNOWN} is returned. * @param forImport the import to check on. * @param inPkg the package doing the import. * @return an {@link AccessResult}. */ public AccessResult checkAccess(final String inPkg, final String forImport) { final AccessResult result; final AccessResult returnValue = localCheckAccess(inPkg, forImport); if (returnValue != AccessResult.UNKNOWN) { result = returnValue; } else if (parent == null) { // we are the top, so default to not allowed. result = AccessResult.DISALLOWED; } else { result = parent.checkAccess(inPkg, forImport); } return result; } /** * Checks whether any of the rules for this node control access to * a specified package or class. * @param forImport the import to check. * @param inPkg the package doing the import. * @return an {@link AccessResult}. */ private AccessResult localCheckAccess(final String inPkg, final String forImport) { AccessResult localCheckAccessResult = AccessResult.UNKNOWN; for (AbstractImportRule importRule : rules) { // Check if an import rule is only meant to be applied locally. if (!importRule.isLocalOnly() || matchesExactly(inPkg)) { final AccessResult result = importRule.verifyImport(forImport); if (result != AccessResult.UNKNOWN) { localCheckAccessResult = result; break; } } } return localCheckAccessResult; } /** * Check for equality of this with pkg * @param pkg the package to compare with. * @return if it matches. */ private boolean matchesExactly(final String pkg) { final boolean result; if (regex) { result = patternForExactMatch.matcher(pkg).matches(); } else { result = fullPackage.equals(pkg); } return result; } }