1 ///////////////////////////////////////////////////////////////////////////////////////////////
2 // checkstyle: Checks Java source code and other text files for adherence to a set of rules.
3 // Copyright (C) 2001-2026 the original author or authors.
4 //
5 // This library is free software; you can redistribute it and/or
6 // modify it under the terms of the GNU Lesser General Public
7 // License as published by the Free Software Foundation; either
8 // version 2.1 of the License, or (at your option) any later version.
9 //
10 // This library is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 // Lesser General Public License for more details.
14 //
15 // You should have received a copy of the GNU Lesser General Public
16 // License along with this library; if not, write to the Free Software
17 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
18 ///////////////////////////////////////////////////////////////////////////////////////////////
19
20 package com.puppycrawl.tools.checkstyle.checks.imports;
21
22 import java.util.Locale;
23 import java.util.regex.Matcher;
24 import java.util.regex.Pattern;
25
26 import com.puppycrawl.tools.checkstyle.FileStatefulCheck;
27 import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
28 import com.puppycrawl.tools.checkstyle.api.DetailAST;
29 import com.puppycrawl.tools.checkstyle.api.FullIdent;
30 import com.puppycrawl.tools.checkstyle.api.TokenTypes;
31 import com.puppycrawl.tools.checkstyle.utils.CommonUtil;
32 import com.puppycrawl.tools.checkstyle.utils.UnmodifiableCollectionUtil;
33
34 /**
35 * <div>
36 * Checks the ordering/grouping of imports. Features are:
37 * </div>
38 * <ul>
39 * <li>
40 * groups type/static imports: ensures that groups of imports come in a specific order
41 * (e.g., java. comes first, javax. comes second, then everything else)
42 * </li>
43 * <li>
44 * adds a separation between type import groups : ensures that a blank line sit between each group
45 * </li>
46 * <li>
47 * type/static import groups aren't separated internally: ensures that each group aren't separated
48 * internally by blank line or comment
49 * </li>
50 * <li>
51 * sorts type/static imports inside each group: ensures that imports within each group are in
52 * lexicographic order
53 * </li>
54 * <li>
55 * sorts according to case: ensures that the comparison between imports is case-sensitive, in
56 * <a href="https://en.wikipedia.org/wiki/ASCII#Order">ASCII sort order</a>
57 * </li>
58 * <li>
59 * arrange static imports: ensures the relative order between type imports and static imports
60 * (see
61 * <a href="https://checkstyle.org/property-types.html#ImportOrderOption">ImportOrderOption</a>)
62 * </li>
63 * </ul>
64 *
65 * @since 3.2
66 */
67 @FileStatefulCheck
68 public class ImportOrderCheck
69 extends AbstractCheck {
70
71 /**
72 * A key is pointing to the warning message text in "messages.properties"
73 * file.
74 */
75 public static final String MSG_SEPARATION = "import.separation";
76
77 /**
78 * A key is pointing to the warning message text in "messages.properties"
79 * file.
80 */
81 public static final String MSG_SEPARATED_IN_GROUP = "import.groups.separated.internally";
82
83 /**
84 * A key is pointing to the warning message text in "messages.properties"
85 * file.
86 */
87 public static final String MSG_ORDERING_LEX = "import.ordering.lex";
88
89 /**
90 * A key is pointing to the warning message text in "messages.properties"
91 * file.
92 */
93 public static final String MSG_ORDERING_STATIC = "import.ordering.static";
94
95 /**
96 * A key is pointing to the warning message text in "messages.properties"
97 * file.
98 */
99 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 * {@snippet lang="text" :
594 * import static HttpConstants.COLON => HttpConstants
595 * import static HttpHeaders.addHeader => HttpHeaders
596 * import static HttpHeaders.setHeader => HttpHeaders
597 * import static HttpHeaders.Names.DATE => HttpHeaders.Names
598 * }
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 * {@snippet lang="text" :
641 * import static HttpConstants.COLON => HttpConstants
642 * import static HttpHeaders.addHeader => HttpHeaders
643 * import static HttpHeaders.setHeader => HttpHeaders
644 * import static HttpHeaders.Names.DATE => HttpHeaders.Names
645 * }
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 }