View Javadoc
1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2018 the original author or authors.
4   //
5   // This library is free software; you can redistribute it and/or
6   // modify it under the terms of the GNU Lesser General Public
7   // License as published by the Free Software Foundation; either
8   // version 2.1 of the License, or (at your option) any later version.
9   //
10  // This library is distributed in the hope that it will be useful,
11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  // Lesser General Public License for more details.
14  //
15  // You should have received a copy of the GNU Lesser General Public
16  // License along with this library; if not, write to the Free Software
17  // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
18  ////////////////////////////////////////////////////////////////////////////////
19  
20  package com.puppycrawl.tools.checkstyle.checks.imports;
21  
22  import java.util.ArrayList;
23  import java.util.Deque;
24  import java.util.LinkedList;
25  import java.util.List;
26  import java.util.regex.Pattern;
27  
28  /**
29   * Represents a tree of import rules for controlling whether packages or
30   * classes are allowed to be used. Each instance must have a single parent or
31   * be the root node. Each instance may have zero or more children.
32   *
33   * @author Oliver Burn
34   */
35  class ImportControl {
36  
37      /** The package separator: "." */
38      private static final String DOT = ".";
39      /** A pattern matching the package separator: "." */
40      private static final Pattern DOT_PATTERN = Pattern.compile(DOT, Pattern.LITERAL);
41      /** The regex for the package separator: "\\.". */
42      private static final String DOT_REGEX = "\\.";
43      /** List of {@link AbstractImportRule} objects to check. */
44      private final Deque<AbstractImportRule> rules = new LinkedList<>();
45      /** List of children {@link ImportControl} objects. */
46      private final List<ImportControl> children = new ArrayList<>();
47      /** The parent. Null indicates we are the root node. */
48      private final ImportControl parent;
49      /** The full package name for the node. */
50      private final String fullPackage;
51      /**
52       * The regex pattern for partial match (exact and for subpackages) - only not
53       * null if regex is true.
54       */
55      private final Pattern patternForPartialMatch;
56      /** The regex pattern for exact matches - only not null if regex is true. */
57      private final Pattern patternForExactMatch;
58      /** If this package represents a regular expression. */
59      private final boolean regex;
60      /** Strategy in a case if matching allow/disallow rule was not found. */
61      private final MismatchStrategy strategyOnMismatch;
62  
63      /**
64       * Construct a root node.
65       * @param pkgName the name of the package.
66       * @param regex flags interpretation of pkgName as regex pattern.
67       * @param strategyOnMismatch strategy in a case if matching allow/disallow rule was not found.
68       */
69      ImportControl(String pkgName, boolean regex,
70                    MismatchStrategy strategyOnMismatch) {
71          parent = null;
72          this.regex = regex;
73          this.strategyOnMismatch = strategyOnMismatch;
74          if (regex) {
75              // ensure that fullPackage is a self-contained regular expression
76              fullPackage = encloseInGroup(pkgName);
77              patternForPartialMatch = createPatternForPartialMatch(fullPackage);
78              patternForExactMatch = createPatternForExactMatch(fullPackage);
79          }
80          else {
81              fullPackage = pkgName;
82              patternForPartialMatch = null;
83              patternForExactMatch = null;
84          }
85      }
86  
87      /**
88       * Construct a root node.
89       * @param pkgName the name of the package.
90       * @param regex flags interpretation of pkgName as regex pattern.
91       */
92      ImportControl(String pkgName, boolean regex) {
93          this(pkgName, regex, MismatchStrategy.DISALLOWED);
94      }
95  
96      /**
97       * Construct a child node. The concatenation of regular expressions needs special care:
98       * see {@link #ensureSelfContainedRegex(String, boolean)} for more details.
99       * @param parent the parent node.
100      * @param subPkg the sub package name.
101      * @param regex flags interpretation of subPkg as regex pattern.
102      * @param strategyOnMismatch strategy in a case if matching allow/disallow rule was not found.
103      */
104     ImportControl(ImportControl parent, String subPkg, boolean regex,
105                   MismatchStrategy strategyOnMismatch) {
106         this.parent = parent;
107         this.strategyOnMismatch = strategyOnMismatch;
108         if (regex || parent.regex) {
109             // regex gets inherited
110             final String parentRegex = ensureSelfContainedRegex(parent.fullPackage, parent.regex);
111             final String thisRegex = ensureSelfContainedRegex(subPkg, regex);
112             fullPackage = parentRegex + DOT_REGEX + thisRegex;
113             patternForPartialMatch = createPatternForPartialMatch(fullPackage);
114             patternForExactMatch = createPatternForExactMatch(fullPackage);
115             this.regex = true;
116         }
117         else {
118             fullPackage = parent.fullPackage + DOT + subPkg;
119             patternForPartialMatch = null;
120             patternForExactMatch = null;
121             this.regex = false;
122         }
123     }
124 
125     /**
126      * Construct a child node. The concatenation of regular expressions needs special care:
127      * see {@link #ensureSelfContainedRegex(String, boolean)} for more details.
128      * @param parent the parent node.
129      * @param subPkg the sub package name.
130      * @param regex flags interpretation of subPkg as regex pattern.
131      */
132     ImportControl(ImportControl parent, String subPkg, boolean regex) {
133         this(parent, subPkg, regex, MismatchStrategy.DELEGATE_TO_PARENT);
134     }
135 
136     /**
137      * Returns a regex that is suitable for concatenation by 1) either converting a plain string
138      * into a regular expression (handling special characters) or 2) by enclosing {@code input} in
139      * a (non-capturing) group if {@code input} already is a regular expression.
140      *
141      * <p>1) When concatenating a non-regex package component (like "org.google") with a regex
142      * component (like "[^.]+") the other component has to be converted into a regex too, see
143      * {@link #toRegex(String)}.
144      *
145      * <p>2) The grouping is strictly necessary if a) {@code input} is a regular expression that b)
146      * contains the alteration character ('|') and if c) the pattern is not already enclosed in a
147      * group - as you see in this example: {@code parent="com|org", child="common|uncommon"} will
148      * result in the pattern {@code "(?:org|com)\.(?common|uncommon)"} what will match
149      * {@code "com.common"}, {@code "com.uncommon"}, {@code "org.common"}, and {@code
150      * "org.uncommon"}. Without the grouping it would be {@code "com|org.common|uncommon"} which
151      * would match {@code "com"}, {@code "org.common"}, and {@code "uncommon"}, which clearly is
152      * undesirable. Adding the group fixes this.
153      *
154      * <p>For simplicity the grouping is added to regular expressions unconditionally.
155      *
156      * @param input the input string.
157      * @param alreadyRegex signals if input already is a regular expression.
158      * @return a regex string.
159      */
160     private static String ensureSelfContainedRegex(String input, boolean alreadyRegex) {
161         final String result;
162         if (alreadyRegex) {
163             result = encloseInGroup(input);
164         }
165         else {
166             result = toRegex(input);
167         }
168         return result;
169     }
170 
171     /**
172      * Enclose {@code expression} in a (non-capturing) group.
173      * @param expression the input regular expression
174      * @return a grouped pattern.
175      */
176     private static String encloseInGroup(String expression) {
177         return "(?:" + expression + ")";
178     }
179 
180     /**
181      * Converts a normal package name into a regex pattern by escaping all
182      * special characters that may occur in a java package name.
183      * @param input the input string.
184      * @return a regex string.
185      */
186     private static String toRegex(String input) {
187         return DOT_PATTERN.matcher(input).replaceAll(DOT_REGEX);
188     }
189 
190     /**
191      * Creates a Pattern from {@code expression} that matches exactly and child packages.
192      * @param expression a self-contained regular expression matching the full package exactly.
193      * @return a Pattern.
194      */
195     private static Pattern createPatternForPartialMatch(String expression) {
196         // javadoc of encloseInGroup() explains how to concatenate regular expressions
197         // no grouping needs to be added to fullPackage since this already have been done.
198         return Pattern.compile(expression + "(?:\\..*)?");
199     }
200 
201     /**
202      * Creates a Pattern from {@code expression}.
203      * @param expression a self-contained regular expression matching the full package exactly.
204      * @return a Pattern.
205      */
206     private static Pattern createPatternForExactMatch(String expression) {
207         return Pattern.compile(expression);
208     }
209 
210     /**
211      * Adds an {@link AbstractImportRule} to the node.
212      * @param rule the rule to be added.
213      */
214     protected void addImportRule(AbstractImportRule rule) {
215         rules.addLast(rule);
216     }
217 
218     /**
219      * Adds new child import control.
220      * @param importControl child import control
221      */
222     public void addChild(ImportControl importControl) {
223         children.add(importControl);
224     }
225 
226     /**
227      * Search down the tree to locate the finest match for a supplied package.
228      * @param forPkg the package to search for.
229      * @return the finest match, or null if no match at all.
230      */
231     public ImportControl locateFinest(String forPkg) {
232         ImportControl finestMatch = null;
233         // Check if we are a match.
234         if (matchesAtFront(forPkg)) {
235             // If there won't be match so I am the best there is.
236             finestMatch = this;
237             // Check if any of the children match.
238             for (ImportControl child : children) {
239                 final ImportControl match = child.locateFinest(forPkg);
240                 if (match != null) {
241                     finestMatch = match;
242                     break;
243                 }
244             }
245         }
246         return finestMatch;
247     }
248 
249     /**
250      * Matches other package name exactly or partially at front.
251      * @param pkg the package to compare with.
252      * @return if it matches.
253      */
254     private boolean matchesAtFront(String pkg) {
255         final boolean result;
256         if (regex) {
257             result = patternForPartialMatch.matcher(pkg).matches();
258         }
259         else {
260             result = matchesAtFrontNoRegex(pkg);
261         }
262         return result;
263     }
264 
265     /**
266      * Non-regex case. Ensure a trailing dot for subpackages, i.e. "com.puppy"
267      * will match "com.puppy.crawl" but not "com.puppycrawl.tools".
268      * @param pkg the package to compare with.
269      * @return if it matches.
270      */
271     private boolean matchesAtFrontNoRegex(String pkg) {
272         return pkg.startsWith(fullPackage)
273                 && (pkg.length() == fullPackage.length()
274                     || pkg.charAt(fullPackage.length()) == '.');
275     }
276 
277     /**
278      * Returns whether a package or class is allowed to be imported.
279      * The algorithm checks with the current node for a result, and if none is
280      * found then calls its parent looking for a match. This will recurse
281      * looking for match. If there is no clear result then
282      * {@link AccessResult#UNKNOWN} is returned.
283      * @param forImport the import to check on.
284      * @param inPkg the package doing the import.
285      * @return an {@link AccessResult}.
286      */
287     public AccessResult checkAccess(String inPkg, String forImport) {
288         final AccessResult result;
289         final AccessResult returnValue = localCheckAccess(inPkg, forImport);
290         if (returnValue != AccessResult.UNKNOWN) {
291             result = returnValue;
292         }
293         else if (parent == null) {
294             if (strategyOnMismatch == MismatchStrategy.ALLOWED) {
295                 result = AccessResult.ALLOWED;
296             }
297             else {
298                 result = AccessResult.DISALLOWED;
299             }
300         }
301         else {
302             if (strategyOnMismatch == MismatchStrategy.ALLOWED) {
303                 result = AccessResult.ALLOWED;
304             }
305             else if (strategyOnMismatch == MismatchStrategy.DISALLOWED) {
306                 result = AccessResult.DISALLOWED;
307             }
308             else {
309                 result = parent.checkAccess(inPkg, forImport);
310             }
311         }
312         return result;
313     }
314 
315     /**
316      * Checks whether any of the rules for this node control access to
317      * a specified package or class.
318      * @param forImport the import to check.
319      * @param inPkg the package doing the import.
320      * @return an {@link AccessResult}.
321      */
322     private AccessResult localCheckAccess(String inPkg, String forImport) {
323         AccessResult localCheckAccessResult = AccessResult.UNKNOWN;
324         for (AbstractImportRule importRule : rules) {
325             // Check if an import rule is only meant to be applied locally.
326             if (!importRule.isLocalOnly() || matchesExactly(inPkg)) {
327                 final AccessResult result = importRule.verifyImport(forImport);
328                 if (result != AccessResult.UNKNOWN) {
329                     localCheckAccessResult = result;
330                     break;
331                 }
332             }
333         }
334         return localCheckAccessResult;
335     }
336 
337     /**
338      * Check for equality of this with pkg.
339      * @param pkg the package to compare with.
340      * @return if it matches.
341      */
342     private boolean matchesExactly(String pkg) {
343         final boolean result;
344         if (regex) {
345             result = patternForExactMatch.matcher(pkg).matches();
346         }
347         else {
348             result = fullPackage.equals(pkg);
349         }
350         return result;
351     }
352 
353 }