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