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