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