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