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