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