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