View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
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   * A generic version, that is a version that accepts any input string and tries to apply common sense sorting. See
35   * {@link GenericVersionScheme} for details.
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       * Creates a generic version from the specified string.
47       *
48       * @param version The version string, must not be {@code null}.
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       * Returns this instance backing string representation.
58       *
59       * @since 1.9.5
60       */
61      public String asString() {
62          return version;
63      }
64  
65      /**
66       * Returns this instance tokenized representation as unmodifiable list.
67       *
68       * @since 1.9.5
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       * Visible for testing.
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                 // do not stop here, but continue, skipping non-number members
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                             // normalize numbers and strip leading zeros (prereq for Integer/BigInteger handling)
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; // i.e. kind != string/qualifier
348         }
349 
350         public int compareTo(Item that) {
351             int rel;
352             if (that == null) {
353                 // null in this context denotes the pad item (0 or "ga")
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 }