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