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