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  
27  import org.eclipse.aether.version.Version;
28  
29  import static java.util.Objects.requireNonNull;
30  
31  /**
32   * A generic version, that is a version that accepts any input string and tries to apply common sense sorting. See
33   * {@link GenericVersionScheme} for details.
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       * Creates a generic version from the specified string.
45       *
46       * @param version the version string, must not be {@code null}
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       * Returns this instance backing string representation.
56       *
57       * @since 1.9.5
58       */
59      public String asString() {
60          return version;
61      }
62  
63      /**
64       * Returns this instance tokenized representation as unmodifiable list.
65       *
66       * @since 1.9.5
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       * Visible for testing.
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                 // do not stop here, but continue, skipping non-number members
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') { // only ASCII digits
213                         int digit = c - '0';
214                         if (state == -1) {
215                             end = index;
216                             terminatedByNumber = true;
217                             break;
218                         }
219                         if (state == 0) {
220                             // normalize numbers and strip leading zeros (prereq for Integer/BigInteger handling)
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; // i.e. kind != string/qualifier
323         }
324 
325         public int compareTo(Item that) {
326             int rel;
327             if (that == null) {
328                 // null in this context denotes the pad item (0 or "ga")
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 }