001///////////////////////////////////////////////////////////////////////////////////////////////
002// checkstyle: Checks Java source code and other text files for adherence to a set of rules.
003// Copyright (C) 2001-2026 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.Locale;
023import java.util.regex.Matcher;
024import java.util.regex.Pattern;
025
026import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
027import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
028import com.puppycrawl.tools.checkstyle.api.DetailAST;
029import com.puppycrawl.tools.checkstyle.api.FullIdent;
030import com.puppycrawl.tools.checkstyle.api.TokenTypes;
031import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
032import com.puppycrawl.tools.checkstyle.utils.UnmodifiableCollectionUtil;
033
034/**
035 * <div>
036 * Checks the ordering/grouping of imports. Features are:
037 * </div>
038 * <ul>
039 * <li>
040 * groups type/static imports: ensures that groups of imports come in a specific order
041 * (e.g., java. comes first, javax. comes second, then everything else)
042 * </li>
043 * <li>
044 * adds a separation between type import groups : ensures that a blank line sit between each group
045 * </li>
046 * <li>
047 * type/static import groups aren't separated internally: ensures that each group aren't separated
048 * internally by blank line or comment
049 * </li>
050 * <li>
051 * sorts type/static imports inside each group: ensures that imports within each group are in
052 * lexicographic order
053 * </li>
054 * <li>
055 * sorts according to case: ensures that the comparison between imports is case-sensitive, in
056 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>
057 * </li>
058 * <li>
059 * arrange static imports: ensures the relative order between type imports and static imports
060 * (see
061 * <a href="https://checkstyle.org/property_types.html#ImportOrderOption">ImportOrderOption</a>)
062 * </li>
063 * </ul>
064 *
065 * @since 3.2
066 */
067@FileStatefulCheck
068public class ImportOrderCheck
069    extends AbstractCheck {
070
071    /**
072     * A key is pointing to the warning message text in "messages.properties"
073     * file.
074     */
075    public static final String MSG_SEPARATION = "import.separation";
076
077    /**
078     * A key is pointing to the warning message text in "messages.properties"
079     * file.
080     */
081    public static final String MSG_SEPARATED_IN_GROUP = "import.groups.separated.internally";
082
083    /**
084     * A key is pointing to the warning message text in "messages.properties"
085     * file.
086     */
087    public static final String MSG_ORDERING_LEX = "import.ordering.lex";
088
089    /**
090     * A key is pointing to the warning message text in "messages.properties"
091     * file.
092     */
093    public static final String MSG_ORDERING_STATIC = "import.ordering.static";
094
095    /**
096     * A key is pointing to the warning message text in "messages.properties"
097     * file.
098     */
099    public static final String MSG_ORDERING_GROUP = "import.ordering.group";
100
101    /** The special wildcard that catches all remaining groups. */
102    private static final String WILDCARD_GROUP_NAME = "*";
103
104    /** The Forward slash. */
105    private static final String FORWARD_SLASH = "/";
106
107    /** Empty array of pattern type needed to initialize check. */
108    private static final Pattern[] EMPTY_PATTERN_ARRAY = new Pattern[0];
109
110    /**
111     * Specify list of <b>type import</b> groups. Every group identified either by a common prefix
112     * string, or by a regular expression enclosed in forward slashes (e.g. {@code /regexp/}).
113     * If an import matches two or more groups,
114     * the best match is selected (closest to the start, and the longest match).
115     * All type imports, which does not match any group, falls into an additional group,
116     * located at the end. Thus, the empty list of type groups (the default value) means one group
117     * for all type imports.
118     */
119    private String[] groups = CommonUtil.EMPTY_STRING_ARRAY;
120
121    /**
122     * Specify list of <b>static</b> import groups. Every group identified either by a common prefix
123     * string, or by a regular expression enclosed in forward slashes (e.g. {@code /regexp/}).
124     * If an import matches two or more groups,
125     * the best match is selected (closest to the start, and the longest match).
126     * All static imports, which does not match any group, fall into an additional group, located
127     * at the end. Thus, the empty list of static groups (the default value) means one group for all
128     * static imports. This property has effect only when the property {@code option} is set to
129     * {@code top} or {@code bottom}.
130     */
131    private String[] staticGroups = CommonUtil.EMPTY_STRING_ARRAY;
132
133    /**
134     * Control whether type import groups should be separated by, at least, one blank
135     * line or comment and aren't separated internally. It doesn't affect separations for static
136     * imports.
137     */
138    private boolean separated;
139
140    /**
141     * Control whether static import groups should be separated by, at least, one blank
142     * line or comment and aren't separated internally. When the property {@code option}
143     * is set to {@code top} or {@code bottom}, this property requires {@code staticGroups}
144     * to be enabled. When the property {@code option} is set to {@code under} or
145     * {@code above}, this property allows blank lines between static and non-static
146     * imports within the same group.
147     */
148    private boolean separatedStaticGroups;
149
150    /**
151     * Control whether type imports within each group should be sorted.
152     * It doesn't affect sorting for static imports.
153     */
154    private boolean ordered = true;
155
156    /**
157     * Control whether string comparison should be case-sensitive or not. Case-sensitive
158     * sorting is in <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>.
159     * It affects both type imports and static imports.
160     */
161    private boolean caseSensitive = true;
162
163    /** Last imported group. */
164    private int lastGroup;
165    /** Line number of last import. */
166    private int lastImportLine;
167    /** Name of last import. */
168    private String lastImport;
169    /** If last import was static. */
170    private boolean lastImportStatic;
171    /** Whether there were any imports. */
172    private boolean beforeFirstImport;
173    /**
174     * Whether static and type import groups should be split apart.
175     * When the {@code option} property is set to {@code INFLOW}, {@code ABOVE} or {@code UNDER},
176     * both the type and static imports use the properties {@code groups} and {@code separated}.
177     * When the {@code option} property is set to {@code TOP} or {@code BOTTOM}, static imports
178     * uses the properties {@code staticGroups} and {@code separatedStaticGroups}.
179     **/
180    private boolean staticImportsApart;
181
182    /**
183     * Control whether <b>static imports</b> located at <b>top</b> or <b>bottom</b> are
184     * sorted within the group.
185     */
186    private boolean sortStaticImportsAlphabetically;
187
188    /**
189     * Control whether to use container ordering (Eclipse IDE term) for static imports
190     * or not.
191     */
192    private boolean useContainerOrderingForStatic;
193
194    /**
195     * Specify policy on the relative order between type imports and static imports.
196     */
197    private ImportOrderOption option = ImportOrderOption.UNDER;
198
199    /**
200     * Complied array of patterns for property {@code groups}.
201     */
202    private Pattern[] groupsReg = EMPTY_PATTERN_ARRAY;
203
204    /**
205     * Complied array of patterns for property {@code staticGroups}.
206     */
207    private Pattern[] staticGroupsReg = EMPTY_PATTERN_ARRAY;
208
209    /**
210     * Creates a new {@code ImportOrderCheck} instance.
211     */
212    public ImportOrderCheck() {
213        // no code by default
214    }
215
216    /**
217     * Setter to specify policy on the relative order between type imports and static imports.
218     *
219     * @param optionStr string to decode option from
220     * @throws IllegalArgumentException if unable to decode
221     * @since 5.0
222     */
223    public void setOption(String optionStr) {
224        option = ImportOrderOption.valueOf(optionStr.trim().toUpperCase(Locale.ENGLISH));
225    }
226
227    /**
228     * Setter to specify list of <b>type import</b> groups. Every group identified either by a
229     * common prefix string, or by a regular expression enclosed in forward slashes
230     * (e.g. {@code /regexp/}). If an import matches two or more groups,
231     * the best match is selected (closest to the start, and the longest match).
232     * All type imports, which does not match any group, falls into an
233     * additional group, located at the end. Thus, the empty list of type groups (the default value)
234     * means one group for all type imports.
235     *
236     * @param packageGroups a comma-separated list of package names/prefixes.
237     * @since 3.2
238     */
239    public void setGroups(String... packageGroups) {
240        groups = UnmodifiableCollectionUtil.copyOfArray(packageGroups, packageGroups.length);
241        groupsReg = compilePatterns(packageGroups);
242    }
243
244    /**
245     * Setter to specify list of <b>static</b> import groups. Every group identified either by a
246     * common prefix string, or by a regular expression enclosed in forward slashes
247     * (e.g. {@code /regexp/}). If an import matches two or more groups,
248     * the best match is selected (closest to the start, and the longest match).
249     * All static imports, which does not match any group, fall into an
250     * additional group, located at the end. Thus, the empty list of static groups (the default
251     * value) means one group for all static imports. This property has effect only when
252     * the property {@code option} is set to {@code top} or {@code bottom}.
253     *
254     * @param packageGroups a comma-separated list of package names/prefixes.
255     * @since 8.12
256     */
257    public void setStaticGroups(String... packageGroups) {
258        staticGroups = UnmodifiableCollectionUtil.copyOfArray(packageGroups, packageGroups.length);
259        staticGroupsReg = compilePatterns(packageGroups);
260    }
261
262    /**
263     * Setter to control whether type imports within each group should be sorted.
264     * It doesn't affect sorting for static imports.
265     *
266     * @param ordered
267     *            whether lexicographic ordering of imports within a group
268     *            required or not.
269     * @since 3.2
270     */
271    public void setOrdered(boolean ordered) {
272        this.ordered = ordered;
273    }
274
275    /**
276     * Setter to control whether type import groups should be separated by, at least,
277     * one blank line or comment and aren't separated internally.
278     * It doesn't affect separations for static imports.
279     *
280     * @param separated
281     *            whether groups should be separated by one blank line or comment.
282     * @since 3.2
283     */
284    public void setSeparated(boolean separated) {
285        this.separated = separated;
286    }
287
288    /**
289     * Setter to control whether static import groups should be separated by, at least,
290     * one blank line or comment and aren't separated internally.
291     * When the property {@code option} is set to {@code top} or {@code bottom},
292     * this property requires {@code staticGroups} to be enabled.
293     * When the property {@code option} is set to {@code under} or {@code above},
294     * this property allows blank lines between static and non-static imports
295     * within the same group.
296     *
297     * @param separatedStaticGroups
298     *            whether groups should be separated by one blank line or comment.
299     * @since 8.12
300     */
301    public void setSeparatedStaticGroups(boolean separatedStaticGroups) {
302        this.separatedStaticGroups = separatedStaticGroups;
303    }
304
305    /**
306     * Setter to control whether string comparison should be case-sensitive or not.
307     * Case-sensitive sorting is in
308     * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>.
309     * It affects both type imports and static imports.
310     *
311     * @param caseSensitive
312     *            whether string comparison should be case-sensitive.
313     * @since 3.3
314     */
315    public void setCaseSensitive(boolean caseSensitive) {
316        this.caseSensitive = caseSensitive;
317    }
318
319    /**
320     * Setter to control whether <b>static imports</b> located at <b>top</b> or
321     * <b>bottom</b> are sorted within the group.
322     *
323     * @param sortAlphabetically true or false.
324     * @since 6.5
325     */
326    public void setSortStaticImportsAlphabetically(boolean sortAlphabetically) {
327        sortStaticImportsAlphabetically = sortAlphabetically;
328    }
329
330    /**
331     * Setter to control whether to use container ordering (Eclipse IDE term) for static
332     * imports or not.
333     *
334     * @param useContainerOrdering whether to use container ordering for static imports or not.
335     * @since 7.1
336     */
337    public void setUseContainerOrderingForStatic(boolean useContainerOrdering) {
338        useContainerOrderingForStatic = useContainerOrdering;
339    }
340
341    @Override
342    public int[] getDefaultTokens() {
343        return getRequiredTokens();
344    }
345
346    @Override
347    public int[] getAcceptableTokens() {
348        return getRequiredTokens();
349    }
350
351    @Override
352    public int[] getRequiredTokens() {
353        return new int[] {TokenTypes.IMPORT, TokenTypes.STATIC_IMPORT};
354    }
355
356    @Override
357    public void beginTree(DetailAST rootAST) {
358        lastGroup = Integer.MIN_VALUE;
359        lastImportLine = Integer.MIN_VALUE;
360        lastImportStatic = false;
361        beforeFirstImport = true;
362        staticImportsApart =
363            option == ImportOrderOption.TOP || option == ImportOrderOption.BOTTOM;
364    }
365
366    // -@cs[CyclomaticComplexity] SWITCH was transformed into IF-ELSE.
367    @Override
368    public void visitToken(DetailAST ast) {
369        final FullIdent ident;
370        final boolean isStatic;
371
372        if (ast.getType() == TokenTypes.IMPORT) {
373            ident = FullIdent.createFullIdentBelow(ast);
374            isStatic = false;
375        }
376        else {
377            ident = FullIdent.createFullIdent(ast.getFirstChild()
378                    .getNextSibling());
379            isStatic = true;
380        }
381
382        // using set of IF instead of SWITCH to analyze Enum options to satisfy coverage.
383        // https://github.com/checkstyle/checkstyle/issues/1387
384        if (option == ImportOrderOption.TOP || option == ImportOrderOption.ABOVE) {
385            final boolean isStaticAndNotLastImport = isStatic && !lastImportStatic;
386            doVisitToken(ident, isStatic, isStaticAndNotLastImport, ast);
387        }
388        else if (option == ImportOrderOption.BOTTOM || option == ImportOrderOption.UNDER) {
389            final boolean isLastImportAndNonStatic = lastImportStatic && !isStatic;
390            doVisitToken(ident, isStatic, isLastImportAndNonStatic, ast);
391        }
392        else if (option == ImportOrderOption.INFLOW) {
393            // "previous" argument is useless here
394            doVisitToken(ident, isStatic, true, ast);
395        }
396        else {
397            throw new IllegalStateException(
398                    String.format(Locale.ROOT, "Unexpected option for static imports: %s", option));
399        }
400
401        lastImportLine = ast.findFirstToken(TokenTypes.SEMI).getLineNo();
402        lastImportStatic = isStatic;
403        beforeFirstImport = false;
404    }
405
406    /**
407     * Shares processing...
408     *
409     * @param ident the import to process.
410     * @param isStatic whether the token is static or not.
411     * @param previous previous non-static but current is static (above), or
412     *                  previous static but current is non-static (under).
413     * @param ast node of the AST.
414     */
415    private void doVisitToken(FullIdent ident, boolean isStatic, boolean previous, DetailAST ast) {
416        final String name = ident.getText();
417        final int groupIdx = getGroupNumber(isStatic && staticImportsApart, name);
418
419        if (groupIdx > lastGroup) {
420            handleGreaterGroup(isStatic, ast, name);
421        }
422        else if (groupIdx == lastGroup) {
423            doVisitTokenInSameGroup(isStatic, previous, name, ast);
424
425            if (separatedStaticGroups
426                    && isStatic != lastImportStatic
427                    && !isSeparatorBeforeImport(ast.getLineNo())) {
428                log(ast, MSG_SEPARATION, name);
429            }
430        }
431        else {
432            handleLowerGroup(previous, ast, name);
433        }
434        if (isSeparatorInGroup(groupIdx, isStatic, ast.getLineNo())) {
435            log(ast, MSG_SEPARATED_IN_GROUP, name);
436        }
437
438        lastGroup = groupIdx;
439        lastImport = name;
440    }
441
442    /**
443     * Handles the case when the current import belongs to a group
444     * that comes after the previous group. Verifies whether a
445     * separator between groups is required.
446     *
447     * @param isStatic whether the current import is static
448     * @param ast the AST node of the current import
449     * @param name the fully qualified name of the current import
450     */
451    private void handleGreaterGroup(boolean isStatic, DetailAST ast, String name) {
452        if (!beforeFirstImport
453            && ast.getLineNo() - lastImportLine < 2
454            && needSeparator(isStatic)) {
455            log(ast, MSG_SEPARATION, name);
456        }
457    }
458
459    /**
460     * Handles the case when the current import belongs to a group
461     * that should appear before the previous group. Logs either
462     * a static-order violation or a group-order violation.
463     *
464     * @param previous indicates a static/non-static ordering transition
465     * @param ast the AST node of the current import
466     * @param name the fully qualified name of the current import
467     */
468    private void handleLowerGroup(boolean previous, DetailAST ast, String name) {
469        if (previous
470                && (option == ImportOrderOption.TOP || option == ImportOrderOption.BOTTOM)) {
471            log(ast, MSG_ORDERING_STATIC, name);
472        }
473        else {
474            log(ast, MSG_ORDERING_GROUP, name);
475        }
476    }
477
478    /**
479     * Checks whether import groups should be separated.
480     *
481     * @param isStatic whether the token is static or not.
482     * @return true if imports groups should be separated.
483     */
484    private boolean needSeparator(boolean isStatic) {
485        final boolean typeImportSeparator = !isStatic && separated;
486        final boolean staticImportSeparator;
487        if (staticImportsApart) {
488            staticImportSeparator = isStatic && separatedStaticGroups;
489        }
490        else {
491            staticImportSeparator = separated;
492        }
493        final boolean separatorBetween = isStatic != lastImportStatic
494            && (separated || separatedStaticGroups);
495
496        return typeImportSeparator || staticImportSeparator || separatorBetween;
497    }
498
499    /**
500     * Checks whether imports group separated internally.
501     *
502     * @param groupIdx group number.
503     * @param isStatic whether the token is static or not.
504     * @param line the line of the current import.
505     * @return true if imports group are separated internally.
506     */
507    private boolean isSeparatorInGroup(int groupIdx, boolean isStatic, int line) {
508        final boolean inSameGroup = groupIdx == lastGroup;
509        final boolean result;
510        if (inSameGroup) {
511            result = (isStatic == lastImportStatic || !separatedStaticGroups)
512                    && isSeparatorBeforeImport(line);
513        }
514        else {
515            result = !needSeparator(isStatic) && isSeparatorBeforeImport(line);
516        }
517        return result;
518    }
519
520    /**
521     * Checks whether there is any separator before current import.
522     *
523     * @param line the line of the current import.
524     * @return true if there is separator before current import which isn't the first import.
525     */
526    private boolean isSeparatorBeforeImport(int line) {
527        return line - lastImportLine > 1;
528    }
529
530    /**
531     * Shares processing...
532     *
533     * @param isStatic whether the token is static or not.
534     * @param previous previous non-static but current is static (above), or
535     *     previous static but current is non-static (under).
536     * @param name the name of the current import.
537     * @param ast node of the AST.
538     */
539    private void doVisitTokenInSameGroup(boolean isStatic,
540            boolean previous, String name, DetailAST ast) {
541        if (ordered) {
542            if (option == ImportOrderOption.INFLOW) {
543                if (isWrongOrder(name, isStatic)) {
544                    log(ast, MSG_ORDERING_LEX, name, lastImport);
545                }
546            }
547            else {
548                if (previous) {
549                    // static vs non-static placement violation
550                    log(ast, MSG_ORDERING_STATIC, name);
551                }
552                else if (lastImportStatic == isStatic && isWrongOrder(name, isStatic)) {
553                    // lexicographical violation
554                    log(ast, MSG_ORDERING_LEX, name, lastImport);
555                }
556            }
557        }
558    }
559
560    /**
561     * Checks whether import name is in wrong order.
562     *
563     * @param name import name.
564     * @param isStatic whether it is a static import name.
565     * @return true if import name is in wrong order.
566     */
567    private boolean isWrongOrder(String name, boolean isStatic) {
568        final boolean result;
569        if (isStatic) {
570            if (useContainerOrderingForStatic) {
571                result = compareContainerOrder(lastImport, name, caseSensitive) > 0;
572            }
573            else if (staticImportsApart) {
574                result = sortStaticImportsAlphabetically
575                    && compare(lastImport, name, caseSensitive) > 0;
576            }
577            else {
578                result = compare(lastImport, name, caseSensitive) > 0;
579            }
580        }
581        else {
582            // out of lexicographic order
583            result = compare(lastImport, name, caseSensitive) > 0;
584        }
585        return result;
586    }
587
588    /**
589     * Compares two import strings.
590     * We first compare the container of the static import, container being the type enclosing
591     * the static element being imported. When this returns 0, we compare the qualified
592     * import name. For e.g. this is what is considered to be container names:
593     * <pre>
594     * import static HttpConstants.COLON     =&gt; HttpConstants
595     * import static HttpHeaders.addHeader   =&gt; HttpHeaders
596     * import static HttpHeaders.setHeader   =&gt; HttpHeaders
597     * import static HttpHeaders.Names.DATE  =&gt; HttpHeaders.Names
598     * </pre>
599     *
600     * <p>
601     * According to this logic, HttpHeaders.Names would come after HttpHeaders.
602     * For more details, see <a href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=473629#c3">
603     * static imports comparison method</a> in Eclipse.
604     * </p>
605     *
606     * @param importName1 first import name
607     * @param importName2 second import name
608     * @param caseSensitive whether the comparison of fully qualified import names is
609     *                      case-sensitive
610     * @return the value {@code 0} if str1 is equal to str2; a value
611     *         less than {@code 0} if str is less than the str2 (container order
612     *         or lexicographical); and a value greater than {@code 0} if str1 is greater than str2
613     *         (container order or lexicographically)
614     */
615    private static int compareContainerOrder(String importName1, String importName2,
616                                             boolean caseSensitive) {
617        final String container1 = getImportContainer(importName1);
618        final String container2 = getImportContainer(importName2);
619        final int compareContainersOrderResult;
620        if (caseSensitive) {
621            compareContainersOrderResult = container1.compareTo(container2);
622        }
623        else {
624            compareContainersOrderResult = container1.compareToIgnoreCase(container2);
625        }
626        final int result;
627        if (compareContainersOrderResult == 0) {
628            result = compare(importName1, importName2, caseSensitive);
629        }
630        else {
631            result = compareContainersOrderResult;
632        }
633        return result;
634    }
635
636    /**
637     * Extracts import container name from fully qualified import name.
638     * An import container name is the type which encloses the static element being imported.
639     * For example, HttpConstants, HttpHeaders, HttpHeaders.Names are import container names:
640     * <pre>
641     * import static HttpConstants.COLON     =&gt; HttpConstants
642     * import static HttpHeaders.addHeader   =&gt; HttpHeaders
643     * import static HttpHeaders.setHeader   =&gt; HttpHeaders
644     * import static HttpHeaders.Names.DATE  =&gt; HttpHeaders.Names
645     * </pre>
646     *
647     * @param qualifiedImportName fully qualified import name.
648     * @return import container name.
649     */
650    private static String getImportContainer(String qualifiedImportName) {
651        final int lastDotIndex = qualifiedImportName.lastIndexOf('.');
652        return qualifiedImportName.substring(0, lastDotIndex);
653    }
654
655    /**
656     * Finds out what group the specified import belongs to.
657     *
658     * @param isStatic whether the token is static or not.
659     * @param name the import name to find.
660     * @return group number for given import name.
661     */
662    private int getGroupNumber(boolean isStatic, String name) {
663        final Pattern[] patterns;
664        if (isStatic) {
665            patterns = staticGroupsReg;
666        }
667        else {
668            patterns = groupsReg;
669        }
670
671        int number = getGroupNumber(patterns, name);
672
673        if (isStatic && option == ImportOrderOption.BOTTOM) {
674            number += groups.length + 1;
675        }
676        else if (!isStatic && option == ImportOrderOption.TOP) {
677            number += staticGroups.length + 1;
678        }
679        return number;
680    }
681
682    /**
683     * Finds out what group the specified import belongs to.
684     *
685     * @param patterns groups to check.
686     * @param name the import name to find.
687     * @return group number for given import name.
688     */
689    private static int getGroupNumber(Pattern[] patterns, String name) {
690        int bestIndex = patterns.length;
691        int bestEnd = -1;
692        int bestPos = Integer.MAX_VALUE;
693
694        // find out what group this belongs in
695        // loop over patterns and get index
696        for (int i = 0; i < patterns.length; i++) {
697            final Matcher matcher = patterns[i].matcher(name);
698            if (matcher.find()) {
699                if (matcher.start() < bestPos) {
700                    bestIndex = i;
701                    bestEnd = matcher.end();
702                    bestPos = matcher.start();
703                }
704                else if (matcher.start() == bestPos && matcher.end() > bestEnd) {
705                    bestIndex = i;
706                    bestEnd = matcher.end();
707                }
708            }
709        }
710        return bestIndex;
711    }
712
713    /**
714     * Compares two strings.
715     *
716     * @param string1
717     *            the first string
718     * @param string2
719     *            the second string
720     * @param caseSensitive
721     *            whether the comparison is case-sensitive
722     * @return the value {@code 0} if string1 is equal to string2; a value
723     *         less than {@code 0} if string1 is lexicographically less
724     *         than the string2; and a value greater than {@code 0} if
725     *         string1 is lexicographically greater than string2
726     */
727    private static int compare(String string1, String string2,
728            boolean caseSensitive) {
729        final int result;
730        if (caseSensitive) {
731            result = string1.compareTo(string2);
732        }
733        else {
734            result = string1.compareToIgnoreCase(string2);
735        }
736
737        return result;
738    }
739
740    /**
741     * Compiles the list of package groups and the order they should occur in the file.
742     *
743     * @param packageGroups a comma-separated list of package names/prefixes.
744     * @return array of compiled patterns.
745     * @throws IllegalArgumentException if any of the package groups are not valid.
746     */
747    private static Pattern[] compilePatterns(String... packageGroups) {
748        final Pattern[] patterns = new Pattern[packageGroups.length];
749        for (int i = 0; i < packageGroups.length; i++) {
750            String pkg = packageGroups[i];
751            final Pattern grp;
752
753            // if the pkg name is the wildcard, make it match zero chars
754            // from any name, so it will always be used as last resort.
755            if (WILDCARD_GROUP_NAME.equals(pkg)) {
756                // matches any package
757                grp = Pattern.compile("");
758            }
759            else if (pkg.startsWith(FORWARD_SLASH)) {
760                if (!pkg.endsWith(FORWARD_SLASH)) {
761                    throw new IllegalArgumentException("Invalid group: " + pkg);
762                }
763                pkg = pkg.substring(1, pkg.length() - 1);
764                grp = Pattern.compile(pkg);
765            }
766            else {
767                final StringBuilder pkgBuilder = new StringBuilder(pkg);
768                if (!pkg.endsWith(".")) {
769                    pkgBuilder.append('.');
770                }
771                grp = Pattern.compile("^" + Pattern.quote(pkgBuilder.toString()));
772            }
773
774            patterns[i] = grp;
775        }
776        return patterns;
777    }
778
779}