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