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