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