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