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