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