1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.apache.maven.artifact.versioning;
20
21 import java.math.BigInteger;
22 import java.util.ArrayDeque;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.Deque;
26 import java.util.Iterator;
27 import java.util.List;
28 import java.util.Locale;
29 import java.util.Objects;
30 import java.util.Properties;
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66 public class ComparableVersion implements Comparable<ComparableVersion> {
67 private static final int MAX_INTITEM_LENGTH = 9;
68
69 private static final int MAX_LONGITEM_LENGTH = 18;
70
71
72
73
74
75
76
77
78
79 private static final int MAX_VERSION_LENGTH = 256;
80
81 private String value;
82
83 private String canonical;
84
85 private ListItem items;
86
87 private interface Item {
88 int INT_ITEM = 3;
89 int LONG_ITEM = 4;
90 int BIGINTEGER_ITEM = 0;
91 int STRING_ITEM = 1;
92 int LIST_ITEM = 2;
93 int COMBINATION_ITEM = 5;
94
95 int compareTo(Item item);
96
97 int getType();
98
99 boolean isNull();
100 }
101
102
103
104
105 private static class IntItem implements Item {
106 private final int value;
107
108 public static final IntItem ZERO = new IntItem();
109
110 private IntItem() {
111 this.value = 0;
112 }
113
114 IntItem(String str) {
115 this.value = Integer.parseInt(str);
116 }
117
118 @Override
119 public int getType() {
120 return INT_ITEM;
121 }
122
123 @Override
124 public boolean isNull() {
125 return value == 0;
126 }
127
128 @Override
129 public int compareTo(Item item) {
130 if (item == null) {
131 return (value == 0) ? 0 : 1;
132 }
133
134 return switch (item.getType()) {
135 case INT_ITEM -> {
136 int itemValue = ((IntItem) item).value;
137 yield Integer.compare(value, itemValue);
138 }
139 case LONG_ITEM, BIGINTEGER_ITEM -> -1;
140 case STRING_ITEM -> 1;
141 case COMBINATION_ITEM -> 1;
142
143 case LIST_ITEM -> 1;
144
145 default -> throw new IllegalStateException("invalid item: " + item.getClass());
146 };
147 }
148
149 @Override
150 public boolean equals(Object o) {
151 if (this == o) {
152 return true;
153 }
154 if (o == null || getClass() != o.getClass()) {
155 return false;
156 }
157
158 IntItem intItem = (IntItem) o;
159
160 return value == intItem.value;
161 }
162
163 @Override
164 public int hashCode() {
165 return value;
166 }
167
168 @Override
169 public String toString() {
170 return Integer.toString(value);
171 }
172 }
173
174
175
176
177 private static class LongItem implements Item {
178 private final long value;
179
180 LongItem(String str) {
181 this.value = Long.parseLong(str);
182 }
183
184 @Override
185 public int getType() {
186 return LONG_ITEM;
187 }
188
189 @Override
190 public boolean isNull() {
191 return value == 0;
192 }
193
194 @Override
195 public int compareTo(Item item) {
196 if (item == null) {
197 return (value == 0) ? 0 : 1;
198 }
199
200 return switch (item.getType()) {
201 case INT_ITEM -> 1;
202 case LONG_ITEM -> {
203 long itemValue = ((LongItem) item).value;
204 yield Long.compare(value, itemValue);
205 }
206 case BIGINTEGER_ITEM -> -1;
207 case STRING_ITEM -> 1;
208 case COMBINATION_ITEM -> 1;
209
210 case LIST_ITEM -> 1;
211
212 default -> throw new IllegalStateException("invalid item: " + item.getClass());
213 };
214 }
215
216 @Override
217 public boolean equals(Object o) {
218 if (this == o) {
219 return true;
220 }
221 if (o == null || getClass() != o.getClass()) {
222 return false;
223 }
224
225 LongItem longItem = (LongItem) o;
226
227 return value == longItem.value;
228 }
229
230 @Override
231 public int hashCode() {
232 return (int) (value ^ (value >>> 32));
233 }
234
235 @Override
236 public String toString() {
237 return Long.toString(value);
238 }
239 }
240
241
242
243
244 private static class BigIntegerItem implements Item {
245 private final BigInteger value;
246
247 BigIntegerItem(String str) {
248 this.value = new BigInteger(str);
249 }
250
251 @Override
252 public int getType() {
253 return BIGINTEGER_ITEM;
254 }
255
256 @Override
257 public boolean isNull() {
258 return BigInteger.ZERO.equals(value);
259 }
260
261 @Override
262 public int compareTo(Item item) {
263 if (item == null) {
264 return BigInteger.ZERO.equals(value) ? 0 : 1;
265 }
266
267 return switch (item.getType()) {
268 case INT_ITEM, LONG_ITEM -> 1;
269 case BIGINTEGER_ITEM -> value.compareTo(((BigIntegerItem) item).value);
270 case STRING_ITEM -> 1;
271 case COMBINATION_ITEM -> 1;
272
273 case LIST_ITEM -> 1;
274
275 default -> throw new IllegalStateException("invalid item: " + item.getClass());
276 };
277 }
278
279 @Override
280 public boolean equals(Object o) {
281 if (this == o) {
282 return true;
283 }
284 if (o == null || getClass() != o.getClass()) {
285 return false;
286 }
287
288 BigIntegerItem that = (BigIntegerItem) o;
289
290 return value.equals(that.value);
291 }
292
293 @Override
294 public int hashCode() {
295 return value.hashCode();
296 }
297
298 @Override
299 public String toString() {
300 return value.toString();
301 }
302 }
303
304
305
306
307 private static class StringItem implements Item {
308 private static final List<String> QUALIFIERS =
309 Arrays.asList("alpha", "beta", "milestone", "rc", "snapshot", "", "sp");
310 private static final List<String> RELEASE_QUALIFIERS = Arrays.asList("ga", "final", "release");
311
312 private static final Properties ALIASES = new Properties();
313
314 static {
315 ALIASES.put("cr", "rc");
316 }
317
318
319
320
321
322 private static final String RELEASE_VERSION_INDEX = String.valueOf(QUALIFIERS.indexOf(""));
323
324 private final String value;
325
326 StringItem(String value, boolean followedByDigit) {
327 if (followedByDigit && value.length() == 1) {
328
329 switch (value.charAt(0)) {
330 case 'a':
331 value = "alpha";
332 break;
333 case 'b':
334 value = "beta";
335 break;
336 case 'm':
337 value = "milestone";
338 break;
339 default:
340 }
341 }
342 this.value = ALIASES.getProperty(value, value);
343 }
344
345 @Override
346 public int getType() {
347 return STRING_ITEM;
348 }
349
350 @Override
351 public boolean isNull() {
352 return value == null || value.isEmpty();
353 }
354
355
356
357
358
359
360
361
362
363
364
365 public static String comparableQualifier(String qualifier) {
366 if (RELEASE_QUALIFIERS.contains(qualifier)) {
367 return String.valueOf(QUALIFIERS.indexOf(""));
368 }
369
370 int i = QUALIFIERS.indexOf(qualifier);
371
372
373
374
375
376
377 return i == -1 ? (QUALIFIERS.size() + "-" + qualifier) : String.valueOf(i);
378 }
379
380 @Override
381 public int compareTo(Item item) {
382 if (item == null) {
383
384 return comparableQualifier(value).compareTo(RELEASE_VERSION_INDEX);
385 }
386 switch (item.getType()) {
387 case INT_ITEM:
388 case LONG_ITEM:
389 case BIGINTEGER_ITEM:
390 return -1;
391
392 case STRING_ITEM:
393 return comparableQualifier(value).compareTo(comparableQualifier(((StringItem) item).value));
394
395 case COMBINATION_ITEM:
396 int result = this.compareTo(((CombinationItem) item).getStringPart());
397 if (result == 0) {
398 if (compareTo(null) == 0) {
399 return -((CombinationItem) item).getDigitPart().compareTo(null);
400 }
401 return -1;
402 }
403 return result;
404
405 case LIST_ITEM:
406 return -item.compareTo(this);
407
408 default:
409 throw new IllegalStateException("invalid item: " + item.getClass());
410 }
411 }
412
413 @Override
414 public boolean equals(Object o) {
415 if (this == o) {
416 return true;
417 }
418 if (o == null || getClass() != o.getClass()) {
419 return false;
420 }
421
422 StringItem that = (StringItem) o;
423
424 return value.equals(that.value);
425 }
426
427 @Override
428 public int hashCode() {
429 return value.hashCode();
430 }
431
432 @Override
433 public String toString() {
434 return value;
435 }
436 }
437
438
439
440
441
442 private static class CombinationItem implements Item {
443
444 StringItem stringPart;
445
446 Item digitPart;
447
448 CombinationItem(String value) {
449 int index = 0;
450 for (int i = 0; i < value.length(); i++) {
451 char c = value.charAt(i);
452 if (Character.isDigit(c)) {
453 index = i;
454 break;
455 }
456 }
457
458 stringPart = new StringItem(value.substring(0, index), true);
459 digitPart = parseItem(true, value.substring(index));
460 }
461
462 @Override
463 public int compareTo(Item item) {
464 if (item == null) {
465
466 int result = stringPart.compareTo(item);
467 if (result == 0) {
468
469
470
471 return digitPart.compareTo(null);
472 }
473 return result;
474 }
475 int result = 0;
476 switch (item.getType()) {
477 case INT_ITEM:
478 case LONG_ITEM:
479 case BIGINTEGER_ITEM:
480 return -1;
481
482 case STRING_ITEM:
483 result = stringPart.compareTo(item);
484 if (result == 0) {
485 if (stringPart.compareTo(null) == 0) {
486 return digitPart.compareTo(null);
487 }
488
489 return 1;
490 }
491 return result;
492
493 case LIST_ITEM:
494 return -item.compareTo(this);
495
496 case COMBINATION_ITEM:
497 result = stringPart.compareTo(((CombinationItem) item).getStringPart());
498 if (result == 0) {
499 return digitPart.compareTo(((CombinationItem) item).getDigitPart());
500 }
501 return result;
502 default:
503 return 0;
504 }
505 }
506
507 public StringItem getStringPart() {
508 return stringPart;
509 }
510
511 public Item getDigitPart() {
512 return digitPart;
513 }
514
515 @Override
516 public int getType() {
517 return COMBINATION_ITEM;
518 }
519
520 @Override
521 public boolean isNull() {
522 return false;
523 }
524
525 @Override
526 public boolean equals(Object o) {
527 if (this == o) {
528 return true;
529 }
530 if (o == null || getClass() != o.getClass()) {
531 return false;
532 }
533 CombinationItem that = (CombinationItem) o;
534 return Objects.equals(stringPart, that.stringPart) && Objects.equals(digitPart, that.digitPart);
535 }
536
537 @Override
538 public int hashCode() {
539 return Objects.hash(stringPart, digitPart);
540 }
541
542 @Override
543 public String toString() {
544 return stringPart.toString() + digitPart.toString();
545 }
546 }
547
548
549
550
551
552 private static class ListItem extends ArrayList<Item> implements Item {
553 @Override
554 public int getType() {
555 return LIST_ITEM;
556 }
557
558 @Override
559 public boolean isNull() {
560 return (size() == 0);
561 }
562
563 void normalize() {
564 for (int i = size() - 1; i >= 0; i--) {
565 Item lastItem = get(i);
566
567 if (lastItem.isNull()) {
568 if (i == size() - 1 || get(i + 1).getType() == STRING_ITEM) {
569 remove(i);
570 } else if (get(i + 1).getType() == LIST_ITEM) {
571 Item item = ((ListItem) get(i + 1)).get(0);
572 if (item.getType() == COMBINATION_ITEM || item.getType() == STRING_ITEM) {
573 remove(i);
574 }
575 }
576 }
577 }
578
579 if (size() == 1 && get(0) instanceof ListItem list) {
580
581 clear();
582 addAll(list);
583 }
584 }
585
586 @Override
587 public int compareTo(Item item) {
588 if (item == null) {
589 if (size() == 0) {
590 return 0;
591 }
592
593 for (Item i : this) {
594 int result = i.compareTo(null);
595 if (result != 0) {
596 return result;
597 }
598 }
599 return 0;
600 }
601 switch (item.getType()) {
602 case INT_ITEM:
603 case LONG_ITEM:
604 case BIGINTEGER_ITEM:
605 return -1;
606
607 case STRING_ITEM:
608 case COMBINATION_ITEM:
609 int scalarResult = isEmpty() ? -item.compareTo(null) : get(0).compareTo(item);
610 for (int i = 1; scalarResult == 0 && i < size(); i++) {
611 scalarResult = get(i).compareTo(null);
612 }
613 return scalarResult;
614
615 case LIST_ITEM:
616 Iterator<Item> left = iterator();
617 Iterator<Item> right = ((ListItem) item).iterator();
618
619 while (left.hasNext() || right.hasNext()) {
620 Item l = left.hasNext() ? left.next() : null;
621 Item r = right.hasNext() ? right.next() : null;
622
623
624 int result = l == null ? (r == null ? 0 : -1 * r.compareTo(l)) : l.compareTo(r);
625
626 if (result != 0) {
627 return result;
628 }
629 }
630
631 return 0;
632
633 default:
634 throw new IllegalStateException("invalid item: " + item.getClass());
635 }
636 }
637
638 @Override
639 public String toString() {
640 StringBuilder buffer = new StringBuilder();
641 for (Item item : this) {
642 if (buffer.length() > 0) {
643 buffer.append((item instanceof ListItem) ? '-' : '.');
644 }
645 buffer.append(item);
646 }
647 return buffer.toString();
648 }
649
650
651
652
653 private String toListString() {
654 StringBuilder buffer = new StringBuilder();
655 buffer.append("[");
656 for (Item item : this) {
657 if (buffer.length() > 1) {
658 buffer.append(", ");
659 }
660 if (item instanceof ListItem listItem) {
661 buffer.append(listItem.toListString());
662 } else {
663 buffer.append(item);
664 }
665 }
666 buffer.append("]");
667 return buffer.toString();
668 }
669 }
670
671 public ComparableVersion(String version) {
672 parseVersion(version);
673 }
674
675
676
677
678
679 @SuppressWarnings("checkstyle:innerassignment")
680 public final void parseVersion(String version) {
681 if (version.length() > MAX_VERSION_LENGTH) {
682 throw new IllegalArgumentException("Version string is too long (" + version.length() + " > "
683 + MAX_VERSION_LENGTH + " characters): "
684 + version.substring(0, 32) + "...");
685 }
686
687 this.value = version;
688
689 items = new ListItem();
690
691 version = version.toLowerCase(Locale.ENGLISH);
692
693 ListItem list = items;
694
695 Deque<Item> stack = new ArrayDeque<>();
696 stack.push(list);
697
698 boolean isDigit = false;
699
700 boolean isCombination = false;
701
702 int startIndex = 0;
703
704 for (int i = 0; i < version.length(); i++) {
705 char character = version.charAt(i);
706 int c = character;
707 if (Character.isHighSurrogate(character)) {
708
709 try {
710 char low = version.charAt(i + 1);
711 char[] both = {character, low};
712 c = Character.codePointAt(both, 0);
713 i++;
714 } catch (IndexOutOfBoundsException ex) {
715
716
717 }
718 }
719
720 if (c == '.') {
721 if (i == startIndex) {
722 list.add(IntItem.ZERO);
723 } else {
724 list.add(parseItem(isCombination, isDigit, version.substring(startIndex, i)));
725 }
726 isCombination = false;
727 startIndex = i + 1;
728 } else if (c == '-') {
729 if (i == startIndex) {
730 list.add(IntItem.ZERO);
731 } else {
732
733 if (!isDigit && i != version.length() - 1) {
734 char c1 = version.charAt(i + 1);
735 if (Character.isDigit(c1)) {
736 isCombination = true;
737 continue;
738 }
739 }
740 list.add(parseItem(isCombination, isDigit, version.substring(startIndex, i)));
741 }
742 startIndex = i + 1;
743
744 if (!list.isEmpty()) {
745 list.add(list = new ListItem());
746 stack.push(list);
747 }
748 isCombination = false;
749 } else if (c >= '0' && c <= '9') {
750 if (!isDigit && i > startIndex) {
751
752 isCombination = true;
753
754 if (!list.isEmpty()) {
755 list.add(list = new ListItem());
756 stack.push(list);
757 }
758 }
759
760 isDigit = true;
761 } else {
762 if (isDigit && i > startIndex) {
763 list.add(parseItem(isCombination, true, version.substring(startIndex, i)));
764 startIndex = i;
765
766 list.add(list = new ListItem());
767 stack.push(list);
768 isCombination = false;
769 }
770
771 isDigit = false;
772 }
773 }
774
775 if (version.length() > startIndex) {
776
777
778 if (!isDigit && !list.isEmpty()) {
779 list.add(list = new ListItem());
780 stack.push(list);
781 }
782
783 list.add(parseItem(isCombination, isDigit, version.substring(startIndex)));
784 }
785
786 while (!stack.isEmpty()) {
787 list = (ListItem) stack.pop();
788 list.normalize();
789 }
790 }
791
792 private static Item parseItem(boolean isDigit, String buf) {
793 return parseItem(false, isDigit, buf);
794 }
795
796 private static Item parseItem(boolean isCombination, boolean isDigit, String buf) {
797 if (isCombination) {
798 return new CombinationItem(buf.replace("-", ""));
799 } else if (isDigit) {
800 buf = stripLeadingZeroes(buf);
801 if (buf.length() <= MAX_INTITEM_LENGTH) {
802
803 return new IntItem(buf);
804 } else if (buf.length() <= MAX_LONGITEM_LENGTH) {
805
806 return new LongItem(buf);
807 }
808 return new BigIntegerItem(buf);
809 }
810 return new StringItem(buf, false);
811 }
812
813 private static String stripLeadingZeroes(String buf) {
814 if (buf == null || buf.isEmpty()) {
815 return "0";
816 }
817 for (int i = 0; i < buf.length(); ++i) {
818 char c = buf.charAt(i);
819 if (c != '0') {
820 return buf.substring(i);
821 }
822 }
823 return "0";
824 }
825
826 @Override
827 public int compareTo(ComparableVersion o) {
828 return items.compareTo(o.items);
829 }
830
831 @Override
832 public String toString() {
833 return value;
834 }
835
836 public String getCanonical() {
837 if (canonical == null) {
838 canonical = items.toString();
839 }
840 return canonical;
841 }
842
843 @Override
844 public boolean equals(Object o) {
845 return o instanceof ComparableVersion comparableVersion && items.equals(comparableVersion.items);
846 }
847
848 @Override
849 public int hashCode() {
850 return items.hashCode();
851 }
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866 int orderingHashCode() {
867 return orderingHash(items);
868 }
869
870 private static int orderingHash(Item item) {
871 return switch (item.getType()) {
872 case Item.LIST_ITEM -> {
873 ListItem list = (ListItem) item;
874 int end = list.size();
875
876 while (end > 0 && list.get(end - 1).compareTo(null) == 0) {
877 end--;
878 }
879 if (end == 1) {
880 yield orderingHash(list.get(0));
881 }
882 int hash = 1;
883 for (int i = 0; i < end; i++) {
884 hash = 31 * hash + orderingHash(list.get(i));
885 }
886 yield hash;
887 }
888
889 case Item.STRING_ITEM ->
890 StringItem.comparableQualifier(((StringItem) item).value).hashCode();
891 case Item.COMBINATION_ITEM -> {
892 CombinationItem combination = (CombinationItem) item;
893 if (combination.stringPart.compareTo(null) == 0 && combination.digitPart.isNull()) {
894 yield orderingHash(combination.stringPart);
895 }
896 yield 31
897 * StringItem.comparableQualifier(combination.stringPart.value)
898 .hashCode()
899 + orderingHash(combination.digitPart);
900 }
901
902 default -> item.hashCode();
903 };
904 }
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925 public static void main(String... args) {
926 System.out.println("Display parameters as parsed by Maven (in canonical form and as a list of tokens) and"
927 + " comparison result:");
928 if (args.length == 0) {
929 return;
930 }
931
932 ComparableVersion prev = null;
933 int i = 1;
934 for (String version : args) {
935 ComparableVersion c = new ComparableVersion(version);
936
937 if (prev != null) {
938 int compare = prev.compareTo(c);
939 System.out.println(
940 " " + prev + ' ' + ((compare == 0) ? "==" : ((compare < 0) ? "<" : ">")) + ' ' + version);
941 }
942
943 System.out.println(
944 (i++) + ". " + version + " -> " + c.getCanonical() + "; tokens: " + c.items.toListString());
945
946 prev = c;
947 }
948 }
949 }