1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.eclipse.aether.util.version;
20
21 import java.math.BigInteger;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.List;
25 import java.util.Locale;
26 import java.util.Map;
27 import java.util.TreeMap;
28
29 import org.eclipse.aether.version.Version;
30
31 import static java.util.Objects.requireNonNull;
32
33
34
35
36
37 final class GenericVersion implements Version {
38
39 private final String version;
40
41 private final List<Item> items;
42
43 private final int hash;
44
45
46
47
48
49
50 GenericVersion(String version) {
51 this.version = requireNonNull(version, "version cannot be null");
52 items = parse(version);
53 hash = items.hashCode();
54 }
55
56
57
58
59
60
61 public String asString() {
62 return version;
63 }
64
65
66
67
68
69
70 public List<Item> asItems() {
71 return items;
72 }
73
74 private static List<Item> parse(String version) {
75 List<Item> items = new ArrayList<>();
76
77 for (Tokenizer tokenizer = new Tokenizer(version); tokenizer.next(); ) {
78 Item item = tokenizer.toItem();
79 items.add(item);
80 }
81
82 trimPadding(items);
83
84 return Collections.unmodifiableList(items);
85 }
86
87
88
89
90 static void trimPadding(List<Item> items) {
91 Boolean number = null;
92 int end = items.size() - 1;
93 for (int i = end; i > 0; i--) {
94 Item item = items.get(i);
95 if (!Boolean.valueOf(item.isNumber()).equals(number)) {
96 end = i;
97 number = item.isNumber();
98 }
99 if (end == i
100 && (i == items.size() - 1 || items.get(i - 1).isNumber() == item.isNumber())
101 && item.compareTo(null) == 0) {
102 items.remove(i);
103 end--;
104 }
105 }
106 }
107
108 @Override
109 public int compareTo(Version obj) {
110 final List<Item> these = items;
111 final List<Item> those = ((GenericVersion) obj).items;
112
113 boolean number = true;
114
115 for (int index = 0; ; index++) {
116 if (index >= these.size() && index >= those.size()) {
117 return 0;
118 } else if (index >= these.size()) {
119 return -comparePadding(those, index, null);
120 } else if (index >= those.size()) {
121 return comparePadding(these, index, null);
122 }
123
124 Item thisItem = these.get(index);
125 Item thatItem = those.get(index);
126
127 if (thisItem.isNumber() != thatItem.isNumber()) {
128 if (index == 0) {
129 return thisItem.compareTo(thatItem);
130 }
131 if (number == thisItem.isNumber()) {
132 return comparePadding(these, index, number);
133 } else {
134 return -comparePadding(those, index, number);
135 }
136 } else {
137 int rel = thisItem.compareTo(thatItem);
138 if (rel != 0) {
139 return rel;
140 }
141 number = thisItem.isNumber();
142 }
143 }
144 }
145
146 private static int comparePadding(List<Item> items, int index, Boolean number) {
147 int rel = 0;
148 for (int i = index; i < items.size(); i++) {
149 Item item = items.get(i);
150 if (number != null && number != item.isNumber()) {
151
152 continue;
153 }
154 rel = item.compareTo(null);
155 if (rel != 0) {
156 break;
157 }
158 }
159 return rel;
160 }
161
162 @Override
163 public boolean equals(Object obj) {
164 return (obj instanceof GenericVersion) && compareTo((GenericVersion) obj) == 0;
165 }
166
167 @Override
168 public int hashCode() {
169 return hash;
170 }
171
172 @Override
173 public String toString() {
174 return version;
175 }
176
177 static final class Tokenizer {
178
179 private static final Integer QUALIFIER_ALPHA = -5;
180
181 private static final Integer QUALIFIER_BETA = -4;
182
183 private static final Integer QUALIFIER_MILESTONE = -3;
184
185 private static final Map<String, Integer> QUALIFIERS;
186
187 static {
188 QUALIFIERS = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
189 QUALIFIERS.put("alpha", QUALIFIER_ALPHA);
190 QUALIFIERS.put("beta", QUALIFIER_BETA);
191 QUALIFIERS.put("milestone", QUALIFIER_MILESTONE);
192 QUALIFIERS.put("cr", -2);
193 QUALIFIERS.put("rc", -2);
194 QUALIFIERS.put("snapshot", -1);
195 QUALIFIERS.put("ga", 0);
196 QUALIFIERS.put("final", 0);
197 QUALIFIERS.put("release", 0);
198 QUALIFIERS.put("", 0);
199 QUALIFIERS.put("sp", 1);
200 }
201
202 private final String version;
203
204 private final int versionLength;
205
206 private int index;
207
208 private String token;
209
210 private boolean number;
211
212 private boolean terminatedByNumber;
213
214 Tokenizer(String version) {
215 this.version = (!version.isEmpty()) ? version : "0";
216 this.versionLength = this.version.length();
217 }
218
219 public boolean next() {
220 if (index >= versionLength) {
221 return false;
222 }
223
224 int state = -2;
225
226 int start = index;
227 int end = versionLength;
228 terminatedByNumber = false;
229
230 for (; index < versionLength; index++) {
231 char c = version.charAt(index);
232
233 if (c == '.' || c == '-' || c == '_') {
234 end = index;
235 index++;
236 break;
237 } else {
238 int digit = Character.digit(c, 10);
239 if (digit >= 0) {
240 if (state == -1) {
241 end = index;
242 terminatedByNumber = true;
243 break;
244 }
245 if (state == 0) {
246
247 start++;
248 }
249 state = (state > 0 || digit > 0) ? 1 : 0;
250 } else {
251 if (state >= 0) {
252 end = index;
253 break;
254 }
255 state = -1;
256 }
257 }
258 }
259
260 if (end - start > 0) {
261 token = version.substring(start, end);
262 number = state >= 0;
263 } else {
264 token = "0";
265 number = true;
266 }
267
268 return true;
269 }
270
271 @Override
272 public String toString() {
273 return String.valueOf(token);
274 }
275
276 public Item toItem() {
277 if (number) {
278 try {
279 if (token.length() < 10) {
280 return new Item(Item.KIND_INT, Integer.parseInt(token));
281 } else {
282 return new Item(Item.KIND_BIGINT, new BigInteger(token));
283 }
284 } catch (NumberFormatException e) {
285 throw new IllegalStateException(e);
286 }
287 } else {
288 if (index >= version.length()) {
289 if ("min".equalsIgnoreCase(token)) {
290 return Item.MIN;
291 } else if ("max".equalsIgnoreCase(token)) {
292 return Item.MAX;
293 }
294 }
295 if (terminatedByNumber && token.length() == 1) {
296 switch (token.charAt(0)) {
297 case 'a':
298 case 'A':
299 return new Item(Item.KIND_QUALIFIER, QUALIFIER_ALPHA);
300 case 'b':
301 case 'B':
302 return new Item(Item.KIND_QUALIFIER, QUALIFIER_BETA);
303 case 'm':
304 case 'M':
305 return new Item(Item.KIND_QUALIFIER, QUALIFIER_MILESTONE);
306 default:
307 }
308 }
309 Integer qualifier = QUALIFIERS.get(token);
310 if (qualifier != null) {
311 return new Item(Item.KIND_QUALIFIER, qualifier);
312 } else {
313 return new Item(Item.KIND_STRING, token.toLowerCase(Locale.ENGLISH));
314 }
315 }
316 }
317 }
318
319 static final class Item {
320
321 static final int KIND_MAX = 8;
322
323 static final int KIND_BIGINT = 5;
324
325 static final int KIND_INT = 4;
326
327 static final int KIND_STRING = 3;
328
329 static final int KIND_QUALIFIER = 2;
330
331 static final int KIND_MIN = 0;
332
333 static final Item MAX = new Item(KIND_MAX, "max");
334
335 static final Item MIN = new Item(KIND_MIN, "min");
336
337 private final int kind;
338
339 private final Object value;
340
341 Item(int kind, Object value) {
342 this.kind = kind;
343 this.value = value;
344 }
345
346 public boolean isNumber() {
347 return (kind & KIND_QUALIFIER) == 0;
348 }
349
350 public int compareTo(Item that) {
351 int rel;
352 if (that == null) {
353
354 switch (kind) {
355 case KIND_MIN:
356 rel = -1;
357 break;
358 case KIND_MAX:
359 case KIND_BIGINT:
360 case KIND_STRING:
361 rel = 1;
362 break;
363 case KIND_INT:
364 case KIND_QUALIFIER:
365 rel = (Integer) value;
366 break;
367 default:
368 throw new IllegalStateException("unknown version item kind " + kind);
369 }
370 } else {
371 rel = kind - that.kind;
372 if (rel == 0) {
373 switch (kind) {
374 case KIND_MAX:
375 case KIND_MIN:
376 break;
377 case KIND_BIGINT:
378 rel = ((BigInteger) value).compareTo((BigInteger) that.value);
379 break;
380 case KIND_INT:
381 case KIND_QUALIFIER:
382 rel = ((Integer) value).compareTo((Integer) that.value);
383 break;
384 case KIND_STRING:
385 rel = ((String) value).compareToIgnoreCase((String) that.value);
386 break;
387 default:
388 throw new IllegalStateException("unknown version item kind " + kind);
389 }
390 }
391 }
392 return rel;
393 }
394
395 @Override
396 public boolean equals(Object obj) {
397 return (obj instanceof Item) && compareTo((Item) obj) == 0;
398 }
399
400 @Override
401 public int hashCode() {
402 return value.hashCode() + kind * 31;
403 }
404
405 @Override
406 public String toString() {
407 return String.valueOf(value);
408 }
409 }
410 }