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