QUALIFIERS =
309 | Arrays.asList("alpha", "beta", "milestone", "rc", "snapshot", "", "sp");
310 |
311 | private static final Properties ALIASES = new Properties();
312 |
313 | static {
314 | ALIASES.put("ga", "");
315 | ALIASES.put("final", "");
316 | ALIASES.put("release", "");
317 | ALIASES.put("cr", "rc");
318 | }
319 |
320 | /**
321 | * A comparable value for the empty-string qualifier. This one is used to determine if a given
322 | * qualifier makes the version older than one without a qualifier, or more recent.
323 | */
324 | private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS.indexOf(""));
325 |
326 | private final String value;
327 |
328 | StringItem(String value, boolean followedByDigit) {
329 | if (followedByDigit && value.length() == 1) {
330 | // a1 = alpha-1, b1 = beta-1, m1 = milestone-1
331 | switch (value.charAt(0)) {
332 | case 'a':
333 | value = "alpha";
334 | break;
335 | case 'b':
336 | value = "beta";
337 | break;
338 | case 'm':
339 | value = "milestone";
340 | break;
341 | default:
342 | }
343 | }
344 | this.value = ALIASES.getProperty(value, value);
345 | }
346 |
347 | @Override
348 | public int getType() {
349 | return STRING_ITEM;
350 | }
351 |
352 | @Override
353 | public boolean isNull() {
354 | return (comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX) == 0);
355 | }
356 |
357 | /**
358 | * Returns a comparable value for a qualifier.
359 | *
360 | * This method takes into account the ordering of known qualifiers then unknown qualifiers
361 | * with lexical ordering.
362 | *
363 | *
just returning an Integer with the index here is faster, but requires a lot of
364 | * if/then/else to check for -1 or QUALIFIERS.size and then resort to lexical ordering. Most
365 | * comparisons are decided by the first character, so this is still fast. If more characters are
366 | * needed then it requires a lexical sort anyway.
367 | *
368 | * @param qualifier
369 | * @return an equivalent value that can be used with lexical comparison
370 | */
371 | public static String comparableQualifier(String qualifier) {
372 | int i = QUALIFIERS.indexOf(qualifier);
373 |
374 | return i == -1 ? (QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i);
375 | }
376 |
377 | @Override
378 | public int compareTo(Item item) {
379 | if (item == null) {
380 | // 1-rc < 1, 1-ga > 1
381 | return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX);
382 | }
383 | switch (item.getType()) {
384 | case INT_ITEM:
385 | case LONG_ITEM:
386 | case BIGINTEGER_ITEM:
387 | return -1; // 1.any < 1.1 ?
388 |
389 | case STRING_ITEM:
390 | return comparableQualifier(value)
391 | .compareTo(comparableQualifier(((StringItem) item).value));
392 |
393 | case LIST_ITEM:
394 | return -1; // 1.any < 1-1
395 |
396 | default:
397 | throw new IllegalStateException("invalid item: " + item.getClass());
398 | }
399 | }
400 |
401 | @Override
402 | public boolean equals(Object o) {
403 | if (this == o) {
404 | return true;
405 | }
406 | if (o == null || getClass() != o.getClass()) {
407 | return false;
408 | }
409 |
410 | StringItem that = (StringItem) o;
411 |
412 | return value.equals(that.value);
413 | }
414 |
415 | @Override
416 | public int hashCode() {
417 | return value.hashCode();
418 | }
419 |
420 | public String toString() {
421 | return value;
422 | }
423 | }
424 |
425 | /**
426 | * Represents a version list item. This class is used both for the global item list and for
427 | * sub-lists (which start with '-(number)' in the version specification).
428 | */
429 | private static class ListItem extends ArrayList- implements Item {
430 | @Override
431 | public int getType() {
432 | return LIST_ITEM;
433 | }
434 |
435 | @Override
436 | public boolean isNull() {
437 | return (size() == 0);
438 | }
439 |
440 | void normalize() {
441 | for (int i = size() - 1; i >= 0; i--) {
442 | Item lastItem = get(i);
443 |
444 | if (lastItem.isNull()) {
445 | // remove null trailing items: 0, "", empty list
446 | remove(i);
447 | } else if (!(lastItem instanceof ListItem)) {
448 | break;
449 | }
450 | }
451 | }
452 |
453 | @Override
454 | public int compareTo(Item item) {
455 | if (item == null) {
456 | if (size() == 0) {
457 | return 0; // 1-0 = 1- (normalize) = 1
458 | }
459 | Item first = get(0);
460 | return first.compareTo(null);
461 | }
462 | switch (item.getType()) {
463 | case INT_ITEM:
464 | case LONG_ITEM:
465 | case BIGINTEGER_ITEM:
466 | return -1; // 1-1 < 1.0.x
467 |
468 | case STRING_ITEM:
469 | return 1; // 1-1 > 1-sp
470 |
471 | case LIST_ITEM:
472 | Iterator
- left = iterator();
473 | Iterator
- right = ((ListItem) item).iterator();
474 |
475 | while (left.hasNext() || right.hasNext()) {
476 | Item l = left.hasNext() ? left.next() : null;
477 | Item r = right.hasNext() ? right.next() : null;
478 |
479 | // if this is shorter, then invert the compare and mul with -1
480 | int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r);
481 |
482 | if (result != 0) {
483 | return result;
484 | }
485 | }
486 |
487 | return 0;
488 |
489 | default:
490 | throw new IllegalStateException("invalid item: " + item.getClass());
491 | }
492 | }
493 |
494 | @Override
495 | public String toString() {
496 | StringBuilder buffer = new StringBuilder();
497 | for (Item item : this) {
498 | if (buffer.length() > 0) {
499 | buffer.append((item instanceof ListItem) ? '-' : '.');
500 | }
501 | buffer.append(item);
502 | }
503 | return buffer.toString();
504 | }
505 | }
506 |
507 | public ComparableVersion(String version) {
508 | parseVersion(version);
509 | }
510 |
511 | @SuppressWarnings("checkstyle:innerassignment")
512 | public final void parseVersion(String version) {
513 | this.value = version;
514 |
515 | items = new ListItem();
516 |
517 | version = version.toLowerCase(Locale.ENGLISH);
518 |
519 | ListItem list = items;
520 |
521 | Deque
- stack = new ArrayDeque<>();
522 | stack.push(list);
523 |
524 | boolean isDigit = false;
525 |
526 | int startIndex = 0;
527 |
528 | for (int i = 0; i < version.length(); i++) {
529 | char c = version.charAt(i);
530 |
531 | if (c == '.') {
532 | if (i == startIndex) {
533 | list.add(IntItem.ZERO);
534 | } else {
535 | list.add(parseItem(isDigit, version.substring(startIndex, i)));
536 | }
537 | startIndex = i + 1;
538 | } else if (c == '-') {
539 | if (i == startIndex) {
540 | list.add(IntItem.ZERO);
541 | } else {
542 | list.add(parseItem(isDigit, version.substring(startIndex, i)));
543 | }
544 | startIndex = i + 1;
545 |
546 | list.add(list = new ListItem());
547 | stack.push(list);
548 | } else if (Character.isDigit(c)) {
549 | if (!isDigit && i > startIndex) {
550 | list.add(new StringItem(version.substring(startIndex, i), true));
551 | startIndex = i;
552 |
553 | list.add(list = new ListItem());
554 | stack.push(list);
555 | }
556 |
557 | isDigit = true;
558 | } else {
559 | if (isDigit && i > startIndex) {
560 | list.add(parseItem(true, version.substring(startIndex, i)));
561 | startIndex = i;
562 |
563 | list.add(list = new ListItem());
564 | stack.push(list);
565 | }
566 |
567 | isDigit = false;
568 | }
569 | }
570 |
571 | if (version.length() > startIndex) {
572 | list.add(parseItem(isDigit, version.substring(startIndex)));
573 | }
574 |
575 | while (!stack.isEmpty()) {
576 | list = (ListItem) stack.pop();
577 | list.normalize();
578 | }
579 | }
580 |
581 | private static Item parseItem(boolean isDigit, String buf) {
582 | if (isDigit) {
583 | buf = stripLeadingZeroes(buf);
584 | if (buf.length() <= MAX_INTITEM_LENGTH) {
585 | // lower than 2^31
586 | return new IntItem(buf);
587 | } else if (buf.length() <= MAX_LONGITEM_LENGTH) {
588 | // lower than 2^63
589 | return new LongItem(buf);
590 | }
591 | return new BigIntegerItem(buf);
592 | }
593 | return new StringItem(buf, false);
594 | }
595 |
596 | private static String stripLeadingZeroes(String buf) {
597 | if (buf == null || buf.isEmpty()) {
598 | return "0";
599 | }
600 | for (int i = 0; i < buf.length(); ++i) {
601 | char c = buf.charAt(i);
602 | if (c != '0') {
603 | return buf.substring(i);
604 | }
605 | }
606 | return buf;
607 | }
608 |
609 | @Override
610 | public int compareTo(ComparableVersion o) {
611 | return items.compareTo(o.items);
612 | }
613 |
614 | @Override
615 | public String toString() {
616 | return value;
617 | }
618 |
619 | public String getCanonical() {
620 | if (canonical == null) {
621 | canonical = items.toString();
622 | }
623 | return canonical;
624 | }
625 |
626 | @Override
627 | public boolean equals(Object o) {
628 | return (o instanceof ComparableVersion) && items.equals(((ComparableVersion) o).items);
629 | }
630 |
631 | @Override
632 | public int hashCode() {
633 | return items.hashCode();
634 | }
635 |
636 | // CHECKSTYLE_OFF: LineLength
637 | /**
638 | * Main to test version parsing and comparison.
639 | *
640 | *
To check how "1.2.7" compares to "1.2-SNAPSHOT", for example, you can issue
641 | *
642 | *
643 | * java -jar ${maven.repo.local}/org/apache/maven/maven-artifact/${maven.version}/maven-artifact-${maven.version}.jar "1.2.7" "1.2-SNAPSHOT"
644 | *
645 | *
646 | * command to command line. Result of given command will be something like this:
647 | *
648 | *
649 | * Display parameters as parsed by Maven (in canonical form) and comparison result:
650 | * 1. 1.2.7 == 1.2.7
651 | * 1.2.7 > 1.2-SNAPSHOT
652 | * 2. 1.2-SNAPSHOT == 1.2-snapshot
653 | *
654 | *
655 | * @param args the version strings to parse and compare. You can pass arbitrary number of version
656 | * strings and always two adjacent will be compared
657 | */
658 | // CHECKSTYLE_ON: LineLength
659 | public static void main(String... args) {
660 | System.out.println(
661 | "Display parameters as parsed by Maven (in canonical form) and comparison result:");
662 | if (args.length == 0) {
663 | return;
664 | }
665 |
666 | ComparableVersion prev = null;
667 | int i = 1;
668 | for (String version : args) {
669 | ComparableVersion c = new ComparableVersion(version);
670 |
671 | if (prev != null) {
672 | int compare = prev.compareTo(c);
673 | System.out.println(
674 | " "
675 | + prev
676 | + ' '
677 | + ((compare == 0) ? "==" : ((compare < 0) ? "<" : ">"))
678 | + ' '
679 | + version);
680 | }
681 |
682 | System.out.println(i++ + ". " + version + " == " + c.getCanonical());
683 |
684 | prev = c;
685 | }
686 | }
687 | }
688 |
--------------------------------------------------------------------------------