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