1   ////////////////////////////////////////////////////////////////////////////////
2   // checkstyle: Checks Java source code for adherence to a set of rules.
3   // Copyright (C) 2001-2019 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.List;
24  import java.util.regex.Pattern;
25  
26  /**
27   * Represents a tree of import rules for a specific package.
28   * Each instance may have zero or more children. A child may
29   * be a sub-package, a class, or an allow/disallow rule.
30   */
31  class PkgImportControl extends AbstractImportControl {
32      /** The package separator: "." */
33      private static final String DOT = ".";
34      /** A pattern matching the package separator: "." */
35      private static final Pattern DOT_PATTERN = Pattern.compile(DOT, Pattern.LITERAL);
36      /** The regex for the package separator: "\\.". */
37      private static final String DOT_REGEX = "\\.";
38  
39      /** List of children {@link AbstractImportControl} objects. */
40      private final List<AbstractImportControl> children = new ArrayList<>();
41  
42      /** The full name for the package. */
43      private final String fullPackageName;
44      /**
45       * The regex pattern for partial match (exact and for subpackages) - only not
46       * null if regex is true.
47       */
48      private final Pattern patternForPartialMatch;
49      /** The regex pattern for exact matches - only not null if regex is true. */
50      private final Pattern patternForExactMatch;
51      /** If this package represents a regular expression. */
52      private final boolean regex;
53  
54      /**
55       * Construct a root, package node.
56       * @param packageName the name of the package.
57       * @param regex flags interpretation of name as regex pattern.
58       * @param strategyOnMismatch strategy in a case if matching allow/disallow rule was not found.
59       */
60      /* package */ PkgImportControl(String packageName, boolean regex,
61              MismatchStrategy strategyOnMismatch) {
62          super(null, strategyOnMismatch);
63  
64          this.regex = regex;
65          if (regex) {
66              // ensure that fullName is a self-contained regular expression
67              fullPackageName = encloseInGroup(packageName);
68              patternForPartialMatch = createPatternForPartialMatch(fullPackageName);
69              patternForExactMatch = createPatternForExactMatch(fullPackageName);
70          }
71          else {
72              fullPackageName = packageName;
73              patternForPartialMatch = null;
74              patternForExactMatch = null;
75          }
76      }
77  
78      /**
79       * Construct a sub-package node. The concatenation of regular expressions needs special care:
80       * see {@link #ensureSelfContainedRegex(String, boolean)} for more details.
81       * @param parent the parent package.
82       * @param subPackageName the name of the current sub-package.
83       * @param regex flags interpretation of name as regex pattern.
84       * @param strategyOnMismatch strategy in a case if matching allow/disallow rule was not found.
85       */
86      /* package */ PkgImportControl(PkgImportControl parent, String subPackageName, boolean regex,
87              MismatchStrategy strategyOnMismatch) {
88          super(parent, strategyOnMismatch);
89          if (regex || parent.regex) {
90              // regex gets inherited
91              final String parentRegex = ensureSelfContainedRegex(parent.fullPackageName,
92                      parent.regex);
93              final String thisRegex = ensureSelfContainedRegex(subPackageName, regex);
94              fullPackageName = parentRegex + DOT_REGEX + thisRegex;
95              patternForPartialMatch = createPatternForPartialMatch(fullPackageName);
96              patternForExactMatch = createPatternForExactMatch(fullPackageName);
97              this.regex = true;
98          }
99          else {
100             fullPackageName = parent.fullPackageName + DOT + subPackageName;
101             patternForPartialMatch = null;
102             patternForExactMatch = null;
103             this.regex = false;
104         }
105     }
106 
107     /**
108      * Returns a regex that is suitable for concatenation by 1) either converting a plain string
109      * into a regular expression (handling special characters) or 2) by enclosing {@code input} in
110      * a (non-capturing) group if {@code input} already is a regular expression.
111      *
112      * <p>1) When concatenating a non-regex package component (like "org.google") with a regex
113      * component (like "[^.]+") the other component has to be converted into a regex too, see
114      * {@link #toRegex(String)}.
115      *
116      * <p>2) The grouping is strictly necessary if a) {@code input} is a regular expression that b)
117      * contains the alteration character ('|') and if c) the pattern is not already enclosed in a
118      * group - as you see in this example: {@code parent="com|org", child="common|uncommon"} will
119      * result in the pattern {@code "(?:org|com)\.(?common|uncommon)"} what will match
120      * {@code "com.common"}, {@code "com.uncommon"}, {@code "org.common"}, and {@code
121      * "org.uncommon"}. Without the grouping it would be {@code "com|org.common|uncommon"} which
122      * would match {@code "com"}, {@code "org.common"}, and {@code "uncommon"}, which clearly is
123      * undesirable. Adding the group fixes this.
124      *
125      * <p>For simplicity the grouping is added to regular expressions unconditionally.
126      *
127      * @param input the input string.
128      * @param alreadyRegex signals if input already is a regular expression.
129      * @return a regex string.
130      */
131     private static String ensureSelfContainedRegex(String input, boolean alreadyRegex) {
132         final String result;
133         if (alreadyRegex) {
134             result = encloseInGroup(input);
135         }
136         else {
137             result = toRegex(input);
138         }
139         return result;
140     }
141 
142     /**
143      * Enclose {@code expression} in a (non-capturing) group.
144      * @param expression the input regular expression
145      * @return a grouped pattern.
146      */
147     private static String encloseInGroup(String expression) {
148         return "(?:" + expression + ")";
149     }
150 
151     /**
152      * Converts a normal package name into a regex pattern by escaping all
153      * special characters that may occur in a java package name.
154      * @param input the input string.
155      * @return a regex string.
156      */
157     private static String toRegex(String input) {
158         return DOT_PATTERN.matcher(input).replaceAll(DOT_REGEX);
159     }
160 
161     /**
162      * Creates a Pattern from {@code expression} that matches exactly and child packages.
163      * @param expression a self-contained regular expression matching the full package exactly.
164      * @return a Pattern.
165      */
166     private static Pattern createPatternForPartialMatch(String expression) {
167         // javadoc of encloseInGroup() explains how to concatenate regular expressions
168         // no grouping needs to be added to fullPackage since this already have been done.
169         return Pattern.compile(expression + "(?:\\..*)?");
170     }
171 
172     /**
173      * Creates a Pattern from {@code expression}.
174      * @param expression a self-contained regular expression matching the full package exactly.
175      * @return a Pattern.
176      */
177     private static Pattern createPatternForExactMatch(String expression) {
178         return Pattern.compile(expression);
179     }
180 
181     @Override
182     public AbstractImportControl locateFinest(String forPkg, String forFileName) {
183         AbstractImportControl finestMatch = null;
184         // Check if we are a match.
185         if (matchesAtFront(forPkg)) {
186             // If there won't be match so I am the best there is.
187             finestMatch = this;
188             // Check if any of the children match.
189             for (AbstractImportControl child : children) {
190                 final AbstractImportControl match = child.locateFinest(forPkg, forFileName);
191                 if (match != null) {
192                     finestMatch = match;
193                     break;
194                 }
195             }
196         }
197         return finestMatch;
198     }
199 
200     /**
201      * Adds new child import control.
202      * @param importControl child import control
203      */
204     public void addChild(AbstractImportControl importControl) {
205         children.add(importControl);
206     }
207 
208     /**
209      * Matches other package name exactly or partially at front.
210      * @param pkg the package to compare with.
211      * @return if it matches.
212      */
213     private boolean matchesAtFront(String pkg) {
214         final boolean result;
215         if (regex) {
216             result = patternForPartialMatch.matcher(pkg).matches();
217         }
218         else {
219             result = matchesAtFrontNoRegex(pkg);
220         }
221         return result;
222     }
223 
224     /**
225      * Non-regex case. Ensure a trailing dot for subpackages, i.e. "com.puppy"
226      * will match "com.puppy.crawl" but not "com.puppycrawl.tools".
227      * @param pkg the package to compare with.
228      * @return if it matches.
229      */
230     private boolean matchesAtFrontNoRegex(String pkg) {
231         return pkg.startsWith(fullPackageName)
232                 && (pkg.length() == fullPackageName.length()
233                     || pkg.charAt(fullPackageName.length()) == '.');
234     }
235 
236     @Override
237     protected boolean matchesExactly(String pkg, String fileName) {
238         final boolean result;
239         if (regex) {
240             result = patternForExactMatch.matcher(pkg).matches();
241         }
242         else {
243             result = fullPackageName.equals(pkg);
244         }
245         return result;
246     }
247 }