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