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