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.StringTokenizer;
25  import java.util.regex.Matcher;
26  import java.util.regex.Pattern;
27  
28  import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
29  import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
30  import com.puppycrawl.tools.checkstyle.api.DetailAST;
31  import com.puppycrawl.tools.checkstyle.api.FullIdent;
32  import com.puppycrawl.tools.checkstyle.api.TokenTypes;
33  import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
34  
35  /**
36   * <p>
37   * Checks that the groups of import declarations appear in the order specified
38   * by the user. If there is an import but its group is not specified in the
39   * configuration such an import should be placed at the end of the import list.
40   * </p>
41   * <p>
42   * The rule consists of:
43   * </p>
44   * <ol>
45   * <li>
46   * STATIC group. This group sets the ordering of static imports.
47   * </li>
48   * <li>
49   * SAME_PACKAGE(n) group. This group sets the ordering of the same package imports.
50   * Imports are considered on SAME_PACKAGE group if <b>n</b> first domains in package
51   * name and import name are identical:
52   * <pre>
53   * package java.util.concurrent.locks;
54   *
55   * import java.io.File;
56   * import java.util.*; //#1
57   * import java.util.List; //#2
58   * import java.util.StringTokenizer; //#3
59   * import java.util.concurrent.*; //#4
60   * import java.util.concurrent.AbstractExecutorService; //#5
61   * import java.util.concurrent.locks.LockSupport; //#6
62   * import java.util.regex.Pattern; //#7
63   * import java.util.regex.Matcher; //#8
64   * </pre>
65   * If we have SAME_PACKAGE(3) on configuration file, imports #4-6 will be considered as
66   * a SAME_PACKAGE group (java.util.concurrent.*, java.util.concurrent.AbstractExecutorService,
67   * java.util.concurrent.locks.LockSupport). SAME_PACKAGE(2) will include #1-8.
68   * SAME_PACKAGE(4) will include only #6. SAME_PACKAGE(5) will result in no imports assigned
69   * to SAME_PACKAGE group because actual package java.util.concurrent.locks has only 4 domains.
70   * </li>
71   * <li>
72   * THIRD_PARTY_PACKAGE group. This group sets ordering of third party imports.
73   * Third party imports are all imports except STATIC, SAME_PACKAGE(n), STANDARD_JAVA_PACKAGE and
74   * SPECIAL_IMPORTS.
75   * </li>
76   * <li>
77   * STANDARD_JAVA_PACKAGE group. By default this group sets ordering of standard java/javax imports.
78   * </li>
79   * <li>
80   * SPECIAL_IMPORTS group. This group may contains some imports that have particular meaning for the
81   * user.
82   * </li>
83   * </ol>
84   * <p>
85   * Use the separator '###' between rules.
86   * </p>
87   * <p>
88   * To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use
89   * thirdPartyPackageRegExp and standardPackageRegExp options.
90   * </p>
91   * <p>
92   * Pretty often one import can match more than one group. For example, static import from standard
93   * package or regular expressions are configured to allow one import match multiple groups.
94   * In this case, group will be assigned according to priorities:
95   * </p>
96   * <ol>
97   * <li>
98   * STATIC has top priority
99   * </li>
100  * <li>
101  * SAME_PACKAGE has second priority
102  * </li>
103  * <li>
104  * STANDARD_JAVA_PACKAGE and SPECIAL_IMPORTS will compete using "best match" rule: longer
105  * matching substring wins; in case of the same length, lower position of matching substring
106  * wins; if position is the same, order of rules in configuration solves the puzzle.
107  * </li>
108  * <li>
109  * THIRD_PARTY has the least priority
110  * </li>
111  * </ol>
112  * <p>
113  * Few examples to illustrate "best match":
114  * </p>
115  * <p>
116  * 1. patterns STANDARD_JAVA_PACKAGE = "Check", SPECIAL_IMPORTS="ImportOrderCheck" and input file:
117  * </p>
118  * <pre>
119  * import com.puppycrawl.tools.checkstyle.checks.imports.CustomImportOrderCheck;
120  * import com.puppycrawl.tools.checkstyle.checks.imports.ImportOrderCheck;
121  * </pre>
122  * <p>
123  * Result: imports will be assigned to SPECIAL_IMPORTS, because matching substring length is 16.
124  * Matching substring for STANDARD_JAVA_PACKAGE is 5.
125  * </p>
126  * <p>
127  * 2. patterns STANDARD_JAVA_PACKAGE = "Check", SPECIAL_IMPORTS="Avoid" and file:
128  * </p>
129  * <pre>
130  * import com.puppycrawl.tools.checkstyle.checks.imports.AvoidStarImportCheck;
131  * </pre>
132  * <p>
133  * Result: import will be assigned to SPECIAL_IMPORTS. Matching substring length is 5 for both
134  * patterns. However, "Avoid" position is lower than "Check" position.
135  * </p>
136  * <ul>
137  * <li>
138  * Property {@code customImportOrderRules} - Specify list of order declaration customizing by user.
139  * Default value is {@code {}}.
140  * </li>
141  * <li>
142  * Property {@code standardPackageRegExp} - Specify RegExp for STANDARD_JAVA_PACKAGE group imports.
143  * Default value is {@code "^(java|javax)\."}.
144  * </li>
145  * <li>
146  * Property {@code thirdPartyPackageRegExp} - Specify RegExp for THIRD_PARTY_PACKAGE group imports.
147  * Default value is {@code ".*"}.
148  * </li>
149  * <li>
150  * Property {@code specialImportsRegExp} - Specify RegExp for SPECIAL_IMPORTS group imports.
151  * Default value is {@code "^$" (empty)}.
152  * </li>
153  * <li>
154  * Property {@code separateLineBetweenGroups} - Force empty line separator between
155  * import groups.
156  * Default value is {@code true}.
157  * </li>
158  * <li>
159  * Property {@code sortImportsInGroupAlphabetically} - Force grouping alphabetically,
160  * in <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>.
161  * Default value is {@code false}.
162  * </li>
163  * </ul>
164  * <p>
165  * To configure the check so that it matches default Eclipse formatter configuration
166  * (tested on Kepler and Luna releases):
167  * </p>
168  * <ul>
169  * <li>
170  * group of static imports is on the top
171  * </li>
172  * <li>
173  * groups of non-static imports: "java" and "javax" packages first, then "org" and then all other
174  * imports
175  * </li>
176  * <li>
177  * imports will be sorted in the groups
178  * </li>
179  * <li>
180  * groups are separated by single blank line
181  * </li>
182  * </ul>
183  * <p>
184  * Notes:
185  * </p>
186  * <ul>
187  * <li>
188  * "com" package is not mentioned on configuration, because it is ignored by Eclipse Kepler and Luna
189  * (looks like Eclipse defect)
190  * </li>
191  * <li>
192  * configuration below doesn't work in all 100% cases due to inconsistent behavior prior to Mars
193  * release, but covers most scenarios
194  * </li>
195  * </ul>
196  * <pre>
197  * &lt;module name=&quot;CustomImportOrder&quot;&gt;
198  *   &lt;property name=&quot;customImportOrderRules&quot;
199  *     value=&quot;STATIC###STANDARD_JAVA_PACKAGE###SPECIAL_IMPORTS&quot;/&gt;
200  *   &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;^org\.&quot;/&gt;
201  *   &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
202  *   &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;true&quot;/&gt;
203  * &lt;/module&gt;
204  * </pre>
205  * <p>
206  * To configure the check so that it matches default Eclipse formatter configuration
207  * (tested on Mars release):
208  * </p>
209  * <ul>
210  * <li>
211  * group of static imports is on the top
212  * </li>
213  * <li>
214  * groups of non-static imports: "java" and "javax" packages first, then "org" and "com",
215  * then all other imports as one group
216  * </li>
217  * <li>
218  * imports will be sorted in the groups
219  * </li>
220  * <li>
221  * groups are separated by, at least, one blank line
222  * </li>
223  * </ul>
224  * <pre>
225  * &lt;module name=&quot;CustomImportOrder&quot;&gt;
226  *   &lt;property name=&quot;customImportOrderRules&quot;
227  *     value=&quot;STATIC###STANDARD_JAVA_PACKAGE###SPECIAL_IMPORTS###THIRD_PARTY_PACKAGE&quot;/&gt;
228  *   &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;^org\.&quot;/&gt;
229  *   &lt;property name=&quot;thirdPartyPackageRegExp&quot; value=&quot;^com\.&quot;/&gt;
230  *   &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
231  *   &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;true&quot;/&gt;
232  * &lt;/module&gt;
233  * </pre>
234  * <p>
235  * To configure the check so that it matches default IntelliJ IDEA formatter configuration
236  * (tested on v14):
237  * </p>
238  * <ul>
239  * <li>
240  * group of static imports is on the bottom
241  * </li>
242  * <li>
243  * groups of non-static imports: all imports except of "javax" and "java", then "javax" and "java"
244  * </li>
245  * <li>
246  * imports will be sorted in the groups
247  * </li>
248  * <li>
249  * groups are separated by, at least, one blank line
250  * </li>
251  * </ul>
252  * <p>
253  * Note: "separated" option is disabled because IDEA default has blank line between "java" and
254  * static imports, and no blank line between "javax" and "java"
255  * </p>
256  * <pre>
257  * &lt;module name="CustomImportOrder"&gt;
258  *   &lt;property name="customImportOrderRules"
259  *     value="THIRD_PARTY_PACKAGE###SPECIAL_IMPORTS###STANDARD_JAVA_PACKAGE###STATIC"/&gt;
260  *   &lt;property name="specialImportsRegExp" value="^javax\."/&gt;
261  *   &lt;property name="standardPackageRegExp" value="^java\."/&gt;
262  *   &lt;property name="sortImportsInGroupAlphabetically" value="true"/&gt;
263  *   &lt;property name="separateLineBetweenGroups" value="false"/&gt;
264  * &lt;/module&gt;
265  * </pre>
266  * <p>
267  * To configure the check so that it matches default NetBeans formatter configuration
268  * (tested on v8):
269  * </p>
270  * <ul>
271  * <li>
272  * groups of non-static imports are not defined, all imports will be sorted as a one group
273  * </li>
274  * <li>
275  * static imports are not separated, they will be sorted along with other imports
276  * </li>
277  * </ul>
278  * <pre>
279  * &lt;module name=&quot;CustomImportOrder&quot;/&gt;
280  * </pre>
281  * <p>
282  * To set RegExps for THIRD_PARTY_PACKAGE and STANDARD_JAVA_PACKAGE groups use
283  * thirdPartyPackageRegExp and standardPackageRegExp options.
284  * </p>
285  * <pre>
286  * &lt;module name=&quot;CustomImportOrder&quot;&gt;
287  *   &lt;property name=&quot;customImportOrderRules&quot;
288  *     value=&quot;STATIC###SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STANDARD_JAVA_PACKAGE&quot;/&gt;
289  *   &lt;property name=&quot;thirdPartyPackageRegExp&quot; value=&quot;^(com|org)\.&quot;/&gt;
290  *   &lt;property name=&quot;standardPackageRegExp&quot; value=&quot;^(java|javax)\.&quot;/&gt;
291  * &lt;/module&gt;
292  * </pre>
293  * <p>
294  * Also, this check can be configured to force empty line separator between
295  * import groups. For example.
296  * </p>
297  * <pre>
298  * &lt;module name=&quot;CustomImportOrder&quot;&gt;
299  *   &lt;property name=&quot;separateLineBetweenGroups&quot; value=&quot;true&quot;/&gt;
300  * &lt;/module&gt;
301  * </pre>
302  * <p>
303  * It is possible to enforce
304  * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>
305  * of imports in groups using the following configuration:
306  * </p>
307  * <pre>
308  * &lt;module name=&quot;CustomImportOrder&quot;&gt;
309  *   &lt;property name=&quot;sortImportsInGroupAlphabetically&quot; value=&quot;true&quot;/&gt;
310  * &lt;/module&gt;
311  * </pre>
312  * <p>
313  * Example of ASCII order:
314  * </p>
315  * <pre>
316  * import java.awt.Dialog;
317  * import java.awt.Window;
318  * import java.awt.color.ColorSpace;
319  * import java.awt.Frame; // violation here - in ASCII order 'F' should go before 'c',
320  *                        // as all uppercase come before lowercase letters
321  * </pre>
322  * <p>
323  * To force checking imports sequence such as:
324  * </p>
325  * <pre>
326  * package com.puppycrawl.tools.checkstyle.imports;
327  *
328  * import com.google.common.annotations.GwtCompatible;
329  * import com.google.common.annotations.Beta;
330  * import com.google.common.annotations.VisibleForTesting;
331  *
332  * import org.abego.treelayout.Configuration;
333  *
334  * import static sun.tools.util.ModifierFilter.ALL_ACCESS;
335  *
336  * import com.google.common.annotations.GwtCompatible; // violation here - should be in the
337  *                                                     // THIRD_PARTY_PACKAGE group
338  * import android.*;
339  * </pre>
340  * <p>
341  * configure as follows:
342  * </p>
343  * <pre>
344  * &lt;module name=&quot;CustomImportOrder&quot;&gt;
345  *   &lt;property name=&quot;customImportOrderRules&quot;
346  *     value=&quot;SAME_PACKAGE(3)###THIRD_PARTY_PACKAGE###STATIC###SPECIAL_IMPORTS&quot;/&gt;
347  *   &lt;property name=&quot;specialImportsRegExp&quot; value=&quot;^android\.&quot;/&gt;
348  * &lt;/module&gt;
349  * </pre>
350  *
351  * @since 5.8
352  */
353 @FileStatefulCheck
354 public class CustomImportOrderCheck extends AbstractCheck {
355 
356     /**
357      * A key is pointing to the warning message text in "messages.properties"
358      * file.
359      */
360     public static final String MSG_LINE_SEPARATOR = "custom.import.order.line.separator";
361 
362     /**
363      * A key is pointing to the warning message text in "messages.properties"
364      * file.
365      */
366     public static final String MSG_LEX = "custom.import.order.lex";
367 
368     /**
369      * A key is pointing to the warning message text in "messages.properties"
370      * file.
371      */
372     public static final String MSG_NONGROUP_IMPORT = "custom.import.order.nonGroup.import";
373 
374     /**
375      * A key is pointing to the warning message text in "messages.properties"
376      * file.
377      */
378     public static final String MSG_NONGROUP_EXPECTED = "custom.import.order.nonGroup.expected";
379 
380     /**
381      * A key is pointing to the warning message text in "messages.properties"
382      * file.
383      */
384     public static final String MSG_ORDER = "custom.import.order";
385 
386     /** STATIC group name. */
387     public static final String STATIC_RULE_GROUP = "STATIC";
388 
389     /** SAME_PACKAGE group name. */
390     public static final String SAME_PACKAGE_RULE_GROUP = "SAME_PACKAGE";
391 
392     /** THIRD_PARTY_PACKAGE group name. */
393     public static final String THIRD_PARTY_PACKAGE_RULE_GROUP = "THIRD_PARTY_PACKAGE";
394 
395     /** STANDARD_JAVA_PACKAGE group name. */
396     public static final String STANDARD_JAVA_PACKAGE_RULE_GROUP = "STANDARD_JAVA_PACKAGE";
397 
398     /** SPECIAL_IMPORTS group name. */
399     public static final String SPECIAL_IMPORTS_RULE_GROUP = "SPECIAL_IMPORTS";
400 
401     /** NON_GROUP group name. */
402     private static final String NON_GROUP_RULE_GROUP = "NOT_ASSIGNED_TO_ANY_GROUP";
403 
404     /** Pattern used to separate groups of imports. */
405     private static final Pattern GROUP_SEPARATOR_PATTERN = Pattern.compile("\\s*###\\s*");
406 
407     /** Specify list of order declaration customizing by user. */
408     private final List<String> customImportOrderRules = new ArrayList<>();
409 
410     /** Contains objects with import attributes. */
411     private final List<ImportDetails> importToGroupList = new ArrayList<>();
412 
413     /** Specify RegExp for SAME_PACKAGE group imports. */
414     private String samePackageDomainsRegExp = "";
415 
416     /** Specify RegExp for STANDARD_JAVA_PACKAGE group imports. */
417     private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\.");
418 
419     /** Specify RegExp for THIRD_PARTY_PACKAGE group imports. */
420     private Pattern thirdPartyPackageRegExp = Pattern.compile(".*");
421 
422     /** Specify RegExp for SPECIAL_IMPORTS group imports. */
423     private Pattern specialImportsRegExp = Pattern.compile("^$");
424 
425     /** Force empty line separator between import groups. */
426     private boolean separateLineBetweenGroups = true;
427 
428     /**
429      * Force grouping alphabetically,
430      * in <a href="https://en.wikipedia.org/wiki/ASCII#Order"> ASCII sort order</a>.
431      */
432     private boolean sortImportsInGroupAlphabetically;
433 
434     /** Number of first domains for SAME_PACKAGE group. */
435     private int samePackageMatchingDepth = 2;
436 
437     /**
438      * Setter to specify RegExp for STANDARD_JAVA_PACKAGE group imports.
439      * @param regexp
440      *        user value.
441      */
442     public final void setStandardPackageRegExp(Pattern regexp) {
443         standardPackageRegExp = regexp;
444     }
445 
446     /**
447      * Setter to specify RegExp for THIRD_PARTY_PACKAGE group imports.
448      * @param regexp
449      *        user value.
450      */
451     public final void setThirdPartyPackageRegExp(Pattern regexp) {
452         thirdPartyPackageRegExp = regexp;
453     }
454 
455     /**
456      * Setter to specify RegExp for SPECIAL_IMPORTS group imports.
457      * @param regexp
458      *        user value.
459      */
460     public final void setSpecialImportsRegExp(Pattern regexp) {
461         specialImportsRegExp = regexp;
462     }
463 
464     /**
465      * Setter to force empty line separator between import groups.
466      * @param value
467      *        user value.
468      */
469     public final void setSeparateLineBetweenGroups(boolean value) {
470         separateLineBetweenGroups = value;
471     }
472 
473     /**
474      * Setter to force grouping alphabetically, in
475      * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>.
476      * @param value
477      *        user value.
478      */
479     public final void setSortImportsInGroupAlphabetically(boolean value) {
480         sortImportsInGroupAlphabetically = value;
481     }
482 
483     /**
484      * Setter to specify list of order declaration customizing by user.
485      * @param inputCustomImportOrder
486      *        user value.
487      */
488     public final void setCustomImportOrderRules(final String inputCustomImportOrder) {
489         for (String currentState : GROUP_SEPARATOR_PATTERN.split(inputCustomImportOrder)) {
490             addRulesToList(currentState);
491         }
492         customImportOrderRules.add(NON_GROUP_RULE_GROUP);
493     }
494 
495     @Override
496     public int[] getDefaultTokens() {
497         return getRequiredTokens();
498     }
499 
500     @Override
501     public int[] getAcceptableTokens() {
502         return getRequiredTokens();
503     }
504 
505     @Override
506     public int[] getRequiredTokens() {
507         return new int[] {
508             TokenTypes.IMPORT,
509             TokenTypes.STATIC_IMPORT,
510             TokenTypes.PACKAGE_DEF,
511         };
512     }
513 
514     @Override
515     public void beginTree(DetailAST rootAST) {
516         importToGroupList.clear();
517     }
518 
519     @Override
520     public void visitToken(DetailAST ast) {
521         if (ast.getType() == TokenTypes.PACKAGE_DEF) {
522             if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
523                 samePackageDomainsRegExp = createSamePackageRegexp(
524                         samePackageMatchingDepth, ast);
525             }
526         }
527         else {
528             final String importFullPath = getFullImportIdent(ast);
529             final int lineNo = ast.getLineNo();
530             final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT;
531             importToGroupList.add(new ImportDetails(importFullPath,
532                     lineNo, getImportGroup(isStatic, importFullPath),
533                     isStatic));
534         }
535     }
536 
537     @Override
538     public void finishTree(DetailAST rootAST) {
539         if (!importToGroupList.isEmpty()) {
540             finishImportList();
541         }
542     }
543 
544     /** Examine the order of all the imports and log any violations. */
545     private void finishImportList() {
546         final ImportDetails firstImport = importToGroupList.get(0);
547         String currentGroup = getImportGroup(firstImport.isStaticImport(),
548                 firstImport.getImportFullPath());
549         int currentGroupNumber = customImportOrderRules.indexOf(currentGroup);
550         String previousImportFromCurrentGroup = null;
551 
552         for (ImportDetails importObject : importToGroupList) {
553             final String importGroup = importObject.getImportGroup();
554             final String fullImportIdent = importObject.getImportFullPath();
555 
556             if (getCountOfEmptyLinesBefore(importObject.getLineNumber()) > 1) {
557                 log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent);
558             }
559             if (importGroup.equals(currentGroup)) {
560                 if (sortImportsInGroupAlphabetically
561                         && previousImportFromCurrentGroup != null
562                         && compareImports(fullImportIdent, previousImportFromCurrentGroup) < 0) {
563                     log(importObject.getLineNumber(), MSG_LEX,
564                             fullImportIdent, previousImportFromCurrentGroup);
565                 }
566                 else {
567                     previousImportFromCurrentGroup = fullImportIdent;
568                 }
569             }
570             else {
571                 //not the last group, last one is always NON_GROUP
572                 if (customImportOrderRules.size() > currentGroupNumber + 1) {
573                     final String nextGroup = getNextImportGroup(currentGroupNumber + 1);
574                     if (importGroup.equals(nextGroup)) {
575                         if (separateLineBetweenGroups
576                                 && getCountOfEmptyLinesBefore(importObject.getLineNumber()) == 0) {
577                             log(importObject.getLineNumber(), MSG_LINE_SEPARATOR, fullImportIdent);
578                         }
579                         currentGroup = nextGroup;
580                         currentGroupNumber = customImportOrderRules.indexOf(nextGroup);
581                         previousImportFromCurrentGroup = fullImportIdent;
582                     }
583                     else {
584                         logWrongImportGroupOrder(importObject.getLineNumber(),
585                                 importGroup, nextGroup, fullImportIdent);
586                     }
587                 }
588                 else {
589                     logWrongImportGroupOrder(importObject.getLineNumber(),
590                             importGroup, currentGroup, fullImportIdent);
591                 }
592             }
593         }
594     }
595 
596     /**
597      * Log wrong import group order.
598      * @param currentImportLine
599      *        line number of current import current import.
600      * @param importGroup
601      *        import group.
602      * @param currentGroupNumber
603      *        current group number we are checking.
604      * @param fullImportIdent
605      *        full import name.
606      */
607     private void logWrongImportGroupOrder(int currentImportLine, String importGroup,
608             String currentGroupNumber, String fullImportIdent) {
609         if (NON_GROUP_RULE_GROUP.equals(importGroup)) {
610             log(currentImportLine, MSG_NONGROUP_IMPORT, fullImportIdent);
611         }
612         else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) {
613             log(currentImportLine, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent);
614         }
615         else {
616             log(currentImportLine, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent);
617         }
618     }
619 
620     /**
621      * Get next import group.
622      * @param currentGroupNumber
623      *        current group number.
624      * @return
625      *        next import group.
626      */
627     private String getNextImportGroup(int currentGroupNumber) {
628         int nextGroupNumber = currentGroupNumber;
629 
630         while (customImportOrderRules.size() > nextGroupNumber + 1) {
631             if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) {
632                 break;
633             }
634             nextGroupNumber++;
635         }
636         return customImportOrderRules.get(nextGroupNumber);
637     }
638 
639     /**
640      * Checks if current group contains any import.
641      * @param currentGroup
642      *        current group.
643      * @return
644      *        true, if current group contains at least one import.
645      */
646     private boolean hasAnyImportInCurrentGroup(String currentGroup) {
647         boolean result = false;
648         for (ImportDetails currentImport : importToGroupList) {
649             if (currentGroup.equals(currentImport.getImportGroup())) {
650                 result = true;
651                 break;
652             }
653         }
654         return result;
655     }
656 
657     /**
658      * Get import valid group.
659      * @param isStatic
660      *        is static import.
661      * @param importPath
662      *        full import path.
663      * @return import valid group.
664      */
665     private String getImportGroup(boolean isStatic, String importPath) {
666         RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0);
667         if (isStatic && customImportOrderRules.contains(STATIC_RULE_GROUP)) {
668             bestMatch.group = STATIC_RULE_GROUP;
669             bestMatch.matchLength = importPath.length();
670         }
671         else if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
672             final String importPathTrimmedToSamePackageDepth =
673                     getFirstDomainsFromIdent(samePackageMatchingDepth, importPath);
674             if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) {
675                 bestMatch.group = SAME_PACKAGE_RULE_GROUP;
676                 bestMatch.matchLength = importPath.length();
677             }
678         }
679         if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)) {
680             for (String group : customImportOrderRules) {
681                 if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) {
682                     bestMatch = findBetterPatternMatch(importPath,
683                             STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch);
684                 }
685                 if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) {
686                     bestMatch = findBetterPatternMatch(importPath,
687                             group, specialImportsRegExp, bestMatch);
688                 }
689             }
690         }
691         if (bestMatch.group.equals(NON_GROUP_RULE_GROUP)
692                 && customImportOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP)
693                 && thirdPartyPackageRegExp.matcher(importPath).find()) {
694             bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP;
695         }
696         return bestMatch.group;
697     }
698 
699     /** Tries to find better matching regular expression:
700      * longer matching substring wins; in case of the same length,
701      * lower position of matching substring wins.
702      * @param importPath
703      *      Full import identifier
704      * @param group
705      *      Import group we are trying to assign the import
706      * @param regExp
707      *      Regular expression for import group
708      * @param currentBestMatch
709      *      object with currently best match
710      * @return better match (if found) or the same (currentBestMatch)
711      */
712     private static RuleMatchForImport findBetterPatternMatch(String importPath, String group,
713             Pattern regExp, RuleMatchForImport currentBestMatch) {
714         RuleMatchForImport betterMatchCandidate = currentBestMatch;
715         final Matcher matcher = regExp.matcher(importPath);
716         while (matcher.find()) {
717             final int length = matcher.end() - matcher.start();
718             if (length > betterMatchCandidate.matchLength
719                     || length == betterMatchCandidate.matchLength
720                         && matcher.start() < betterMatchCandidate.matchPosition) {
721                 betterMatchCandidate = new RuleMatchForImport(group, length, matcher.start());
722             }
723         }
724         return betterMatchCandidate;
725     }
726 
727     /**
728      * Checks compare two import paths.
729      * @param import1
730      *        current import.
731      * @param import2
732      *        previous import.
733      * @return a negative integer, zero, or a positive integer as the
734      *        specified String is greater than, equal to, or less
735      *        than this String, ignoring case considerations.
736      */
737     private static int compareImports(String import1, String import2) {
738         int result = 0;
739         final String separator = "\\.";
740         final String[] import1Tokens = import1.split(separator);
741         final String[] import2Tokens = import2.split(separator);
742         for (int i = 0; i != import1Tokens.length && i != import2Tokens.length; i++) {
743             final String import1Token = import1Tokens[i];
744             final String import2Token = import2Tokens[i];
745             result = import1Token.compareTo(import2Token);
746             if (result != 0) {
747                 break;
748             }
749         }
750         if (result == 0) {
751             result = Integer.compare(import1Tokens.length, import2Tokens.length);
752         }
753         return result;
754     }
755 
756     /**
757      * Counts empty lines before given.
758      * @param lineNo
759      *        Line number of current import.
760      * @return count of empty lines before given.
761      */
762     private int getCountOfEmptyLinesBefore(int lineNo) {
763         int result = 0;
764         final String[] lines = getLines();
765         //  [lineNo - 2] is the number of the previous line
766         //  because the numbering starts from zero.
767         int lineBeforeIndex = lineNo - 2;
768         while (lineBeforeIndex >= 0
769                 && CommonUtil.isBlank(lines[lineBeforeIndex])) {
770             lineBeforeIndex--;
771             result++;
772         }
773         return result;
774     }
775 
776     /**
777      * Forms import full path.
778      * @param token
779      *        current token.
780      * @return full path or null.
781      */
782     private static String getFullImportIdent(DetailAST token) {
783         String ident = "";
784         if (token != null) {
785             ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText();
786         }
787         return ident;
788     }
789 
790     /**
791      * Parses ordering rule and adds it to the list with rules.
792      * @param ruleStr
793      *        String with rule.
794      */
795     private void addRulesToList(String ruleStr) {
796         if (STATIC_RULE_GROUP.equals(ruleStr)
797                 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr)
798                 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr)
799                 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) {
800             customImportOrderRules.add(ruleStr);
801         }
802         else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) {
803             final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1,
804                     ruleStr.indexOf(')'));
805             samePackageMatchingDepth = Integer.parseInt(rule);
806             if (samePackageMatchingDepth <= 0) {
807                 throw new IllegalArgumentException(
808                         "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr);
809             }
810             customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP);
811         }
812         else {
813             throw new IllegalStateException("Unexpected rule: " + ruleStr);
814         }
815     }
816 
817     /**
818      * Creates samePackageDomainsRegExp of the first package domains.
819      * @param firstPackageDomainsCount
820      *        number of first package domains.
821      * @param packageNode
822      *        package node.
823      * @return same package regexp.
824      */
825     private static String createSamePackageRegexp(int firstPackageDomainsCount,
826              DetailAST packageNode) {
827         final String packageFullPath = getFullImportIdent(packageNode);
828         return getFirstDomainsFromIdent(firstPackageDomainsCount, packageFullPath);
829     }
830 
831     /**
832      * Extracts defined amount of domains from the left side of package/import identifier.
833      * @param firstPackageDomainsCount
834      *        number of first package domains.
835      * @param packageFullPath
836      *        full identifier containing path to package or imported object.
837      * @return String with defined amount of domains or full identifier
838      *        (if full identifier had less domain than specified)
839      */
840     private static String getFirstDomainsFromIdent(
841             final int firstPackageDomainsCount, final String packageFullPath) {
842         final StringBuilder builder = new StringBuilder(256);
843         final StringTokenizer tokens = new StringTokenizer(packageFullPath, ".");
844         int count = firstPackageDomainsCount;
845 
846         while (count > 0 && tokens.hasMoreTokens()) {
847             builder.append(tokens.nextToken()).append('.');
848             count--;
849         }
850         return builder.toString();
851     }
852 
853     /**
854      * Contains import attributes as line number, import full path, import
855      * group.
856      */
857     private static class ImportDetails {
858 
859         /** Import full path. */
860         private final String importFullPath;
861 
862         /** Import line number. */
863         private final int lineNumber;
864 
865         /** Import group. */
866         private final String importGroup;
867 
868         /** Is static import. */
869         private final boolean staticImport;
870 
871         /**
872          * Initialise importFullPath, lineNumber, importGroup, staticImport.
873          * @param importFullPath
874          *        import full path.
875          * @param lineNumber
876          *        import line number.
877          * @param importGroup
878          *        import group.
879          * @param staticImport
880          *        if import is static.
881          */
882         /* package */ ImportDetails(String importFullPath,
883                 int lineNumber, String importGroup, boolean staticImport) {
884             this.importFullPath = importFullPath;
885             this.lineNumber = lineNumber;
886             this.importGroup = importGroup;
887             this.staticImport = staticImport;
888         }
889 
890         /**
891          * Get import full path variable.
892          * @return import full path variable.
893          */
894         public String getImportFullPath() {
895             return importFullPath;
896         }
897 
898         /**
899          * Get import line number.
900          * @return import line.
901          */
902         public int getLineNumber() {
903             return lineNumber;
904         }
905 
906         /**
907          * Get import group.
908          * @return import group.
909          */
910         public String getImportGroup() {
911             return importGroup;
912         }
913 
914         /**
915          * Checks if import is static.
916          * @return true, if import is static.
917          */
918         public boolean isStaticImport() {
919             return staticImport;
920         }
921 
922     }
923 
924     /**
925      * Contains matching attributes assisting in definition of "best matching"
926      * group for import.
927      */
928     private static class RuleMatchForImport {
929 
930         /** Position of matching string for current best match. */
931         private final int matchPosition;
932         /** Length of matching string for current best match. */
933         private int matchLength;
934         /** Import group for current best match. */
935         private String group;
936 
937         /** Constructor to initialize the fields.
938          * @param group
939          *        Matched group.
940          * @param length
941          *        Matching length.
942          * @param position
943          *        Matching position.
944          */
945         /* package */ RuleMatchForImport(String group, int length, int position) {
946             this.group = group;
947             matchLength = length;
948             matchPosition = position;
949         }
950 
951     }
952 
953 }