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