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      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                 // do not stop here, but continue, skipping non-number members
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                             // normalize numbers and strip leading zeros (prereq for Integer/BigInteger handling)
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; // i.e. kind != string/qualifier
342         }
343 
344         public int compareTo(Item that) {
345             int rel;
346             if (that == null) {
347                 // null in this context denotes the pad item (0 or "ga")
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 }