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