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  * <ul>
153  * <li>
154  * Property {@code customImportOrderRules} - Specify ordered list of import groups.
155  * Type is {@code java.lang.String[]}.
156  * Default value is {@code ""}.
157  * </li>
158  * <li>
159  * Property {@code separateLineBetweenGroups} - Force empty line separator between
160  * import groups.
161  * Type is {@code boolean}.
162  * Default value is {@code true}.
163  * </li>
164  * <li>
165  * Property {@code sortImportsInGroupAlphabetically} - Force grouping alphabetically,
166  * in <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>.
167  * Type is {@code boolean}.
168  * Default value is {@code false}.
169  * </li>
170  * <li>
171  * Property {@code specialImportsRegExp} - Specify RegExp for SPECIAL_IMPORTS group imports.
172  * Type is {@code java.util.regex.Pattern}.
173  * Default value is {@code "^$"}.
174  * </li>
175  * <li>
176  * Property {@code standardPackageRegExp} - Specify RegExp for STANDARD_JAVA_PACKAGE group imports.
177  * Type is {@code java.util.regex.Pattern}.
178  * Default value is {@code "^(java|javax)\."}.
179  * </li>
180  * <li>
181  * Property {@code thirdPartyPackageRegExp} - Specify RegExp for THIRD_PARTY_PACKAGE group imports.
182  * Type is {@code java.util.regex.Pattern}.
183  * Default value is {@code ".*"}.
184  * </li>
185  * </ul>
186  *
187  * <p>
188  * Parent is {@code com.puppycrawl.tools.checkstyle.TreeWalker}
189  * </p>
190  *
191  * <p>
192  * Violation Message Keys:
193  * </p>
194  * <ul>
195  * <li>
196  * {@code custom.import.order}
197  * </li>
198  * <li>
199  * {@code custom.import.order.lex}
200  * </li>
201  * <li>
202  * {@code custom.import.order.line.separator}
203  * </li>
204  * <li>
205  * {@code custom.import.order.nonGroup.expected}
206  * </li>
207  * <li>
208  * {@code custom.import.order.nonGroup.import}
209  * </li>
210  * <li>
211  * {@code custom.import.order.separated.internally}
212  * </li>
213  * </ul>
214  *
215  * @since 5.8
216  */
217 @FileStatefulCheck
218 public class CustomImportOrderCheck extends AbstractCheck {
219 
220     /**
221      * A key is pointing to the warning message text in "messages.properties"
222      * file.
223      */
224     public static final String MSG_LINE_SEPARATOR = "custom.import.order.line.separator";
225 
226     /**
227      * A key is pointing to the warning message text in "messages.properties"
228      * file.
229      */
230     public static final String MSG_SEPARATED_IN_GROUP = "custom.import.order.separated.internally";
231 
232     /**
233      * A key is pointing to the warning message text in "messages.properties"
234      * file.
235      */
236     public static final String MSG_LEX = "custom.import.order.lex";
237 
238     /**
239      * A key is pointing to the warning message text in "messages.properties"
240      * file.
241      */
242     public static final String MSG_NONGROUP_IMPORT = "custom.import.order.nonGroup.import";
243 
244     /**
245      * A key is pointing to the warning message text in "messages.properties"
246      * file.
247      */
248     public static final String MSG_NONGROUP_EXPECTED = "custom.import.order.nonGroup.expected";
249 
250     /**
251      * A key is pointing to the warning message text in "messages.properties"
252      * file.
253      */
254     public static final String MSG_ORDER = "custom.import.order";
255 
256     /** STATIC group name. */
257     public static final String STATIC_RULE_GROUP = "STATIC";
258 
259     /** SAME_PACKAGE group name. */
260     public static final String SAME_PACKAGE_RULE_GROUP = "SAME_PACKAGE";
261 
262     /** THIRD_PARTY_PACKAGE group name. */
263     public static final String THIRD_PARTY_PACKAGE_RULE_GROUP = "THIRD_PARTY_PACKAGE";
264 
265     /** STANDARD_JAVA_PACKAGE group name. */
266     public static final String STANDARD_JAVA_PACKAGE_RULE_GROUP = "STANDARD_JAVA_PACKAGE";
267 
268     /** SPECIAL_IMPORTS group name. */
269     public static final String SPECIAL_IMPORTS_RULE_GROUP = "SPECIAL_IMPORTS";
270 
271     /** NON_GROUP group name. */
272     private static final String NON_GROUP_RULE_GROUP = "NOT_ASSIGNED_TO_ANY_GROUP";
273 
274     /** Pattern used to separate groups of imports. */
275     private static final Pattern GROUP_SEPARATOR_PATTERN = Pattern.compile("\\s*###\\s*");
276 
277     /** Specify ordered list of import groups. */
278     private final List<String> customImportOrderRules = new ArrayList<>();
279 
280     /** Contains objects with import attributes. */
281     private final List<ImportDetails> importToGroupList = new ArrayList<>();
282 
283     /** Specify RegExp for SAME_PACKAGE group imports. */
284     private String samePackageDomainsRegExp = "";
285 
286     /** Specify RegExp for STANDARD_JAVA_PACKAGE group imports. */
287     private Pattern standardPackageRegExp = Pattern.compile("^(java|javax)\\.");
288 
289     /** Specify RegExp for THIRD_PARTY_PACKAGE group imports. */
290     private Pattern thirdPartyPackageRegExp = Pattern.compile(".*");
291 
292     /** Specify RegExp for SPECIAL_IMPORTS group imports. */
293     private Pattern specialImportsRegExp = Pattern.compile("^$");
294 
295     /** Force empty line separator between import groups. */
296     private boolean separateLineBetweenGroups = true;
297 
298     /**
299      * Force grouping alphabetically,
300      * in <a href="https://en.wikipedia.org/wiki/ASCII#Order"> ASCII sort order</a>.
301      */
302     private boolean sortImportsInGroupAlphabetically;
303 
304     /** Number of first domains for SAME_PACKAGE group. */
305     private int samePackageMatchingDepth;
306 
307     /**
308      * Setter to specify RegExp for STANDARD_JAVA_PACKAGE group imports.
309      *
310      * @param regexp
311      *        user value.
312      * @since 5.8
313      */
314     public final void setStandardPackageRegExp(Pattern regexp) {
315         standardPackageRegExp = regexp;
316     }
317 
318     /**
319      * Setter to specify RegExp for THIRD_PARTY_PACKAGE group imports.
320      *
321      * @param regexp
322      *        user value.
323      * @since 5.8
324      */
325     public final void setThirdPartyPackageRegExp(Pattern regexp) {
326         thirdPartyPackageRegExp = regexp;
327     }
328 
329     /**
330      * Setter to specify RegExp for SPECIAL_IMPORTS group imports.
331      *
332      * @param regexp
333      *        user value.
334      * @since 5.8
335      */
336     public final void setSpecialImportsRegExp(Pattern regexp) {
337         specialImportsRegExp = regexp;
338     }
339 
340     /**
341      * Setter to force empty line separator between import groups.
342      *
343      * @param value
344      *        user value.
345      * @since 5.8
346      */
347     public final void setSeparateLineBetweenGroups(boolean value) {
348         separateLineBetweenGroups = value;
349     }
350 
351     /**
352      * Setter to force grouping alphabetically, in
353      * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>.
354      *
355      * @param value
356      *        user value.
357      * @since 5.8
358      */
359     public final void setSortImportsInGroupAlphabetically(boolean value) {
360         sortImportsInGroupAlphabetically = value;
361     }
362 
363     /**
364      * Setter to specify ordered list of import groups.
365      *
366      * @param rules
367      *        user value.
368      * @since 5.8
369      */
370     public final void setCustomImportOrderRules(String... rules) {
371         Arrays.stream(rules)
372                 .map(GROUP_SEPARATOR_PATTERN::split)
373                 .flatMap(Arrays::stream)
374                 .forEach(this::addRulesToList);
375 
376         customImportOrderRules.add(NON_GROUP_RULE_GROUP);
377     }
378 
379     @Override
380     public int[] getDefaultTokens() {
381         return getRequiredTokens();
382     }
383 
384     @Override
385     public int[] getAcceptableTokens() {
386         return getRequiredTokens();
387     }
388 
389     @Override
390     public int[] getRequiredTokens() {
391         return new int[] {
392             TokenTypes.IMPORT,
393             TokenTypes.STATIC_IMPORT,
394             TokenTypes.PACKAGE_DEF,
395         };
396     }
397 
398     @Override
399     public void beginTree(DetailAST rootAST) {
400         importToGroupList.clear();
401     }
402 
403     @Override
404     public void visitToken(DetailAST ast) {
405         if (ast.getType() == TokenTypes.PACKAGE_DEF) {
406             samePackageDomainsRegExp = createSamePackageRegexp(
407                     samePackageMatchingDepth, ast);
408         }
409         else {
410             final String importFullPath = getFullImportIdent(ast);
411             final boolean isStatic = ast.getType() == TokenTypes.STATIC_IMPORT;
412             importToGroupList.add(new ImportDetails(importFullPath,
413                     getImportGroup(isStatic, importFullPath), isStatic, ast));
414         }
415     }
416 
417     @Override
418     public void finishTree(DetailAST rootAST) {
419         if (!importToGroupList.isEmpty()) {
420             finishImportList();
421         }
422     }
423 
424     /** Examine the order of all the imports and log any violations. */
425     private void finishImportList() {
426         String currentGroup = getFirstGroup();
427         int currentGroupNumber = customImportOrderRules.lastIndexOf(currentGroup);
428         ImportDetails previousImportObjectFromCurrentGroup = null;
429         String previousImportFromCurrentGroup = null;
430 
431         for (ImportDetails importObject : importToGroupList) {
432             final String importGroup = importObject.getImportGroup();
433             final String fullImportIdent = importObject.getImportFullPath();
434 
435             if (importGroup.equals(currentGroup)) {
436                 validateExtraEmptyLine(previousImportObjectFromCurrentGroup,
437                         importObject, fullImportIdent);
438                 if (isAlphabeticalOrderBroken(previousImportFromCurrentGroup, fullImportIdent)) {
439                     log(importObject.getImportAST(), MSG_LEX,
440                             fullImportIdent, previousImportFromCurrentGroup);
441                 }
442                 else {
443                     previousImportFromCurrentGroup = fullImportIdent;
444                 }
445                 previousImportObjectFromCurrentGroup = importObject;
446             }
447             else {
448                 // not the last group, last one is always NON_GROUP
449                 if (customImportOrderRules.size() > currentGroupNumber + 1) {
450                     final String nextGroup = getNextImportGroup(currentGroupNumber + 1);
451                     if (importGroup.equals(nextGroup)) {
452                         validateMissedEmptyLine(previousImportObjectFromCurrentGroup,
453                                 importObject, fullImportIdent);
454                         currentGroup = nextGroup;
455                         currentGroupNumber = customImportOrderRules.lastIndexOf(nextGroup);
456                         previousImportFromCurrentGroup = fullImportIdent;
457                     }
458                     else {
459                         logWrongImportGroupOrder(importObject.getImportAST(),
460                                 importGroup, nextGroup, fullImportIdent);
461                     }
462                     previousImportObjectFromCurrentGroup = importObject;
463                 }
464                 else {
465                     logWrongImportGroupOrder(importObject.getImportAST(),
466                             importGroup, currentGroup, fullImportIdent);
467                 }
468             }
469         }
470     }
471 
472     /**
473      * Log violation if empty line is missed.
474      *
475      * @param previousImport previous import from current group.
476      * @param importObject current import.
477      * @param fullImportIdent full import identifier.
478      */
479     private void validateMissedEmptyLine(ImportDetails previousImport,
480                                          ImportDetails importObject, String fullImportIdent) {
481         if (isEmptyLineMissed(previousImport, importObject)) {
482             log(importObject.getImportAST(), MSG_LINE_SEPARATOR, fullImportIdent);
483         }
484     }
485 
486     /**
487      * Log violation if extra empty line is present.
488      *
489      * @param previousImport previous import from current group.
490      * @param importObject current import.
491      * @param fullImportIdent full import identifier.
492      */
493     private void validateExtraEmptyLine(ImportDetails previousImport,
494                                         ImportDetails importObject, String fullImportIdent) {
495         if (isSeparatedByExtraEmptyLine(previousImport, importObject)) {
496             log(importObject.getImportAST(), MSG_SEPARATED_IN_GROUP, fullImportIdent);
497         }
498     }
499 
500     /**
501      * Get first import group.
502      *
503      * @return
504      *        first import group of file.
505      */
506     private String getFirstGroup() {
507         final ImportDetails firstImport = importToGroupList.get(0);
508         return getImportGroup(firstImport.isStaticImport(),
509                 firstImport.getImportFullPath());
510     }
511 
512     /**
513      * Examine alphabetical order of imports.
514      *
515      * @param previousImport
516      *        previous import of current group.
517      * @param currentImport
518      *        current import.
519      * @return
520      *        true, if previous and current import are not in alphabetical order.
521      */
522     private boolean isAlphabeticalOrderBroken(String previousImport,
523                                               String currentImport) {
524         return sortImportsInGroupAlphabetically
525                 && previousImport != null
526                 && compareImports(currentImport, previousImport) < 0;
527     }
528 
529     /**
530      * Examine empty lines between groups.
531      *
532      * @param previousImportObject
533      *        previous import in current group.
534      * @param currentImportObject
535      *        current import.
536      * @return
537      *        true, if current import NOT separated from previous import by empty line.
538      */
539     private boolean isEmptyLineMissed(ImportDetails previousImportObject,
540                                       ImportDetails currentImportObject) {
541         return separateLineBetweenGroups
542                 && getCountOfEmptyLinesBetween(
543                      previousImportObject.getEndLineNumber(),
544                      currentImportObject.getStartLineNumber()) != 1;
545     }
546 
547     /**
548      * Examine that imports separated by more than one empty line.
549      *
550      * @param previousImportObject
551      *        previous import in current group.
552      * @param currentImportObject
553      *        current import.
554      * @return
555      *        true, if current import separated from previous by more than one empty line.
556      */
557     private boolean isSeparatedByExtraEmptyLine(ImportDetails previousImportObject,
558                                                 ImportDetails currentImportObject) {
559         return previousImportObject != null
560                 && getCountOfEmptyLinesBetween(
561                      previousImportObject.getEndLineNumber(),
562                      currentImportObject.getStartLineNumber()) > 0;
563     }
564 
565     /**
566      * Log wrong import group order.
567      *
568      * @param importAST
569      *        import ast.
570      * @param importGroup
571      *        import group.
572      * @param currentGroupNumber
573      *        current group number we are checking.
574      * @param fullImportIdent
575      *        full import name.
576      */
577     private void logWrongImportGroupOrder(DetailAST importAST, String importGroup,
578             String currentGroupNumber, String fullImportIdent) {
579         if (NON_GROUP_RULE_GROUP.equals(importGroup)) {
580             log(importAST, MSG_NONGROUP_IMPORT, fullImportIdent);
581         }
582         else if (NON_GROUP_RULE_GROUP.equals(currentGroupNumber)) {
583             log(importAST, MSG_NONGROUP_EXPECTED, importGroup, fullImportIdent);
584         }
585         else {
586             log(importAST, MSG_ORDER, importGroup, currentGroupNumber, fullImportIdent);
587         }
588     }
589 
590     /**
591      * Get next import group.
592      *
593      * @param currentGroupNumber
594      *        current group number.
595      * @return
596      *        next import group.
597      */
598     private String getNextImportGroup(int currentGroupNumber) {
599         int nextGroupNumber = currentGroupNumber;
600 
601         while (customImportOrderRules.size() > nextGroupNumber + 1) {
602             if (hasAnyImportInCurrentGroup(customImportOrderRules.get(nextGroupNumber))) {
603                 break;
604             }
605             nextGroupNumber++;
606         }
607         return customImportOrderRules.get(nextGroupNumber);
608     }
609 
610     /**
611      * Checks if current group contains any import.
612      *
613      * @param currentGroup
614      *        current group.
615      * @return
616      *        true, if current group contains at least one import.
617      */
618     private boolean hasAnyImportInCurrentGroup(String currentGroup) {
619         boolean result = false;
620         for (ImportDetails currentImport : importToGroupList) {
621             if (currentGroup.equals(currentImport.getImportGroup())) {
622                 result = true;
623                 break;
624             }
625         }
626         return result;
627     }
628 
629     /**
630      * Get import valid group.
631      *
632      * @param isStatic
633      *        is static import.
634      * @param importPath
635      *        full import path.
636      * @return import valid group.
637      */
638     private String getImportGroup(boolean isStatic, String importPath) {
639         RuleMatchForImport bestMatch = new RuleMatchForImport(NON_GROUP_RULE_GROUP, 0, 0);
640         if (isStatic && customImportOrderRules.contains(STATIC_RULE_GROUP)) {
641             bestMatch.group = STATIC_RULE_GROUP;
642             bestMatch.matchLength = importPath.length();
643         }
644         else if (customImportOrderRules.contains(SAME_PACKAGE_RULE_GROUP)) {
645             final String importPathTrimmedToSamePackageDepth =
646                     getFirstDomainsFromIdent(samePackageMatchingDepth, importPath);
647             if (samePackageDomainsRegExp.equals(importPathTrimmedToSamePackageDepth)) {
648                 bestMatch.group = SAME_PACKAGE_RULE_GROUP;
649                 bestMatch.matchLength = importPath.length();
650             }
651         }
652         for (String group : customImportOrderRules) {
653             if (STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(group)) {
654                 bestMatch = findBetterPatternMatch(importPath,
655                         STANDARD_JAVA_PACKAGE_RULE_GROUP, standardPackageRegExp, bestMatch);
656             }
657             if (SPECIAL_IMPORTS_RULE_GROUP.equals(group)) {
658                 bestMatch = findBetterPatternMatch(importPath,
659                         group, specialImportsRegExp, bestMatch);
660             }
661         }
662 
663         if (NON_GROUP_RULE_GROUP.equals(bestMatch.group)
664                 && customImportOrderRules.contains(THIRD_PARTY_PACKAGE_RULE_GROUP)
665                 && thirdPartyPackageRegExp.matcher(importPath).find()) {
666             bestMatch.group = THIRD_PARTY_PACKAGE_RULE_GROUP;
667         }
668         return bestMatch.group;
669     }
670 
671     /**
672      * Tries to find better matching regular expression:
673      * longer matching substring wins; in case of the same length,
674      * lower position of matching substring wins.
675      *
676      * @param importPath
677      *      Full import identifier
678      * @param group
679      *      Import group we are trying to assign the import
680      * @param regExp
681      *      Regular expression for import group
682      * @param currentBestMatch
683      *      object with currently best match
684      * @return better match (if found) or the same (currentBestMatch)
685      */
686     private static RuleMatchForImport findBetterPatternMatch(String importPath, String group,
687             Pattern regExp, RuleMatchForImport currentBestMatch) {
688         RuleMatchForImport betterMatchCandidate = currentBestMatch;
689         final Matcher matcher = regExp.matcher(importPath);
690         while (matcher.find()) {
691             final int matchStart = matcher.start();
692             final int length = matcher.end() - matchStart;
693             if (length > betterMatchCandidate.matchLength
694                     || length == betterMatchCandidate.matchLength
695                         && matchStart < betterMatchCandidate.matchPosition) {
696                 betterMatchCandidate = new RuleMatchForImport(group, length, matchStart);
697             }
698         }
699         return betterMatchCandidate;
700     }
701 
702     /**
703      * Checks compare two import paths.
704      *
705      * @param import1
706      *        current import.
707      * @param import2
708      *        previous import.
709      * @return a negative integer, zero, or a positive integer as the
710      *        specified String is greater than, equal to, or less
711      *        than this String, ignoring case considerations.
712      */
713     private static int compareImports(String import1, String import2) {
714         int result = 0;
715         final String separator = "\\.";
716         final String[] import1Tokens = import1.split(separator);
717         final String[] import2Tokens = import2.split(separator);
718         for (int i = 0; i != import1Tokens.length && i != import2Tokens.length; i++) {
719             final String import1Token = import1Tokens[i];
720             final String import2Token = import2Tokens[i];
721             result = import1Token.compareTo(import2Token);
722             if (result != 0) {
723                 break;
724             }
725         }
726         if (result == 0) {
727             result = Integer.compare(import1Tokens.length, import2Tokens.length);
728         }
729         return result;
730     }
731 
732     /**
733      * Counts empty lines between given parameters.
734      *
735      * @param fromLineNo
736      *        One-based line number of previous import.
737      * @param toLineNo
738      *        One-based line number of current import.
739      * @return count of empty lines between given parameters, exclusive,
740      *        eg., (fromLineNo, toLineNo).
741      */
742     private int getCountOfEmptyLinesBetween(int fromLineNo, int toLineNo) {
743         int result = 0;
744         final String[] lines = getLines();
745 
746         for (int i = fromLineNo + 1; i <= toLineNo - 1; i++) {
747             // "- 1" because the numbering is one-based
748             if (CommonUtil.isBlank(lines[i - 1])) {
749                 result++;
750             }
751         }
752         return result;
753     }
754 
755     /**
756      * Forms import full path.
757      *
758      * @param token
759      *        current token.
760      * @return full path or null.
761      */
762     private static String getFullImportIdent(DetailAST token) {
763         String ident = "";
764         if (token != null) {
765             ident = FullIdent.createFullIdent(token.findFirstToken(TokenTypes.DOT)).getText();
766         }
767         return ident;
768     }
769 
770     /**
771      * Parses ordering rule and adds it to the list with rules.
772      *
773      * @param ruleStr
774      *        String with rule.
775      * @throws IllegalArgumentException when SAME_PACKAGE rule parameter is not positive integer
776      * @throws IllegalStateException when ruleStr is unexpected value
777      */
778     private void addRulesToList(String ruleStr) {
779         if (STATIC_RULE_GROUP.equals(ruleStr)
780                 || THIRD_PARTY_PACKAGE_RULE_GROUP.equals(ruleStr)
781                 || STANDARD_JAVA_PACKAGE_RULE_GROUP.equals(ruleStr)
782                 || SPECIAL_IMPORTS_RULE_GROUP.equals(ruleStr)) {
783             customImportOrderRules.add(ruleStr);
784         }
785         else if (ruleStr.startsWith(SAME_PACKAGE_RULE_GROUP)) {
786             final String rule = ruleStr.substring(ruleStr.indexOf('(') + 1,
787                     ruleStr.indexOf(')'));
788             samePackageMatchingDepth = Integer.parseInt(rule);
789             if (samePackageMatchingDepth <= 0) {
790                 throw new IllegalArgumentException(
791                         "SAME_PACKAGE rule parameter should be positive integer: " + ruleStr);
792             }
793             customImportOrderRules.add(SAME_PACKAGE_RULE_GROUP);
794         }
795         else {
796             throw new IllegalStateException("Unexpected rule: " + ruleStr);
797         }
798     }
799 
800     /**
801      * Creates samePackageDomainsRegExp of the first package domains.
802      *
803      * @param firstPackageDomainsCount
804      *        number of first package domains.
805      * @param packageNode
806      *        package node.
807      * @return same package regexp.
808      */
809     private static String createSamePackageRegexp(int firstPackageDomainsCount,
810              DetailAST packageNode) {
811         final String packageFullPath = getFullImportIdent(packageNode);
812         return getFirstDomainsFromIdent(firstPackageDomainsCount, packageFullPath);
813     }
814 
815     /**
816      * Extracts defined amount of domains from the left side of package/import identifier.
817      *
818      * @param firstPackageDomainsCount
819      *        number of first package domains.
820      * @param packageFullPath
821      *        full identifier containing path to package or imported object.
822      * @return String with defined amount of domains or full identifier
823      *        (if full identifier had less domain than specified)
824      */
825     private static String getFirstDomainsFromIdent(
826             final int firstPackageDomainsCount, final String packageFullPath) {
827         final StringBuilder builder = new StringBuilder(256);
828         final StringTokenizer tokens = new StringTokenizer(packageFullPath, ".");
829         int count = firstPackageDomainsCount;
830 
831         while (count > 0 && tokens.hasMoreTokens()) {
832             builder.append(tokens.nextToken());
833             count--;
834         }
835         return builder.toString();
836     }
837 
838     /**
839      * Contains import attributes as line number, import full path, import
840      * group.
841      */
842     private static final class ImportDetails {
843 
844         /** Import full path. */
845         private final String importFullPath;
846 
847         /** Import group. */
848         private final String importGroup;
849 
850         /** Is static import. */
851         private final boolean staticImport;
852 
853         /** Import AST. */
854         private final DetailAST importAST;
855 
856         /**
857          * Initialise importFullPath, importGroup, staticImport, importAST.
858          *
859          * @param importFullPath
860          *        import full path.
861          * @param importGroup
862          *        import group.
863          * @param staticImport
864          *        if import is static.
865          * @param importAST
866          *        import ast
867          */
868         private ImportDetails(String importFullPath, String importGroup, boolean staticImport,
869                                     DetailAST importAST) {
870             this.importFullPath = importFullPath;
871             this.importGroup = importGroup;
872             this.staticImport = staticImport;
873             this.importAST = importAST;
874         }
875 
876         /**
877          * Get import full path variable.
878          *
879          * @return import full path variable.
880          */
881         public String getImportFullPath() {
882             return importFullPath;
883         }
884 
885         /**
886          * Get import start line number from ast.
887          *
888          * @return import start line from ast.
889          */
890         public int getStartLineNumber() {
891             return importAST.getLineNo();
892         }
893 
894         /**
895          * Get import end line number from ast.
896          *
897          * <p>
898          * <b>Note:</b> It can be different from <b>startLineNumber</b> when import statement span
899          * multiple lines.
900          * </p>
901          *
902          * @return import end line from ast.
903          */
904         public int getEndLineNumber() {
905             return importAST.getLastChild().getLineNo();
906         }
907 
908         /**
909          * Get import group.
910          *
911          * @return import group.
912          */
913         public String getImportGroup() {
914             return importGroup;
915         }
916 
917         /**
918          * Checks if import is static.
919          *
920          * @return true, if import is static.
921          */
922         public boolean isStaticImport() {
923             return staticImport;
924         }
925 
926         /**
927          * Get import ast.
928          *
929          * @return import ast.
930          */
931         public DetailAST getImportAST() {
932             return importAST;
933         }
934 
935     }
936 
937     /**
938      * Contains matching attributes assisting in definition of "best matching"
939      * group for import.
940      */
941     private static final class RuleMatchForImport {
942 
943         /** Position of matching string for current best match. */
944         private final int matchPosition;
945         /** Length of matching string for current best match. */
946         private int matchLength;
947         /** Import group for current best match. */
948         private String group;
949 
950         /**
951          * Constructor to initialize the fields.
952          *
953          * @param group
954          *        Matched group.
955          * @param length
956          *        Matching length.
957          * @param position
958          *        Matching position.
959          */
960         private RuleMatchForImport(String group, int length, int position) {
961             this.group = group;
962             matchLength = length;
963             matchPosition = position;
964         }
965 
966     }
967 
968 }